当前位置:网站首页>Redis In Action —— Redis Cache Client 工具类封装 —— 封装了针对于缓存穿透、缓存击穿等问题的优化 —— 缓存空值数据|缓存击穿互斥锁优化|缓存击穿逻辑过期优化
Redis In Action —— Redis Cache Client 工具类封装 —— 封装了针对于缓存穿透、缓存击穿等问题的优化 —— 缓存空值数据|缓存击穿互斥锁优化|缓存击穿逻辑过期优化
2022-08-06 09:35:00 【Alascanfu】
Redis 缓存实战操作解决实际问题
缓存更新策略
缓存更新策略
1️⃣、noeviction:添加数据时,如果redis判断该操作会导致占用内存大小超过内存限制,就返回error,然后啥也不干
2️⃣、allkeys-lru:添加数据时,如果redis判断该操作会导致占用内存大小超过内存限制,就会扫描所有的key,淘汰一些最近未使用的key
3️⃣、volatile-lru:添加数据时,如果redis判断该操作会导致占用内存大小超过内存限制,扫描那些设置里过期时间的key,淘汰一些最近未使用的key
4️⃣、allkeys-random:添加数据时,如果redis判断该操作会导致占用内存大小超过内存限制,就会扫描所有的key,随机淘汰一些key
5️⃣、volatile-random:添加数据时,如果redis判断该操作会导致占用内存大小超过内存限制,扫描那些设置里过期时间的key,随机淘汰一些key
6️⃣、volatile-ttl:添加数据时,如果redis判断该操作会导致占用内存大小超过内存限制,扫描那些设置里过期时间的key,淘汰一些即将过期的key
7️⃣、volatile-lfu:添加数据时,如果redis判断该操作会导致占用内存大小超过内存限制,就会淘汰一些设置了过期时间的,并且最近最少使用的key
8️⃣、allkeys-lfu:添加数据时,如果redis判断该操作会导致占用内存大小超过内存限制,就会扫描所有的key,淘汰一些最近最少使用的key


操作缓存和数据库时有三个问题需要考虑:
1️⃣ 删除缓存还是更新缓存?
- 更新操作:每次更新数据库都会更新缓存,无效写操作较多。
- 删除缓存:更新数据库时会让缓存失效,查询时再更新缓存。
2️⃣ 如何保证缓存与数据库的操作的同时成功或失败?
- 单体系统,将缓存与数据库操作放在一个事务
- 分布式系统,利用 TCC 等分布式事务方案
3️⃣ 先操作缓存还是先操作数据库

总结:
缓存更新策略的最佳实践方案:
1、对于低一致性要求:使用 Redis 自带的内存淘汰机制。
2、对于高一致性要求:使用主动更新缓存策略,并且以超时剔除作为兜底方案:
- 读操作:
- 缓存命中则直接将数据返回
- 缓存未命中则查询数据库,并写入缓存,设定超时时间
- 写操作:
- 先写数据库,然后再删除缓存
- 要保证数据库与缓存操作的原子性
缓存穿透
缓存穿透
缓存穿透代表的是 —— 既要穿透 Redis 又 穿透了 MySQL 即 不存在该值。


