MySQL缓存策略详解

Posted Lion Long

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MySQL缓存策略详解相关的知识,希望对你有一定的参考价值。

mysql缓存方案


前提:读多写少,单个主节点能支撑项目数据量;数据的主要依据是mysql。

一、MySQL缓存方案目的分析

mysql 有自己缓冲层,它的作用也是用来缓存热点数据,这些数据包括索引、记录等。mysql 缓冲层是从自身出发,跟具体的业务无关。这里的缓冲策略主要是 lru。

mysql 数据主要存储在磁盘当中,适合大量重要数据的存储;磁盘当中的数据一般是远大于内存当中的数据。
一般业务场景的关系型数据库(mysql)是作为主要数据库的。

1.1、缓存层的作用

MySQL缓存方案用来缓存用户定义的热点数据 ,用户直接从缓存获取热点数据,降低数据库的读写压力。

1.2、缓存层选择

缓存数据库可以选用 redis,memcached;它们所有数据都存储在内存当中,当然也可以将内存当中的数据持久化到磁盘当中。

服务器 MySQL redis

1.3、场景分析

(1)内存访问速度是磁盘访问速度10W倍,访问磁盘的速度比较慢,尽量使获取数据是从内存中获取。
(2)读的需求远远大于写的需求。主要解决读的性能;因为写没必要优化,必须让数据正确的落盘。如果写性能出现问题,那么请使用横向扩展集群方式来解决。
(3)MySQL自身缓冲层跟业务无关。由于 mysql 的缓冲层不由用户来控制,也就是不能由用户来控制缓存具体数据。
(4)MySQL作为项目主要数据库,便于统计分析。项目中需要存储的数据应该远大于内存的容量,同时需要进行数据统计分析,所以数据存储获取的依据应该是关系型数据库。
(5)缓存数据库作为辅助数据库,存放热点数据。缓存数据库可以存储用户自定义的热点数据。

二、提升MySQL访问性能的方式

(1)读写分离。
(2)连接池。
(3)异步连接。

(4)预处理。
(5)更换存储引擎。
(6)分库分表。(淘汰的技术)
(7)mycat。(淘汰的技术)
(8)tidb。

2.1、MySQL主从复制

  1. 主库更新事件 ( update、insert、delete ) 通过 io-thread写到 binlog。
  2. 从库请求读取 binlog,通过 io-thread 写入从库本地 relaylog(中继日志)。
  3. 从库通过 sql-thread 读取 relay-log,并把更新事件在从库中重放(replay)一遍。

复制流程:

  1. Slave 上面的 IO 线程连接上 Master,并请求从指定日志文件的指定位置(或者从最开始的日志)之后的日志内容。
  2. Master 接收到来自 Slave 的 IO 线程的请求后,负责复制的IO 线程会根据请求信息读取日志指定位置之后的日志信息,返回给 Slave 的 IO 线程。返回信息中除了日志所包含的信息之外,还包括本次返回的信息已经到 Master 端的 binlog 文件的名称以及 binlog 的位置。
  3. Slave 的 IO 线程接收到信息后,将接收到的日志内容依次添加到 Slave 端的 relay-log 文件的最末端,并将读取到的
    Master 端的 binlog 的文件名和位置记录到master-info 文件中,以便在下一次读取的时候能够清楚的告诉 Master 从何
    处开始读取日志。
  4. Slave 的 sql 进程检测到 relay-log 中新增加了内容后,会马上解析 relay-log 的内容成为在 Master 端真实执行时候的那
    些可执行的内容,并在自身执行。

由于MySQL的主从复制是异步的,所以同一时刻主数据库和从数据库的数据可能存在不一致的现象,这就造成可能从数据库中读取的数据不是最新的。

2.2、读写分离

DML操作 复制 复制 复制 复制 应用层

读写分离会设置多个从数据库,从数据库可能会在多个机器中。

写操作依然在主数据库中,主数据库提供数据的主要依据。

读写分离通过设置多个从数据库解决读压力。

读写分离主要依据MySQL的主从复制原理,因为MySQL的主从复制是异步复制的,所以读写分离只能保证数据的最终一致性,不能保证实时一致性。
如果读操作有强一致性要求,那么需要读操作去读主数据库。

2.3、连接池

