spring boot:使用caffeine+redis做二級快取(spring boot 2.3.1)
一,為什麼要使用二級快取?
我們通常會使用caffeine做本地快取(或者叫做程序內快取),
它的優點是速度快,操作方便,缺點是不方便管理,不方便擴充套件
而通常會使用redis作為分散式快取,
它的優點是方便擴充套件,方便管理,但速度上肯定比本地快取要慢一些,因為有網路io
所以在生產環境中,我們通常把兩者都啟用,
這樣本地快取做為一級快取,雖然容量不夠大,但也可以把熱點資料快取下來,
把高頻訪問攔截在redis的上游,
而redis做為二級快取,把訪問請求攔截在資料庫的上游,
歸根到底,這樣可以更有效的減少到資料庫的訪問,
從而減輕資料庫的壓力,支援更高併發的訪問
說明:劉巨集締的架構森林是一個專注架構的部落格,地址:
對應的原始碼可以訪問這裡獲取:https://github.com/liuhongdi/
說明:作者:劉巨集締 郵箱: [email protected]
二,演示專案的相關資訊
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)