一,為什么要使用二級緩存?
我們通常會使用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)