作者 | Garnett
來源 | Garnett的Java之路(ID:gh_009246af52d4)
頭圖 | CSDN 下載自東方IC
前言
在之前我們介紹了如何使用Redis或者Caffeine來做緩存,那么肯定會有人問,我用了 redis 已經很快了,為什么還要結合使用其他的緩存呢,緩存最大的作用確實是提高效率,但是隨著業務需求的發展,業務體量的增大,多級緩存的作用就凸顯了出來,接下來讓我們盯緊了哦!
為什么要用多級緩存?
如果只使用redis來做緩存我們會有大量的請求到redis,但是每次請求的數據都是一樣的,假如這一部分數據就放在應用服務器本地,那么就省去了請求redis的網路開銷,請求速度就會快很多。但是使用redis橫向擴展很方便。
如果只使用Caffeine來做本地緩存,我們的應用服務器的內存是有限,并且單獨為了緩存去擴展應用服務器是非常不劃算。所以,只使用本地緩存也是有很大局限性的。
至此我們是不是有一個想法了,兩個一起用,將熱點數據放本地緩存(一級緩存),將非熱點數據放redis緩存(二級緩存),
1、緩存的選擇
一級緩存:Caffeine是一個一個高性能的 Java 緩存庫;使用 Window TinyLfu 回收策略,提供了一個近乎最佳的命中率。
二級緩存:redis是一高性能、高可用的key-value資料庫,支持多種數據類型,支持集群,和應用服務器分開部署易于橫向擴展。
2、數據流向數據讀取流程
數據刪除流程
解決思路
Spring 本來就提供了Cache的支持,最核心的就是實現Cache和CacheManager 接口,
實戰多級緩存的用法
以下演示項目的代碼在公眾號【 Garnett的Java之路】 后臺回復【多級緩存】可以自取哦!
1、項目說明
我們在項目中使用了兩級緩存
本地緩存的時間為60秒,過期后則從redis中取數據,
如果redis中不存在,則從資料庫獲取數據,
從資料庫得到數據后,要寫入到redis
2、項目結構
3、配置文件說明
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是否生效,
在測試環境中我們有時需要關閉緩存來調試資料庫,
在生產環境中如果緩存出現問題也有關閉緩存的需求,
所以要有相應的控制
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='商品表'
4、Java代碼說明
CacheConfig.java
@Profile("cacheenable") http://prod這個profile時緩存才生效
@Configuration
@EnableCaching http://開啟緩存
public class CacheConfig {
public static final int DEFAULT_MAXSIZE = 10000;
public static final int DEFAULT_TTL = 600;
private SimpleCacheManager cacheManager = new SimpleCacheManager();
http://定義cache名稱、超時時長(秒)、最大容量
public enum CacheEnum{
goods(60,1000), http://有效期60秒 , 最大容量1000
homePage(7200,1000), http://有效期2個小時 , 最大容量1000
;
CacheEnum(int ttl, int maxSize) {
this.ttl = ttl;
this.maxSize = maxSize;
}
private int maxSize=DEFAULT_MAXSIZE; http://最大數量
private int ttl=DEFAULT_TTL; http://過期時間(秒)
public int getMaxSize() {
return maxSize;
}
public int getTtl() {
return ttl;
}
}
http://創建基于Caffeine的Cache Manager
@Bean
@Primary
public CacheManager caffeineCacheManager() {
ArrayList caches = new ArrayList();
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
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 redis1Template(
@Qualifier("redis1LettuceConnectionFactory") LettuceConnectionFactory redis1LettuceConnectionFactory) {
RedisTemplate redisTemplate = new RedisTemplate<>();
http://使用Jackson2JsonRedisSerializer來序列化和反序列化redis的value值
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
http://使用StringRedisSerializer來序列化和反序列化redis的key值
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
http://開啟事務
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的連接
HomeController.java
http://商品詳情 參數:商品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這個注解來使本地緩存生效
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
5、測試效果
訪問地址:
http: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毫秒
具體的緩存時間可以根據自己業務數據的更新頻率來確定 ,原則上:本地緩存的時長要比redis更短一些,因為redis中的數據我們通常會采用同步機制來更新, 而本地緩存因為在各臺web服務內部,所以時間上不要太長!
總結
本文介紹了多級緩存的原理以及用法,通過這些知識的介紹相信你也收獲了不少。希望這篇文章可以帶你了解多級緩存,知道在什么場景下可以使用!