1.缓存更新策略
主动更新用的最多。
主动更新一般是由缓存的调用者,在更新数据库的同时,更新缓存。
操作缓存和数据库时有三个问题需要考虑:
- 删除缓存还是更新缓存?
更新缓存:每次更新数据库都更新缓存,无效写操作较多
删除缓存:更新数据库时让缓存失效,查询时再更新缓存 - 如何保证缓存与数据库的操作的同时成功或失败?
单体系统,将缓存与数据库操作放在一个事务
分布式系统,利用TCC等分布式事务方案 - 先操作缓存还是先操作数据库?
先操作数据库,再删除缓存
先删除缓存,再操作数据库
2.缓存穿透
缓存穿透产生的原因是什么?
用户请求的数据在缓存中和数据库中都不存在,不断发起这样的请求,给数据库带来巨大压力
缓存穿透的解决方案有哪些?
- 缓存null值
- 布隆过滤
- 增强id的复杂度,避免被猜测id规律
- 做好数据的基础格式校验
- 加强用户权限校验
- 做好热点参数的限流
如下图所示,这里基于缓存空对象实现:
- 缓存穿透代码
// 封装了缓存穿透的处理/*参数里面需要传递查询数据库的函数*/public <R, ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Long time, TimeUnit unit,Function<ID, R> dbFallback) {// 1.从Redis查询商铺缓存String key = keyPrefix + id;String json = stringRedisTemplate.opsForValue().get(key);// 2.判断是否存在if (StrUtil.isNotBlank(json)) {// 3.存在则返回缓存数据return JSONUtil.toBean(json, type);}// 命中是否为空的缓存if(json != null){return null;}// 4.不存在则查询数据库R r = dbFallback.apply(id);// 5. 数据库不存在if (r == null) {// 将空值保存在redis中,应对缓存穿透stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);return null;}// 6. 数据库存在,写入缓存this.set(key, r, time, unit);return r;}
- 数据写入缓存代码
public void set(String key, Object value, Long time, TimeUnit unit) {stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);}
3. 缓存雪崩
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
解决方案:
- 给不同的Key的TTL添加随机值
- 利用Redis集群提高服务的可用性
- 给缓存业务添加降级限流策略
- 给业务添加多级缓存
4.缓存击穿
缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
常见的解决方案有两种:
- 互斥锁
- 逻辑过期
互斥锁
// 互斥锁解决缓存击穿public <R,ID> R queryWithMutex(String keyPrefix, ID id, Class<R> type, Long time, TimeUnit unit, Function<ID, R> dbFallback) {// 1.从Redis查询商铺缓存String key = keyPrefix + id;String json = stringRedisTemplate.opsForValue().get(key);// 2.判断是否存在if (StrUtil.isNotBlank(json)) {// 3.存在则返回缓存数据return JSONUtil.toBean(json, type);}// 命中是否为空的缓存if(json != null){return null;}// 4. 实现缓存重建// 4.1 尝试获取分布式锁String lockKey = "lock:shop:" + id;R r = null;try {boolean isLock = tryLock(lockKey);// 4.2 判断是否获取成功if (!isLock) {// 4.3 失败,则失眠并重试Thread.sleep(50);return queryWithMutex(keyPrefix, id, type, time, unit, dbFallback);}// 4.4 成功则查询数据库r = dbFallback.apply(id);// 5. 数据库不存在if (r == null) {// 将空值保存在redis中,应对缓存穿透this.set(key, "", time, unit);return null;}// 6. 数据库存在,写入缓存this.set(key, r, time, unit);} catch (InterruptedException e) {throw new RuntimeException(e);} finally {// 7. 释放锁releaseLock(lockKey);}return r;}
- 锁相关代码
// 线程池private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);// 尝试获取分布式锁private boolean tryLock(String key) {Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);// 这样更安全return BooleanUtil.isTrue(lock);}// 释放分布式锁private void releaseLock(String key) {stringRedisTemplate.delete(key);}
逻辑过期
- 逻辑过期存入数据
public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {RedisData redisData = new RedisData();redisData.setData(value);redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));}
- 逻辑过期锁查询数据
public <R,ID> R queryWithLogicalExpire(String keyPrefix ,ID id, Class<R> type, Long time, TimeUnit unit, Function<ID, R> dbFallback) {// 1.从Redis查询商铺缓存String key = keyPrefix + id;String json = stringRedisTemplate.opsForValue().get(key);// 2.判断是否存在if (StrUtil.isBlank(json)) {// 3.不存在return null;}// 4. 命中,json反序列化对象RedisData redisData = JSONUtil.toBean(json, RedisData.class);JSONObject data = (JSONObject) redisData.getData();R r = JSONUtil.toBean(data, type);LocalDateTime expireTime = redisData.getExpireTime();// 5. 判断是否过期if (expireTime.isAfter(LocalDateTime.now())) {// 5.1 未过期直接返回return r;}// 5.2 过期则异步更新缓存// 6. 缓存重建// 6.1 尝试获取分布式锁String lockKey = LOCK_SHOP_KEY + id;// 6.2 判断是否获取成功if (tryLock(lockKey)) {// 6.3 成功,开启独立线程实现缓存重建CACHE_REBUILD_EXECUTOR.submit(()->{// 重建缓存try {// 查数据库R r1 = dbFallback.apply(id);// 保存在redis中this.setWithLogicalExpire(key, r1, time, unit);} catch (Exception e) {throw new RuntimeException(e);} finally {releaseLock(lockKey);}});}// 6.4 返回过期的信息return r;}