连接池的定义:在服务端当中创建多个与数据库的连接线程。
解决的问题:并发提升数据库访问性能;同时复用连接,避免连接建立、断开依据安全验证的开销。
原理:利用MySQL的网络模型创建多个连接,每个连接复用去出路SQL语句。值得注意的是,如果发送一个事务(多条SQL语句),这个事务必须要在一个连接里面完成。

2.4、异步连接

在服务端创建一个连接,针对这个连接采用非阻塞IO。这种方式可以节省网络传输时间。

三、redis作为主数据库的常用方法

  1. 以redis为主,在redis中读写数据,MySQL作为数据备份,过程中可能需要分布式消息队列(kafka)进行异步同步。这种方式性能最高,但安全性较差。仅适合小项目。工程中要会在效率和安全直接做权衡。
数据备份 server MySQL redis kafka
  1. 针对redis持久化较差的情况,最早使用leveldb伪装成从数据库,不断从redis中获取数据来实时持久化。
获取数据 返回数据 server redis leveldb 持久化

随着技术提升,leveldb的方式被淘汰,使用了更完善的pika方式。pika内部使用的rocksdb,支持redis协议。

server pika

四、缓存方案

4.1、缓存和MySQL一致性状态分析

没有缓冲层之前,对数据的读写都是基于 mysql;所以不存在同步问题;这句话也不是必然,比如读写分离就存在同步问题(数据一致性问题)。
引入缓冲层后,对数据的获取需要分别操作缓存数据库和mysql,那么这个时候数据可能存在以下状态:

  1. mysql 有,缓存无。
  2. mysql 无,缓存有。
  3. 都有,但数据不一致。
  4. 都有,数据一致。
  5. 都没有。
缓存和MySQL一致性状态分析 MySQL有,redis无

本地缓存Caffeine详解+整合SpringBoot的@EnableCaching

目录

前言:

Caffeine详解

加载策略

同步

异步,即多线程加载

回收策略

回收策略-数量

回收策略-权重

回收策略-时间

回收策略-软引用/弱引用

移除监听

统计

整合SpringBoot

@EnableCaching:启用缓存功能

@Cacheable

@CacheEvict

@CachePut

通过Spring配置定义CacheManager


前言:


分布式缓存大多比较熟悉的有Memcached、Redis,提到本地缓存大多数人还在创建Map来存储,作为新时代的农民工显然是不能接受的,本文为大家推荐一个堪称本地缓存之王的一个本地存储框架Caffeine,以及与SpringBoot的@EnableCaching进行整合使用。

