缓存穿透
场景
请求的数据在缓存
和数据库
都不存在,
永远打到数据库

解决方案
1、缓存空对象
请求的数据,redis没有,数据库也没有,直接返回缓存null
(如果后面的数据库中增加这个店铺的信息了,不必担心一直会返回缓存中的空对象,
因为这里Redis会给店铺设置过期时间,当店铺缓存过期,那么下面第一个判断就会失效,
直接去查询数据库,重写添加对应店铺的缓存)
流程如下:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| public class RedisConstants{ public static final String LOGIN_CODE_KEY = "login:code:": public static final Long LOGIN_CODE_TTL = 2L; public static final String LOGIN_USER_KEY = "login:token:"; public static final Long LOGIN_USER_TTL = 30L; public static final Long CACHE_NULL_TTL = 2L; public static final Long CACHE_SHOP_TTL = 30L; public static final String CACHE_SHOP_KEY = "cache:shop;" }
public Result queryById(Long id) { String key = CACHE_SHOP_KEY + id; String shopJson = stringRedisTemplate.opsForValue().get(key); if (StrUtil.isNotBlank(shopJson)) { Shop shop = JSONUtil.toBean(shopJson,Shop.class); return Result.ok(shop); }
if (shopJson != null) { return Result.fail("店铺不存在!"); }
Shop shop = getById(id); if (shop == null) { stringRedisTemplate.opsForValue().set(key, value:"", CACHE_NULL_TTL,TimeUnit.MINUTES); return Result.fail("店铺不存在!"); } stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES): return Result.ok(shop); }
|
缓存击穿
场景
缓存击穿问题也叫热点Kev问题,就是一个被高并发访问并且缓存重建业务较复杂(这个key的value需要多表联查后,返回的结果集,耗时比较久,如果有多个线程在这个缓存创建前请求,就会打到数据库)的kev突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。

解决方案
1、互斥锁
采用 redis的setnx ,设置keysetnx lock 1 成功返回1, get lock 返回value
1 ,如果key 存在, 重新设置value就会不成功,如果 setnx lock 2 返回0
表示设置value失败(类似于锁) 释放锁 del lock , 释放锁后则可以重新设置
setnx lock 2
时序图

流程图

实现代码
常量
1 2 3 4 5 6 7 8 9 10
| public class RedisConstants{ public static final String LOGIN_CODE_KEY = "login:code:": public static final Long LOGIN_CODE_TTL = 2L; public static final String LOGIN_USER_KEY = "login:token:"; public static final Long LOGIN_USER_TTL = 30L; public static final Long CACHE_NULL_TTL = 2L; public static final Long CACHE_SHOP_TTL = 30L; public static final String CACHE_SHOP_KEY = "cache:shop;" }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92
| public Result queryById(Long id){ Shop shop = queryWithMutex(id)); if(shop == null){ return Result.fail("店铺不存在!"); } return Result.ok(shop); }
public Shop queryWithPassThrough(Long id) { String key = CACHE_SHOP_KEY + id; String shopJson = stringRedisTemplate.opsForValue().get(key); if (StrUtil.isNotBlank(shopJson)) { Shop shop = JSONUtil.toBean(shopJson,Shop.class); return shop; }
Shop shop = getById(id); if (shop == null) { stringRedisTemplate.opsForValue().set(key, value: "" ,CACHE_NULL_TTL,TimeUnit.MINUTES); return null; }
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES): return shop; }
public Shop queryWithMutex(Long id) { String key = CACHE_SHOP_KEY + id; String shopJson = stringRedisTemplate.opsForValue().get(key); try{ if (StrUtil.isNotBlank(shopJson)) { Shop shop = JSONUtil.toBean(shopJson,Shop.class); return shop; } String lockKey = "lock:shop:"+ id; if(!tryLock(lockKey)){ Thread.sleep(50); return queryWithMutex(id); } Shop shop = getById(id); Thread.sleep(200); if (shop == null) { return null; } stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES): }catch(Exception e){ throw new RuntimeException(e); }finally{ unlock(lockKey); } return shop; }
private boolean tryLock(String key) [ Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, value: "1", timeout: 10, TimeUnit.SECONDS): return BooleanUtil.isTrue(flag); }
private void unlock(String key) { stringRedisTemplate.delete(key); }
|
2、逻辑过期删除
逻辑过期的方式,可以避免互斥锁需要线程争夺资源 进入等待状态 ,
解决互斥锁耗时问题 问题是会返回一些过期数据
在redis里面的value 里面设置过期字段,以及过期时间
KEY |
VALUE |
sys:user:1 |
{name:"Jack",age:21,expire:152141223} |
时序图如下:

具体实现

缓存雪崩
场景
缓存雪崩是指在同一时段大量的缓存key同时失效
或者Redis服务宕机
,导致大量请求到达数据库,带来巨大压力

解决方案
1、不同的key TTL追加随机值
如做缓存预热时,将数据库的数据添加到redis中时,应该将TTL后面多+一段随机时间,避免大量缓存同时失效

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component;
import java.util.List; import java.util.Random; import java.util.concurrent.TimeUnit;
@Component public class CachePreheat {
@Autowired private StringRedisTemplate stringRedisTemplate;
private static final long CACHE_TTL_MINUTES = 30; private static final long RANDOM_TTL_MINUTES = 10;
public void preloadCache() { List<Long> ids = getAllIdsFromDatabase();
for (Long id : ids) { String key = "cache_key:" + id; String value = getValueFromDatabase(id);
long ttl = CACHE_TTL_MINUTES + new Random().nextInt((int) RANDOM_TTL_MINUTES);
stringRedisTemplate.opsForValue().set(key, value, ttl, TimeUnit.MINUTES); } }
private List<Long> getAllIdsFromDatabase() { return null; }
private String getValueFromDatabase(Long id) { return null; } }
|
2、搭建集群
针对单一redis节点宕机问题 需要搭建Redis集群 提高系统的高可用性