具体实现
/** * 功能描述 * 商户查询缓存 —— 使用 hash 来进行数据缓存 * @date 2022/8/1 * @author Alascanfu */
@Override
public Result queryById(Long id) {
// 1. 根据商铺 id 到 Redis 中查询商铺缓存
String shopKey = RedisConstants.CACHE_SHOP_KEY + id ;
Map<Object, Object> shopMap = stringRedisTemplate.opsForHash().entries(shopKey);
// 2. 缓存命中 => 返回商铺信息
if (shopMap.isEmpty()){
// 3. 缓存未命中 => 根据 id 查询数据库
Shop shop = getById(id);
// 3.1 判断商铺缓存是否存在 => 不存在 返回404状态码 提示错误信息
if (shop == null){
stringRedisTemplate.opsForHash().put(shopKey,RedisConstants.CACHE_NULL,RedisConstants.CACHE_NULL_VALUE);
stringRedisTemplate.expire(shopKey,RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
return Result.fail("404 您访问的商户不存在!");
}
// 3.2 判断商铺缓存是否存在 => 存在 将商铺数据写入到 Redis 中 => 返回 商铺信息
Map<String, Object> redisCache = BeanUtil.beanToMap(shop,new HashMap<>()
,CopyOptions.create()
.setIgnoreNullValue(true)
.setFieldValueEditor((k,v)->{
if (k == null){
v = "0";
}else {
v = v + "";
}
return v;
}));
stringRedisTemplate.opsForHash().putAll(shopKey , redisCache);
// todo 更新数据到缓存中 并且设置超时时间
stringRedisTemplate.expire(shopKey,RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
return Result.ok(redisCache);
}
if (stringRedisTemplate.opsForHash().get(shopKey,RedisConstants.CACHE_NULL) != null ){
return Result.fail("404 您访问的商户不存在!");
}
// 4 结束
return Result.ok(shopMap);
}

缓存雪崩
缓存雪崩概念
缓存雪崩是指在同一时段大量的缓存 key 同时失效或者 Redis 服务宕机,从而导致的大量请求到达数据库,带来巨大压力。
解决方案
缓存雪崩的主要解决方案如下:
- 针对于批量数据加载到缓存时,添加过期时间时额外添加一个随机时间TTL
- Redis 集群提高服务高可用
- 给缓存业务 添加降级限流策略
- 给业务添加多级缓存

缓存击穿
缓存击穿的概念:
缓存击穿又被称之为热点 Key 问题,就是一个被高并发访问并且缓存重建业务较复杂的 key 突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
常见的解决方案
- 互斥锁 ——
- 逻辑过期 ——

缓存击穿的 互斥锁 与 逻辑过期优缺点对比
| 解决方案 | 优点 | 缺点 |
|---|---|---|
| 互斥锁 | - 没有额外的内存消耗 - 保证一致性 - 实现简单 | 可能造成死锁风险 线程需要等待,效率受到影响 |
| 逻辑过期 | - 线程无需等待,性能较好 | 不保证一致性 有额外的内存消耗 实现较为复杂 |
案例:基于互斥锁方式解决缓存击穿问题
/** * 功能描述 * 缓存击穿优化 + 缓存穿透 * @date 2022/8/1 * @author Alascanfu */
public Map<?, Object> queryWithMutex(Long id){
// 1. 根据商铺 id 到 Redis 中查询商铺缓存
String shopKey = RedisConstants.CACHE_SHOP_KEY + id ;
Map<Object, Object> shopMap = stringRedisTemplate.opsForHash().entries(shopKey);
// 2. 缓存命中 => 返回商铺信息
if (shopMap.isEmpty()){
// todo 缓存未命中 重建缓存操作 并且添加对应的锁
boolean isLock = lock.tryLock(id);
try {
if (!isLock){
TimeUnit.MILLISECONDS.sleep(50);
return queryWithMutex(id);
}
// 获取到锁之后还需再次检测 redis 缓存是否存在 做 DoubleCheck
shopMap = stringRedisTemplate.opsForHash().entries(shopKey);
if ( !shopMap.isEmpty() && !stringRedisTemplate.opsForHash().get(shopKey,RedisConstants.CACHE_NULL).equals(RedisConstants.CACHE_NULL_VALUE) ){
return shopMap;
}
// 3. 缓存未命中 => 根据 id 查询数据库
Shop shop = getById(id);
// 3.1 判断商铺缓存是否存在 => 不存在 返回404状态码 提示错误信息
if (shop == null){
stringRedisTemplate.opsForHash().put(shopKey,RedisConstants.CACHE_NULL,RedisConstants.CACHE_NULL_VALUE);
stringRedisTemplate.expire(shopKey,RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
// 3.2 判断商铺缓存是否存在 => 存在 将商铺数据写入到 Redis 中 => 返回 商铺信息
Map<String, Object> redisCache = BeanUtil.beanToMap(shop,new HashMap<>()
,CopyOptions.create()
.setIgnoreNullValue(true)
.setFieldValueEditor((k,v)->{
if (k == null){
v = "0";
}else {
v = v + "";
}
return v;
}));
stringRedisTemplate.opsForHash().putAll(shopKey , redisCache);
// todo 更新数据到缓存中 并且设置超时时间
stringRedisTemplate.expire(shopKey,RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
return redisCache;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unLock(RedisConstants.LOCK_SHOP_KEY + id);
}
}
// - 缓存命中空值直接返回 缓存击穿
if (stringRedisTemplate.opsForHash().get(shopKey,RedisConstants.CACHE_NULL) != null){
return null;
}
// 4 结束
return shopMap ;
}
JMETER 压测测试
1000QPS 完美运行~,同时使得缓存瞬间失效也能完美解决缓存击穿与缓存穿透的问题。
案例:基于延迟缓存方式解决缓存击穿问题
public void saveShop2RedisByHash(Long id , Long expireSeconds){
Shop shop = getById(id);
Map<String, Object> redisCache = BeanUtil.beanToMap(shop,new HashMap<>()
,CopyOptions.create()
.setIgnoreNullValue(true)
.setFieldValueEditor((k,v)->{
if (k == null){
v = "0";
}else {
v = v + "";
}
return v;
}));
redisCache.put(RedisConstants.EXPIRE_SECONDS_KEY,String.valueOf(LocalDateTime.now().plusSeconds(expireSeconds)));
stringRedisTemplate.opsForHash().putAll(RedisConstants.CACHE_SHOP_KEY + id ,redisCache);
System.out.println("重建缓存成功...");
}
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
/** * 功能描述 * 缓存击穿优化 —— 逻辑缓存 * @date 2022/8/1 * @author Alascanfu */
public Map<? extends Object, Object> queryWithLogicalExpire(Long id ,Long expireTimeSec){
// 1. 根据商铺 id 到 Redis 中查询商铺缓存
String shopKey = RedisConstants.CACHE_SHOP_KEY + id ;
Map<Object, Object> shopMap = stringRedisTemplate.opsForHash().entries(shopKey);
// 如果缓存不存在 或者命中 值为 null 的缓存直接返回就好了
if (shopMap.isEmpty() ){
return null ;
}
// todo 2 反之获取得到对应的缓存
// 2-1 判断对象的缓存是否过期
LocalDateTime expireTime = LocalDateTime.parse((String) shopMap.get(RedisConstants.EXPIRE_SECONDS_KEY));
if (expireTime.isAfter(LocalDateTime.now())){
// 未过期直接返回商铺的信息数据
shopMap.remove(RedisConstants.EXPIRE_SECONDS_KEY);
return shopMap;
}
String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
// 每个人进来先要进行加锁,key值为"good_lock",value随机生成
String value = UUID.randomUUID().toString().replace("-","");
boolean isLock = tryLock(lockKey,value);
if (isLock){
try {
// 获取到锁之后进行 DoubleCheck
expireTime = LocalDateTime.parse((String) shopMap.get(RedisConstants.EXPIRE_SECONDS_KEY));
if (!expireTime.isAfter(LocalDateTime.now())){
CACHE_REBUILD_EXECUTOR.submit(()->{
saveShop2RedisByHash(id , 20L);
});
}
} catch (Exception e) {
throw new RuntimeException("重建缓存失败...");
}finally {
unLock(lockKey);
}
}
shopMap.remove(RedisConstants.EXPIRE_SECONDS_KEY);
// 4 结束
return shopMap ;
}
缓存工具封装

/*** * @author: Alascanfu * @date : Created in 2022/8/4 14:43 * @description: utils.CacheClient * 解决缓存穿透、缓存击穿的 Redis 工具封装类 —— 针对于 Redis 的字符串类型 以及 Redis 的 Hash 类型 * 缓存穿透的优化 —— 缓存空对象进行返回 * 缓存击穿的优化 —— 互斥锁、以及 逻辑过期 解决方案 * @modified By: Alascanfu **/
@Component
public class CacheClient {
private final StringRedisTemplate stringRedisTemplate;
private final static ExecutorService CACHE_EXECUTOR = Executors.newFixedThreadPool(10);
public CacheClient(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
/** * 功能描述 * 设置对应的 key 与 value 并设置对应的过期时间 * * @date 2022/8/4 * @author Alascanfu */
public void set(String key, Object obj, Long l, TimeUnit timeUnit) {
this.stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(obj), l, timeUnit);
}
/** * 功能描述 * 设置对应的 key 与 value 并设置对应的逻辑过期时间 * * @date 2022/8/4 * @author Alascanfu */
public void setWithLogicalExpire(String key, Object obj, Long l, TimeUnit timeUnit) {
RedisData redisData = new RedisData();
redisData.setData(obj);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(l)));
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
}
/** * 功能描述 * 缓存穿透 —— 缓存空对象 * * @date 2022/8/4 * @author Alascanfu */
public <R, ID> R queryWithPassThrough(String prefixKey, ID id,
Class<R> type, Function<ID, R> dbFallBack, Long l, TimeUnit timeUnit) {
// 前置判断当前 id 是否合法
if (id == null) {
return null;
}
// 1、尝试获取从缓存中获取对应的用户数据
String key = prefixKey + id;
String json = stringRedisTemplate.opsForValue().get(key);
// 2、判断缓存是否命中 ——
// 2.1 命中缓存则直接返回缓存数据 / 这里需要进行判断 命中的缓存是否是空缓存数据
// 如果是空数据就进行对应的 null 返回
if (StrUtil.isNotBlank(json)) {
// 反之返回对应的数据
return JSONUtil.toBean(json, type);
}
// 空值处理
if (json != null) {
return null;
}
// 2.2 未命中缓存则查询数据库数据
R r = dbFallBack.apply(id);
// 2.2.1 数据库中存在对应的数据 则 加载到缓存当中并且返回当前数据
if (r == null) {
stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
return null;
}
// 2.2.2 数据库不存在当前对应的数据 则 缓存空对象到 redis 当中
this.set(key, json, l, timeUnit);
return r;
}
/** * 功能描述 * 尝试获取 Redis 互斥锁 setnx来实现 * * @date 2022/8/4 * @author Alascanfu */
private boolean tryLock(String key) {
Boolean flag = this.stringRedisTemplate.opsForValue().setIfAbsent(key, UUID.randomUUID().toString(), RedisConstants.LOCK_TTL, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
/** * 功能描述 * 释放 Redis 互斥锁 * * @date 2022/8/4 * @author Alascanfu */
public void unLock(String key) {
this.stringRedisTemplate.delete(key);
}
/** * 功能描述 * 缓存击穿 —— 逻辑过期解决 * @date 2022/8/4 * @author Alascanfu */
public <R, ID> R queryWithLogicalExpire(String prefixKey, ID id, Class<R> type ,Function<ID,R> dbFallBack , Long l ,TimeUnit timeUnit) {
// todo 1、通过 prefixKey 与 id 查询缓存中的数据
String key = prefixKey + id;
String json = stringRedisTemplate.opsForValue().get(key);
// todo 1.1 如果缓存数据不存在,直接返回空就好了
if (StrUtil.isBlank(json)) {
return null;
}
// todo 1.2 如果缓存数据存在
// todo 1.2.1 首先尝试获取缓存数据中的 逻辑过期时间 对象数据信息
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
LocalDateTime expireTime = redisData.getExpireTime();
// todo 如果 逻辑过期时间还没有到 则直接将查询到的缓存数据进行返回
if (expireTime.isAfter(LocalDateTime.now())) {
return r;
}
// todo 如果 逻辑过期时间已经到达 此时尝试去获取互斥锁
String lockKey = RedisConstants.LOCK_SHOP_KEY + id ;
boolean isLock = tryLock(lockKey);
// todo 互斥锁 获得之后 还需要进行双重检测 做 Double Check 检测获取到锁的 逻辑时间是否已经被修改过了
if (isLock) {
try {
if (expireTime.isBefore(LocalDateTime.now())) {
// todo 反之 直接 开启另一条线程修改逻辑过期时间 追加时间以免过期而造成的缓存击穿问题 当前线程直接返回旧数据
CACHE_EXECUTOR.submit(() -> {
rebuildCache(RedisConstants.CACHE_SHOP_KEY,dbFallBack,id,l,timeUnit);
});
} else {
// todo 如果 double check 此时检测逻辑时间已经被修改 那么直接释放锁返回数据
return r;
}
} catch (Exception e) {
throw new RuntimeException("出现异常错误...");
} finally {
unLock(lockKey);
}
}
return r ;
}
/** * 功能描述 * 用于缓存击穿重建 热点 key 缓存作用 * @date 2022/8/4 * @author Alascanfu */
public <ID,R> void rebuildCache(String prefixKey,Function<ID,R> dbFallBack ,ID id , Long l , TimeUnit timeUnit) {
R r = dbFallBack.apply(id);
String shopKey = prefixKey + id ;
this.setWithLogicalExpire(shopKey,r,l,timeUnit);
}
}
边栏推荐
- C语言结构体
- C. Robot in a Hallway (recursion/prefix sum/dynamic programming)
- Linux - several ways to install MySQL
- 【OpenCV】 人脸识别
- Expansion mechanism of ArrayList
- 实验9(交换综合实验)
- Fusion communication FAQ | 7 issue of the cloud small classroom
- The 22nd day of the special assault version of the sword offer
- 卡尔曼滤波器(目标跟踪一)(上)
- Remember to deduplicate es6 Set to implement common menus
猜你喜欢

Token design scheme under microservice

18 days (link aggregation of configuration, the working process of the VRRP, IPV6 configuration)

Common loss functions

pytorch中的两个重要的自学函数 dir() ; help()

ELT.zip 】 【 OpenHarmony chew club - the methodology of academic research paper precipitation series

GEE(9): Area area statistics (using connectedPixelCount and ee.Image.pixelArea())

Neo4j:通过 Docker 和 Cypher 查询语言 运行图形数据库
![[mysql chapter - advanced chapter] index](/img/b1/7231fa397e8b147235a20e7f97cd31.png)
[mysql chapter - advanced chapter] index

分布式链路追踪opentracing-go jaeger小示例
![Web version Xshell supports FTP connection and SFTP connection [Detailed tutorial] Continue from the previous article](/img/30/c9d087554bb028582c494098664275.png)
Web version Xshell supports FTP connection and SFTP connection [Detailed tutorial] Continue from the previous article
随机推荐
Fusion communication FAQ | 7 issue of the cloud small classroom
域名授权验证系统v1.0.6开源版本网站源码
创建一个 Dapp,为什么要选择波卡?
Neo4j:通过 Docker 和 Cypher 查询语言 运行图形数据库
[图]Edge 104稳定版发布:引入“增强安全模式”
【ELT.ZIP】OpenHarmony啃论文俱乐部——学术科研方法论沉淀辑
jupyter notebook & pycharm (anaconda)
Folyd
The 22nd day of the special assault version of the sword offer
PHP online examination system 4.0 version source code computer + mobile terminal
Dijkstr堆优化
Summary of the experience of project operation and maintenance work
/var/log/messages is empty
接口自动化落地实践
Let's talk about the pits of mysql's unique index, why does it still generate duplicate data?
WIFI营销小程序源码
融合通信常见问题7月刊 | 云信小课堂
卡尔曼滤波器(目标跟踪一)(上)
JDBC数据库连接
pytorch中的两个重要的自学函数 dir() ; help()