先来说一说它有哪些能力:

  1. 当基于频率和最近访问的缓存达到最大容量时,该组件会自动切换到基于大小的模式(这就是他称王的主要原因,原自它使用的算法W-TinyLFU,想深究的可以自行去了解下,这里提供下视频,讲的非常好1009_【理解】Caffeine数据存储结构_哔哩哔哩_bilibili
  2. 能够将数据存储到本地缓存中(废话)
  3. 能够实现多种缓存过期策略(数量、大小、时间、引用)
  4. 能够时间对消息过期的监听(第一个想到的类似RabbitMq里面的死信队列)
  5. 能够将缓存的内容转存到外部存储(暂时感觉没啥大用)
  6. 能够自动统计缓存信息,命中率等,便于调优
  7. 非常方便整合到SpringBoot中

Caffeine详解


For Java 11 or above, use 3.x otherwise use 2.x

官方提示java11或以上的用3.X,其他的用2.X

<dependency>
   <groupId>com.github.ben-manes.caffeine</groupId>
   <artifactId>caffeine</artifactId>
   <version>2.6.2</version>
</dependency>
  • 加载策略

    • 同步

      •     @Test
            void method3() throws InterruptedException 
                LoadingCache<String, String> cache = Caffeine.newBuilder()
                        .maximumSize(100)
                        .expireAfterAccess(3L, TimeUnit.SECONDS) // 无访问3秒后失效
                        .build((key) -> 
                            Thread.sleep(1000);
                            logger.info("LoadingCache reload:"+key);
                            return key+"value";
                        );
        
                cache.put("key1","samples1");
                cache.put("key2","samples2");
                logger.info("未失效获取key1:"+cache.getIfPresent("key1"));
                logger.info("未失效获取key2:"+cache.getIfPresent("key2"));
                Thread.sleep(4000L);
                List<String> list = new ArrayList<>();
                list.add("key1");
                list.add("key2");
                cache.getAll(list);
                logger.info(cache.getIfPresent("key1"));
                logger.info(cache.getIfPresent("key2"));
            

        11:18:21.194  10376 -[main] pplicationTests: 未失效获取key1:samples1
        11:18:21.195  10376 -[main] pplicationTests: 未失效获取key2:samples2
        11:18:26.220  10376 -[main] pplicationTests: LoadingCache reload:key1
        11:18:27.234  10376 -[main] pplicationTests: LoadingCache reload:key2
        11:18:27.234  10376 -[main] pplicationTests: key1value
        11:18:27.234  10376 -[main] pplicationTests: key2value

        同步加载即在缓存失效的情况下进行单线程加载,下面我们看一下异步加载

    • 异步,即多线程加载

      •     @Test
            void method4() throws Exception 
                AsyncLoadingCache<Object, String> cache = Caffeine.newBuilder()
                        .maximumSize(100)
                        .expireAfterAccess(3L, TimeUnit.SECONDS) // 无访问3秒后失效
                        .buildAsync((key, executer) ->
                                CompletableFuture.supplyAsync(() -> 
                                    try 
                                        Thread.sleep(1000);
                                     catch (InterruptedException e) 
                                        throw new RuntimeException(e);
                                    
                                    logger.info("LoadingCache reload:" + key);
                                    return key + "value";
                                ));
        
                cache.put("key1",CompletableFuture.completedFuture("samples1"));
                cache.put("key2",CompletableFuture.completedFuture("samples2"));
                logger.info("未失效获取:"+cache.getIfPresent("key1").get());
                logger.info("未失效获取:"+cache.getIfPresent("key2").get());
                Thread.sleep(4000L);
                List<String> list = new ArrayList<>();
                list.add("key1");
                list.add("key2");
                cache.getAll(list).get();
                logger.info("异步加载完成前:"+cache.getIfPresent("key1").get());
                Thread.sleep(2000L);
                logger.info("异步加载完成后:"+cache.getIfPresent("key1").get());
            

        11:23:44.820 [           main] pplicationTests: 未失效获取:samples1
        11:23:44.821 [           main] pplicationTests: 未失效获取:samples2
        11:23:49.851 [onPool-worker-1] pplicationTests: LoadingCache reload:key1
        11:23:49.851 [onPool-worker-2] pplicationTests: LoadingCache reload:key2
        11:23:49.851 [           main] pplicationTests: 异步加载完成前:key1value
        11:23:51.866 [           main] pplicationTests: 异步加载完成后:key1value

        异步加载较为繁琐,需要通过CompletableFuture进行包装,但是我们可以看到,多线程同时进行加载

  • 回收策略

    • 回收策略-数量

      •     @Test
            void method111() throws InterruptedException 
                Cache<String, String> cache = Caffeine.newBuilder()
                        .maximumSize(2)
                        .build();
                cache.put("key1","samples1");
                cache.put("key2","samples2");
                // 在未超过最大值的时候进行获取,结果应该是正常的
                String key1Value = cache.getIfPresent("key1");
                logger.info("未超过最大值获取key1Value:"+key1Value);
                String key2Value = cache.getIfPresent("key2");
                logger.info("未超过最大值获取key2Value:"+key2Value);
                cache.put("key3","samples3");
                // 此时已经超过最大值,但是缓存不会立马删除,所以需要等待100毫秒,再看结果
                Thread.sleep(100L);
                String key1 = cache.getIfPresent("key1");
                logger.info("超过最大值获取key1Value:"+key1);
                String key2 = cache.getIfPresent("key2");
                logger.info("超过最大值获取key2Value:"+key2);
                String key3 = cache.getIfPresent("key3");
                logger.info("超过最大值获取key3Value:"+key3);
            

        c.e.c.CaffeinedemoApplicationTests       : 未超过最大值获取key1Value:samples1
        c.e.c.CaffeinedemoApplicationTests       : 未超过最大值获取key2Value:samples2
        c.e.c.CaffeinedemoApplicationTests       : 超过最大值获取key1Value:null
        c.e.c.CaffeinedemoApplicationTests       : 超过最大值获取key2Value:samples2
        c.e.c.CaffeinedemoApplicationTests       : 超过最大值获取key3Value:samples3

      • 最大容量数量建议在使用缓存的时候都加上,具体原因无非一个扩容带来的性能问题,二个本身占用内存的限制,

      • 另外回收策略基于最大容量和基于权重不能同时存在

    • 回收策略-权重

      •     @Test
            void method1_1() throws InterruptedException 
                Cache<String, String> cache = Caffeine.newBuilder()
                        .maximumWeight(100)
                        .weigher((key,value) -> 
                            if(key.toString().contains("key"))
                                return 20;
                             else if (key.toString().contains("wang"))
                                return 80;
                             else 
                                return 40;
                            
                        )
                        .build();
                cache.put("key1","samples1");
                cache.put("key2","samples2");
                cache.put("zhang1","zhang1");
                logger.info(cache.getIfPresent("key1"));
                logger.info(cache.getIfPresent("key2"));
                logger.info(cache.getIfPresent("zhang1"));
                logger.info("===============");
                cache.put("wang","wang");
                Thread.sleep(100L);
                logger.info(cache.getIfPresent("key1"));
                logger.info(cache.getIfPresent("key2"));
                logger.info(cache.getIfPresent("zhang1"));
                logger.info(cache.getIfPresent("wang"));
            

        c.e.c.CaffeinedemoApplicationTests       : samples1
        c.e.c.CaffeinedemoApplicationTests       : samples2
        c.e.c.CaffeinedemoApplicationTests       : zhang1
        c.e.c.CaffeinedemoApplicationTests       : ===============
        c.e.c.CaffeinedemoApplicationTests       : samples1
        c.e.c.CaffeinedemoApplicationTests       : null
        c.e.c.CaffeinedemoApplicationTests       : null
        c.e.c.CaffeinedemoApplicationTests       : wang

        基于权重即可以针对不同的key设置不同的权重,当权重达到最大值的时候,会进行清空部分内容

    • 回收策略-时间

      •     @Test
            void method1() throws InterruptedException 
                Cache<String, String> cache = Caffeine.newBuilder()
                        .maximumSize(100)
        //              .expireAfterWrite(3L,TimeUnit.SECONDS)  // 创建后3秒失效
                        .expireAfterAccess(3L, TimeUnit.SECONDS) // 无访问3秒后失效
                        .build();
                cache.put("key1","samples1");
                Thread.sleep(2000L);
                String value1 = cache.getIfPresent("key1");
                logger.info("过两秒钟value1:"+value1);
                Thread.sleep(2000L);
                String value2 = cache.getIfPresent("key1");
                logger.info("再过两秒钟value2:"+value2);
                Thread.sleep(4000L);
                String value3 = cache.getIfPresent("key1");
                logger.info("再过4秒钟value3:"+value3);
            

         c.e.c.CaffeinedemoApplicationTests       : 过两秒钟value1:samples1
         c.e.c.CaffeinedemoApplicationTests       : 再过两秒钟value2:samples1
         c.e.c.CaffeinedemoApplicationTests       : 再过4秒钟value3:null

        上面是基于某个缓存固定的数据过期策略,如果我想要实现不同的数据指定过期时间怎么搞?

      •     @Test
            void method1_2() throws InterruptedException 
                Cache<String, String> cache = Caffeine.newBuilder()
                        .maximumSize(100)
                        .expireAfter(new Expiry<String, String>() 
                            /**
                             * 创建后过期策略
                             * @param key
                             * @param value
                             * @param currentTime 当前时间往后算 多长时间过期
                             * @return
                             */
                            @Override
                            public long expireAfterCreate(@NonNull String key, @NonNull String value, long currentTime) 
                                logger.info("expireAfterRead expireAfterCreate:key",key);
                                return currentTime;
                            
        
                            /**
                             * 更新后过期策略
                             * @param key
                             * @param value
                             * @param currentTime  当前时间往后算 多长时间过期
                             * @param currentDuration  剩余多长时间过期
                             * @return
                             */
                            @Override
                            public long expireAfterUpdate(@NonNull String key, @NonNull String value, long currentTime, @NonNegative long currentDuration) 
                                logger.info("expireAfterRead expireAfterUpdate:key,value:",key,value);
                                return currentDuration;
                            
                            /**
                             * 读取后过期策略
                             * @param key
                             * @param value
                             * @param currentTime  当前时间往后算 多长时间过期
                             * @param currentDuration  剩余多长时间过期
                             * @return
                             */
                            @Override
                            public long expireAfterRead(@NonNull String key, @NonNull String value, long currentTime, @NonNegative long currentDuration) 
                                logger.info("expireAfterRead expireAfterRead:key",key);
                                return currentDuration;
                            
                        )
                        .build();
                // 指定单条数据过期时间
                cache.policy().expireVariably().ifPresent(policy -> 
                            policy.put("key","value",5, TimeUnit.SECONDS);
                        );
                logger.info("第一次获取:",cache.getIfPresent("key"));
                Thread.sleep(3000L);
                cache.put("key","value1");
                Thread.sleep(3000L);
                logger.info("第二次获取:",cache.getIfPresent("key"));
            

        c.e.c.CaffeinedemoApplicationTests       : expireAfterRead expireAfterRead:keykey
        c.e.c.CaffeinedemoApplicationTests       : 第一次获取:value
        c.e.c.CaffeinedemoApplicationTests       : expireAfterRead expireAfterUpdate:keykey,value:value1
        c.e.c.CaffeinedemoApplicationTests       : 第二次获取:null

      • 这块需要大家手动去跑一下看看,尝试一下currentTime和currentDuration分别代表啥意思,就拿上面代码距离,currentTime就是指每次更新或者读取过后,再过5秒失效,currentDuration就是指每次更新获取读取过后,再过(5 - X)秒后失效。

      • 此方法不和expireAfterWrite以及expireAfterAccess同时使用,如果你想要固定时间过期并且记录每次操作记录,可以在里面重写的三个方法里面写死即可,其他的(获取/塞值)操作不变

    • 回收策略-软引用/弱引用

      •     @Test
            void method132() throws InterruptedException 
                Cache<String, String> cache = Caffeine.newBuilder()
                        .maximumSize(100)
                        .expireAfterAccess(3L, TimeUnit.SECONDS) // 无访问3秒后失效
                        .weakKeys()
                        .weakValues()
                        .build();
        
                String key = new String("key");
                String value = new String("samples1");
                cache.put(key,value);
                key = null;
                value = null;
                logger.info("未超过最大值获取key1Value:"+cache.getIfPresent("key"));
            

        c.e.c.CaffeinedemoApplicationTests       : 未超过最大值获取key1Value:null

            @Test
            void method132() throws InterruptedException 
                Cache<String, String> cache = Caffeine.newBuilder()
                        .maximumSize(100)
                        .expireAfterAccess(3L, TimeUnit.SECONDS) // 无访问3秒后失效
        //                .weakKeys()
        //                .weakValues()
                        .build();
        
                String key = new String("key");
                String value = new String("samples1");
                cache.put(key,value);
                key = null;
                value = null;
                logger.info("未超过最大值获取key1Value:"+cache.getIfPresent("key"));
            

        c.e.c.CaffeinedemoApplicationTests       : 未超过最大值获取key1Value:samples1

        如上述,如果使用了弱引用,对象如果被回收掉(本文通过赋空置模拟),则缓存中也会丢失

      • 注:AsyncLoadingCache不支持弱引用和软引用

      • 注:weakValues()和softValues()不可以一起使用

      • Caffeine.weakKeys():使用弱引用存储key。如果没有其他地方对该key有强引用,那么该缓存就会被垃圾回收器回收。由于垃圾回收器只依赖于身份(identity)相等,因此这会导致整个缓存使用身份 (==) 相等来比较 key,而不是使用 equals()

      • Caffeine.weakValues() :使用弱引用存储value。如果没有其他地方对该value有强引用,那么该缓存就会被垃圾回收器回收。由于垃圾回收器只依赖于身份(identity)相等,因此这会导致整个缓存使用身份 (==) 相等来比较 key,而不是使用 equals()

      • Caffeine.softValues() :使用软引用存储value。当内存满了过后,软引用的对象以将使用最近最少使用(least-recently-used ) 的方式进行垃圾回收。由于使用软引用是需要等到内存满了才进行回收,所以我们通常建议给缓存配置一个使用内存的最大值。softValues() 将使用身份相等(identity) (==) 而不是equals() 来比较值

  • 移除监听

    •     @Test
          void method5() throws InterruptedException 
              Cache<String, String> cache = Caffeine.newBuilder()
                      .maximumSize(100)
                      .expireAfterWrite(3L, TimeUnit.SECONDS)
                      .removalListener(new RemovalListener<String, String>() 
                          @Override
                          public void onRemoval(@Nullable String s, @Nullable String s2, @NonNull RemovalCause removalCause) 
                              logger.info("evictionListener key remove:"+s +"===="+removalCause);
                          
                      )
                      .build();
              cache.put("key1","samples1");
              String key1 = cache.getIfPresent("key1");
              logger.info("未失效获取:"+key1);
              cache.invalidate("key1");
              cache.put("key2","samples2");
              Thread.sleep(4000L);
              logger.info("未失效获取:"+cache.getIfPresent("key2"));
          

      CaffeinedemoApplicationTests: 未失效获取:samples1
      CaffeinedemoApplicationTests: evictionListener key remove:key1====EXPLICIT
      CaffeinedemoApplicationTests: 未失效获取:null
      CaffeinedemoApplicationTests: evictionListener key remove:key2====EXPIRED

      这个例子列举了两种情况,一种情况是手动清除,则会立即被removalListener监听到,还有一种情况是等待过期自动清除,这时则需要等到下次调用的时候才会被removalListener监听到

  • 统计

    •     @Test
          void method6() throws InterruptedException 
              Cache<String, String> cache = Caffeine.newBuilder()
                      .maximumSize(100)
                      .expireAfterAccess(3L, TimeUnit.SECONDS) // 无访问3秒后失效
                      .recordStats()
                      .build();
              cache.put("key1","samples1");
              cache.getIfPresent("key1");
              cache.getIfPresent("key1");
              Thread.sleep(5000L);
              cache.getIfPresent("key1");
      
              logger.info(cache.stats().hitCount()+"");
              logger.info(cache.stats().requestCount()+"");
              logger.info(cache.stats().hitRate()+"");
          

      c.e.c.CaffeinedemoApplicationTests: 2
      c.e.c.CaffeinedemoApplicationTests: 3
      c.e.c.CaffeinedemoApplicationTests: 0.6666666666666666

       

整合SpringBoot


<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>2.6.2</version>
</dependency>
  • @EnableCaching:启用缓存功能

    • 开启缓存功能,配置类中需要加上这个注解,有了这个注解之后,spring才知道你需要使用缓存的功能,其他和缓存相关的注解才会有效,spring中主要是通过aop实现的,通过aop来拦截需要使用缓存的方法,实现缓存的功能
    • package com.xxx.xxx.app.config;
      
      import lombok.extern.slf4j.Slf4j;
      import org.springframework.cache.annotation.EnableCaching;
      import org.springframework.context.annotation.ComponentScan;
      import org.springframework.context.annotation.Configuration;
      
      /**
       * @author pengzf
       * @date 2022/11/9 9:36
       * @discribe
       */
      @Slf4j
      @EnableCaching
      @ComponentScan
      @Configuration
      public class CacheManagerConfig 
      
          @Primary
          @Bean(name = "localEntityCacheManager")
          public CacheManager localEntityCacheManager()
              CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager();
              Caffeine caffine = Caffeine.newBuilder()
                      .initialCapacity(10)  // 初始大小
                      .maximumSize(100)     // 最大容量
                      .recordStats()        // 打开统计
                      .expireAfterAccess(5, TimeUnit.MINUTES);  // 5分钟不访问自动丢弃
      //              .executor(ThreadPoolUtil.getThreadPool()); // 走线程池,需自定义线程池,可不用
              caffeineCacheManager.setCaffeine(caffine);
              caffeineCacheManager.setCacheNames(getNames());  // 设定缓存器名称
              caffeineCacheManager.setAllowNullValues(false);  // 值不可为空
              return caffeineCacheManager;
          
      
          private static List<String> getNames() 
              List<String> names = new ArrayList<>(1);
              names.add("localEntityCache");
              return names;
          
      
      
      
    • 注:

      initialCapacity=[integer]: 初始的缓存空间大小
      maximumSize=[long]: 缓存的最大条数
      maximumWeight=[long]: 缓存的最大权重
      expireAfterAccess=[duration]: 最后一次写入或访问后经过固定时间过期
      expireAfterWrite=[duration]: 最后一次写入后经过固定时间过期
      refreshAfterWrite=[duration]: 创建缓存或者最近一次更新缓存后经过固定的时间间隔,刷新缓存
      weakKeys: 打开key的弱引用
      weakValues:打开value的弱引用
      softValues:打开value的软引用
      recordStats:开发统计功能
      注意:
      expireAfterWrite和expireAfterAccess同时存在时,以expireAfterWrite为准。
      maximumSize和maximumWeight不可以同时使用
      weakValues和softValues不可以同时使用

  • @Cacheable

    • 触发缓存入口(这里一般放在创建和获取的方法上,@Cacheable注解会先查询是否已经有缓存,有会使用缓存,没有则会执行方法并缓存)
    •     @Cacheable(cacheNames = "localEntityCache",key = "'info'+#info.id",unless = "#result==null" , condition = "#cache")
          public String addCache(PlatformStatusEntity info,boolean cache)
              log.info("TestService addCache:",info.toString());
              return "cachevalue1";
          

      注:这里面(key、unless等)会用到一些SPEL表达式,Spring为我们提供了一个root对象可以用来生成key,通过该root对象我们可以获取到以下信息

      cacheNames:用来指定缓存名称,可以指定多个

      key:缓存的key,spel表达式,写法参考@Cacheable中的key

      condition:spel表达式,写法和@Cacheable中的condition一样,当为空或者计算结果为true的时候,方法的返回值才会丢到缓存中;否则结果不会丢到缓存中

      unless:当condition为空或者计算结果为true的时候,unless才会起效;true:结果不会被丢到缓存,false:结果会被丢到缓存

    • SPEL简单介绍

      类型运算符

      关系

      <,>,<=,>=,==,!=,lt,gt,le,ge,eq,ne
      算术+,- ,* ,/,%,^
      逻辑&&,||,!,and,or,not,between,instanceof
      条件?: (ternary),?: (elvis)
      正则表达式
      其他类型?.,?[…],![…],^[…],$[…]
      属性名称 描述 示例 methodName 当前方法名 #root.methodName method 当前方法 #root.method.name target 当前被调用的对象 #root.target targetClass 当前被调用的对象的class #root.targetClass args 当前方法参数组成的数组 #root.args[0] caches 当前被调用的方法使用的Cache #root.caches[0].name

 

 

 

 

 

 

 

  • @CacheEvict

    • 用来清除缓存的,@CacheEvict也可以标注在类或者方法上,被标注在方法上,则目标方法被调用的时候,会清除指定的缓存;当标注在类上,相当于在类的所有方法上标注了
    •     @CacheEvict(cacheNames = "localEntityCache",key = "'info'+#info.id")
          public void removeCache(PlatformStatusEntity info)
              log.info("TestService removeCache:",info.toString());
          

  • @CachePut

    • 可以标注在类或者方法上,被标注的方法每次都会被调用,然后方法执行完毕之后,会将方法结果丢到缓存中;当标注在类上,相当于在类的所有方法上标注了
    •     @CachePut(cacheNames = "localEntityCache",key = "'info'+#info.id",unless = "#result==null" , condition = "#cache")
          public String updateCache(PlatformStatusEntity info,boolean cache)
              log.info("TestService addCache:",info.toString());
              return "cachevalue1";
          

  • 通过Spring配置定义CacheManager

    •                         
      spring:
        cache:
          cache-names: localEntityCache
          caffeine:
            spec: initialCapacity=50,maximumSize=100,expireAfterWrite=10m
          type: caffeine
    • package com.xxx.xxx.app.config;
      
      import lombok.extern.slf4j.Slf4j;
      import org.springframework.cache.annotation.EnableCaching;
      import org.springframework.context.annotation.ComponentScan;
      import org.springframework.context.annotation.Configuration;
      
      /**
       * @author pengzf
       * @date 2022/11/9 9:36
       * @discribe
       */
      @Slf4j
      @EnableCaching
      @ComponentScan
      @Configuration
      public class CacheManagerConfig 
      
          @Value("$spring.cache.caffeine.spec")
          private String caffeineSpec;
      
          @Primary
          @Bean(name = "localEntityCacheManager")
          public CacheManager localEntityCacheManager() 
              CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager();
              Caffeine caffeine = Caffeine.from(caffeineSpec)
                      .executor(ThreadPoolUtil.getThreadPool());
              caffeineCacheManager.setCaffeine(caffeine);
              caffeineCacheManager.setAllowNullValues(false);
              return caffeineCacheManager;
          
      
      
      

      同样的效果。

注:上述代码均手动执行过,与SpringBoot集成测试东西较多,但是功能均已实现。

以上是关于MySQL缓存策略详解的主要内容,如果未能解决你的问题,请参考以下文章

「mysql优化专题」详解引擎(InnoDB,MyISAM)的内存优化攻略?

游戏服务器缓存策略

MySQL“写缓冲“优化策略

MySQL“写缓冲“优化策略

MySQL“写缓冲“优化策略

MySQL查询缓存详解