spring boot:使用caffeine+redis做二級緩存(spring boot 2.3.1)


一,為什么要使用二級緩存?

我們通常會使用caffeine做本地緩存(或者叫做進程內緩存),

它的優點是速度快,操作方便,缺點是不方便管理,不方便擴展

而通常會使用redis作為分布式緩存,

它的優點是方便擴展,方便管理,但速度上肯定比本地緩存要慢一些,因為有網絡io

所以在生產環境中,我們通常把兩者都啟用,

這樣本地緩存做為一級緩存,雖然容量不夠大,但也可以把熱點數據緩存下來,

把高頻訪問攔截在redis的上游,

而redis做為二級緩存,把訪問請求攔截在數據庫的上游,

歸根到底,這樣可以更有效的減少到數據庫的訪問,

從而減輕數據庫的壓力,支持更高並發的訪問

 

說明:劉宏締的架構森林是一個專注架構的博客,地址:https://www.cnblogs.com/architectforest

         對應的源碼可以訪問這里獲取: https://github.com/liuhongdi/

說明:作者:劉宏締 郵箱: 371125307@qq.com

 

二,演示項目的相關信息

1,項目地址

https://github.com/liuhongdi/twocache

 

2,項目說明

我們在項目中使用了兩級緩存:

本地緩存的時間為60秒,過期后則從redis中取數據,

如果redis中不存在,則從數據庫獲取數據,

從數據庫得到數據后,要寫入到redis

 

3,項目結構:如圖

 

三,配置文件說明

 1,application.properties

#redis1
spring.redis1.host=127.0.0.1
spring.redis1.port=6379
spring.redis1.password=lhddemo
spring.redis1.database=0

spring.redis1.lettuce.pool.max-active=32
spring.redis1.lettuce.pool.max-wait=300
spring.redis1.lettuce.pool.max-idle=16
spring.redis1.lettuce.pool.min-idle=8

spring.redis1.enabled=1
        
#profile
spring.profiles.active=cacheenable

說明: spring.redis1.enabled=1: 用來控制redis是否生效

spring.profiles.active=cacheenable: 用來控制caffeine是否生效,

在測試環境中我們有時需要關閉緩存來調試數據庫,

在生產環境中如果緩存出現問題也有關閉緩存的需求,

所以要有相應的控制

 

2,mysql中的表結構:

