赞
踩
缓存是数据交换的缓冲区,是存贮数据的临时地方,一般读写性能较高。
缓存的作用:
缓存的成本:
业务查询:
Cache Aside Pattern基本思想:
特点:
需要思考的问题!
相比较而言方案二安全性更高一些:
原因:方案二需要满足,线程1查询时缓存恰好失效,且更新数据库的操作间隔要比写入缓存的时间短。(但还是有可能),需要赋予超时剔除作为兜底方案。
缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这些请求都会打到数据库,给数据库带来巨大的压力。
解决方案
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
解决方案:
缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务比较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
存在的问题:需要等待阻塞
利用setnx来模拟简单的分布式锁。
# 获得锁(一般上会设置有效期)
setnx lock 1
# 删除锁
del lock
基于逻辑过期的方式会存在一段时间内的不一致性,一旦线程完成了缓存重建,就能够得到一致性的结果。
基于StringRedisTemplate封装一个缓存工具类,满足下列需求:
@Slf4j @Component public class CacheClient { private final StringRedisTemplate stringRedisTemplate; private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10); public CacheClient(StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate = stringRedisTemplate; } public void set(String key, Object value, Long time, TimeUnit unit){ stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,unit); } 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))); // 写入Redis stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData)); } public <R,ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback, Long time, TimeUnit unit){ String key = keyPrefix + id; // 1. 从Redis中查询缓存 String json = stringRedisTemplate.opsForValue().get(key); // 2. 判断是否存在 if (StrUtil.isNotBlank(json)){ // 3. 存在,直接返回 return JSONUtil.toBean(json,type); } // 判断命中的是否是空值 if (json!=null){ return null; } // 4. 不存在,根据id查询数据库 R r = dbFallback.apply(id); // 5. 数据库不存在,返回错误 if (r==null){ stringRedisTemplate.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES); return null; } // 6. 存在,写入Redis this.set(key,r,time,unit); // 7. 返回 return r; } public <R,ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback, Long time, TimeUnit unit){ String key = keyPrefix + id; // 1. 从Redis中查询缓存 String json = stringRedisTemplate.opsForValue().get(key); // 2. 判断是否存在 if (StrUtil.isBlank(json)){ // 3. 不存在直接返回 return null; } // 4.命中,需要先把json反序列化对象 RedisData redisData = JSONUtil.toBean(json, RedisData.class); R r = JSONUtil.toBean((JSONObject) redisData.getData(), type); LocalDateTime expireTime = redisData.getExpireTime(); // 5. 判断是否过期 if(expireTime.isAfter(LocalDateTime.now())){ // 5.1 未过期,直接返回店铺信息 return r; } // 5.2 已过期,需要缓存重建 // 6 缓存重建 // 6.1 获取互斥锁 String lockKey = RedisConstants.LOCK_SHOP_KEY + id; boolean isLock = tryLock(lockKey); // 6.2 判断是否获取锁成功 if(isLock){ // 6.3 成功,开启独立线程,实现缓存重建 CACHE_REBUILD_EXECUTOR.submit(()->{ // TODO 重建缓存,需要修改过期时间为1800秒 try { // 查询数据库 R r1 = dbFallback.apply(id); // 写入redis this.setWithLogicalExpire(key,r1,time,unit); }catch (Exception e){ throw new RuntimeException(e); }finally { unlock(lockKey); }}); } // 6.4 先返回过期的商铺信息 return r; } private boolean tryLock(String key){ Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS); return BooleanUtil.isTrue(flag); } private void unlock(String key){ stringRedisTemplate.delete(key); } }
递归调用 queryWithMutex(id) 可能会导致栈溢出,因为没有任何条件来终止递归。在这种情况下,如果无法获取锁,线程会无限制地尝试递归调用自身,并且每次递归都会消耗一些栈空间,最终导致栈溢出异常。
public Shop queryWithMutex(Long id){ String key = RedisConstants.CACHE_SHOP_KEY + id; // 1. 从Redis中查询缓存 String shopJson = stringRedisTemplate.opsForValue().get(key); // 2. 判断是否存在 if (StrUtil.isNotBlank(shopJson)){ // 3. 存在,直接返回 return JSONUtil.toBean(shopJson,Shop.class); } // 判断命中的是否是空值 if (shopJson!=null){ return null; } // 4. 实现缓存重建 // 4.1 获取互斥锁 String lockKey = RedisConstants.LOCK_SHOP_KEY + id; Shop shop = null; try { boolean isLock = tryLock(lockKey); // 4.2 判断是否获取成功 if (!isLock){ // 4.3 失败,则休眠并重试 Thread.sleep(50); // TODO 感觉这里代码有问题。建议重新修改 return queryWithMutex(id); } // 4.4 成功,根据id查询数据库 shop = getById(id); // TODO 模拟重建的延时(正常运行时需要删除) Thread.sleep(200); // 5. 数据库不存在,返回错误 if (shop==null){ stringRedisTemplate.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES); return null; } // 6. 存在,写入Redis stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES); }catch (Exception e){ throw new RuntimeException(e); }finally { // 7.释放获取锁 unlock(lockKey); } // 8. 返回 return shop; }
修改后的代码如下:采用了循环替代了递归,并设置了最大循环次数。达到最大循环后没有成功即返回null.在并发为200/s的时候平均每个请求需要在循环中执行的次数为7次。
public Shop queryWithMutex2(Long id){ String key = RedisConstants.CACHE_SHOP_KEY + id; // 从Redis中查询缓存 String shopJson = stringRedisTemplate.opsForValue().get(key); if (StrUtil.isNotBlank(shopJson)){ return JSONUtil.toBean(shopJson,Shop.class); } // 判断命中的是否是空值,即是否等于空字符串 if (shopJson!=null){ return null; } // 尝试准备从数据库中获取数据 int MAX_RETRY_COUNT = 10; boolean isLock = false; int retryCount = 0; String lockKey = RedisConstants.LOCK_SHOP_KEY + id; Shop shop = null; try{ // 4.2 循环重试直至获取锁成功或达到最大重试次数 while (!isLock && retryCount < MAX_RETRY_COUNT) { isLock = tryLock(lockKey); if (!isLock) { // 4.3 失败,则休眠并重试 Thread.sleep(50); retryCount++; } //休眠结束后尝试从缓存中查询数据 shopJson = stringRedisTemplate.opsForValue().get(key); if (StrUtil.isNotBlank(shopJson)){ System.out.println("Thread尝试的次数为:"+retryCount); return JSONUtil.toBean(shopJson,Shop.class); } // 否者继续循环获得锁 } // 判断是否获取锁成功,或者超过最大重试次数 if (!isLock || retryCount == MAX_RETRY_COUNT){ return null; } // 获取锁成功,根据id查询数据库 shop = getById(id); // TODO 模拟重建的延时(正常运行时需要删除) Thread.sleep(200); // 5. 数据库不存在,返回错误 if (shop==null){ stringRedisTemplate.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES); return null; } // 6. 存在,写入Redis stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES); }catch (Exception e){ throw new RuntimeException(e); }finally { // 7.释放获取锁 unlock(lockKey); } // 8. 返回 return shop; }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。