CREATE TABLE `goods` (
 `goodsId` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
 `goodsName` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT 'name',
 `subject` varchar(200) NOT NULL DEFAULT '' COMMENT '標題',
 `price` decimal(15,2) NOT NULL DEFAULT '0.00' COMMENT '價格',
 `stock` int(11) NOT NULL DEFAULT '0' COMMENT 'stock',
 PRIMARY KEY (`goodsId`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='商品表'

 

四, java代碼說明

1,CacheConfig.java

@Profile("cacheenable")   //prod這個profile時緩存才生效
@Configuration
@EnableCaching //開啟緩存
public class CacheConfig {
    public static final int DEFAULT_MAXSIZE = 10000;
    public static final int DEFAULT_TTL = 600;

    private SimpleCacheManager cacheManager = new SimpleCacheManager();

     //定義cache名稱、超時時長(秒)、最大容量
    public enum CacheEnum{
        goods(60,1000),          //有效期60秒 , 最大容量1000
        homePage(7200,1000),  //有效期2個小時 , 最大容量1000
        ;
        CacheEnum(int ttl, int maxSize) {
            this.ttl = ttl;
            this.maxSize = maxSize;
        }
        private int maxSize=DEFAULT_MAXSIZE;    //最大數量
        private int ttl=DEFAULT_TTL;        //過期時間(秒)
        public int getMaxSize() {
            return maxSize;
        }
        public int getTtl() {
            return ttl;
        }
    }


    //創建基於Caffeine的Cache Manager
    @Bean
    @Primary
    public CacheManager caffeineCacheManager() {
        ArrayList<CaffeineCache> caches = new ArrayList<CaffeineCache>();
        for(CacheEnum c : CacheEnum.values()){
            caches.add(new CaffeineCache(c.name(),
                    Caffeine.newBuilder().recordStats()
                            .expireAfterWrite(c.getTtl(), TimeUnit.SECONDS)
                            .maximumSize(c.getMaxSize()).build())
            );
        }
        cacheManager.setCaches(caches);
        return cacheManager;
    }

    @Bean
    public CacheManager getCacheManager() {
        return cacheManager;
    }
}

作用:把定義的緩存添加到Caffeine

 

2,RedisConfig.java

@Configuration
public class RedisConfig {

    @Bean
    @Primary
    public LettuceConnectionFactory redis1LettuceConnectionFactory(RedisStandaloneConfiguration redis1RedisConfig,
                                                                    GenericObjectPoolConfig redis1PoolConfig) {
        LettuceClientConfiguration clientConfig =
                LettucePoolingClientConfiguration.builder().commandTimeout(Duration.ofMillis(100))
                        .poolConfig(redis1PoolConfig).build();
        return new LettuceConnectionFactory(redis1RedisConfig, clientConfig);
    }

    @Bean
    public RedisTemplate<String, String> redis1Template(
            @Qualifier("redis1LettuceConnectionFactory") LettuceConnectionFactory redis1LettuceConnectionFactory) {
        RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
        //使用Jackson2JsonRedisSerializer來序列化和反序列化redis的value值
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        //使用StringRedisSerializer來序列化和反序列化redis的key值
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        //開啟事務
        redisTemplate.setEnableTransactionSupport(true);
        redisTemplate.setConnectionFactory(redis1LettuceConnectionFactory);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    @Configuration
    public static class Redis1Config {
        @Value("${spring.redis1.host}")
        private String host;
        @Value("${spring.redis1.port}")
        private Integer port;
        @Value("${spring.redis1.password}")
        private String password;
        @Value("${spring.redis1.database}")
        private Integer database;

        @Value("${spring.redis1.lettuce.pool.max-active}")
        private Integer maxActive;
        @Value("${spring.redis1.lettuce.pool.max-idle}")
        private Integer maxIdle;
        @Value("${spring.redis1.lettuce.pool.max-wait}")
        private Long maxWait;
        @Value("${spring.redis1.lettuce.pool.min-idle}")
        private Integer minIdle;

        @Bean
        public GenericObjectPoolConfig redis1PoolConfig() {
            GenericObjectPoolConfig config = new GenericObjectPoolConfig();
            config.setMaxTotal(maxActive);
            config.setMaxIdle(maxIdle);
            config.setMinIdle(minIdle);
            config.setMaxWaitMillis(maxWait);
            return config;
        }

        @Bean
        public RedisStandaloneConfiguration redis1RedisConfig() {
            RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
            config.setHostName(host);
            config.setPassword(RedisPassword.of(password));
            config.setPort(port);
            config.setDatabase(database);
            return config;
        }
    }
}

作用:生成redis的連接,

注意對value的處理使用了Jackson2JsonRedisSerializer,否則不能直接保存一個對象

 

3, HomeController.java

    //商品詳情 參數:商品id
    @Cacheable(value = "goods", key="#goodsId",sync = true)
    @GetMapping("/goodsget")
    @ResponseBody
    public Goods goodsInfo(@RequestParam(value="goodsid",required = true,defaultValue = "0") Long goodsId) {
        Goods goods = goodsService.getOneGoodsById(goodsId);
        return goods;
    }

注意使用Cacheable這個注解來使本地緩存生效

 

4,GoodsServiceImpl.java

    @Override
    public Goods getOneGoodsById(Long goodsId) {
        Goods goodsOne;
        if (redis1enabled == 1) {
            System.out.println("get data from redis");
            Object goodsr = redis1Template.opsForValue().get("goods_"+String.valueOf(goodsId));
            if (goodsr == null) {
                System.out.println("get data from mysql");
                goodsOne = goodsMapper.selectOneGoods(goodsId);
                if (goodsOne == null) {
                    redis1Template.opsForValue().set("goods_"+String.valueOf(goodsId),"-1",600, TimeUnit.SECONDS);
                } else {
                    redis1Template.opsForValue().set("goods_"+String.valueOf(goodsId),goodsOne,600, TimeUnit.SECONDS);
                }
            } else {
                if (goodsr.equals("-1")) {
                    goodsOne = null;
                } else {
                    goodsOne = (Goods)goodsr;
                }
            }
        } else {
            goodsOne = goodsMapper.selectOneGoods(goodsId);
        }
        return goodsOne;
    }

作用:先從redis中得到數據,如果找不到則從數據庫中訪問,

注意做了redis1enabled是否==1的判斷,即:redis全局生效時,

才使用redis,否則直接訪問mysql

 

五,測試效果

1,訪問地址:

http://127.0.0.1:8080/home/goodsget?goodsid=3

查看控制台的輸出:

get data from redis
get data from mysql
costtime aop 方法doafterreturning:毫秒數:395

因為caffeine/redis中都沒有數據,可以看到程序從mysql中查詢數據

costtime aop 方法doafterreturning:毫秒數:0

再次刷新時,沒有從redis/mysql中讀數據,直接從caffeine返回,使用的時間不足1毫秒

get data from redis
costtime aop 方法doafterreturning:毫秒數:8

本地緩存過期后,可以看到數據在從redis中獲取,用時8毫秒

2,具體的緩存時間可以根據自己業務數據的更新頻率來確定 ,

     原則上:本地緩存的時長要比redis更短一些,

     因為redis中的數據我們通常會采用同步機制來更新,

     而本地緩存因為在各台web服務內部,
     所以時間上不要太長

 

六,查看spring boot的版本:

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.3.1.RELEASE)

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM