文章目录
缓存
- 缓存是数据交换的缓冲区(cache),是数据存储临时的地方,一般读写性能较高
- 好处
- 降低后端负载
- 提高读写效率,降低响应时间
- 成本
- 一致性成本
- 代码维护(为了一致性)
- 运维成本(缓存一般是集群的)
redis缓存
- redis缓存执行流程
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public Result queryById(Long id) {
String key = "cache:shop:"+id;
//1、从Redis查询id
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2、判断redis是否存在数据
if(StrUtil.isNotBlank(shopJson)) {
//3、存在
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
//4、不存在
Shop shop = getById(id);
//5、存在返回错误
if(shop == null){
return Result.fail("店铺不存在");
}
//6、存在写入reids
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop));
//7、返回
return Result.ok(shop);
}
}
缓存更新策略
- 缓存更新策略是为了解决缓存不足的问题
- 内存淘汰:redis的内存淘汰机制,当内存不足自动淘汰部分数据,下次查询时更新缓存
- 一致性差、无维护成本
- 超时删除:给缓存数据添加过期时间,到期删除缓存数据,下次查询更新缓存
- 一致性一般,维护成本低
- 主动更新:编写业务逻辑,在修改数据库的同时,更新缓存
- 一致性好
- 内存淘汰:redis的内存淘汰机制,当内存不足自动淘汰部分数据,下次查询时更新缓存
- 场景
- 低一致性需求:使用内存淘汰机制,基本不会改变的数据
- 高一致性需求:主动更新,经常改变的数据
主动更新策略
-
缓存调用者,在更新数据库的同时更新缓存(推荐)
-
删除缓存与更新缓存
- 删除缓存:每次更新数据库时让缓存失效,查询时在更新缓存(推荐)
- 更新缓存:每次更新数据库都更新缓存,无效写操作多
-
保证缓存与数据库操作同时成功或者失败
- 单体系统,将缓存与数据库操作放在一个事务
- 分布式系统:利用TCC等分布式事务方案
-
先删除缓存,在更新数据库、先更新据库,在删除缓存
- 都会造成一致性问题,后者概率低,需要加锁也就是上面说的事务
-
-
缓存与数据库合为一个服务,有服务来维护一致性,调用者调用该服务,无需关心缓存一致性问题(实现起来复杂)
-
调用者只操作缓存,由其他线程异步的将缓存数据持久化到数据库(实现起来复杂)
缓存穿透
缓存穿透:客户端请求的数据在缓存中和数据库都不存在,这样缓存永远不会生效,这些请求都会到达数据库
- 缓存空对象
- 布隆过滤
- 增强id复杂度
- 做好数据格式校验
- 加强用户格式校验
缓存穿透现象
- 客户端访问了一个数据库不存在的数据
缓存空对象处理缓存穿透
- 当数据没查到数据时,我们将null数据设置到缓存中,并设置过期时间
- 好处
- 实现简单,维护方便
- 缺点
- 额外内存消耗
- 可能造成短期不一致(突然插入该数据),解决就是将插入的数据存入缓存
布隆过过滤器处理缓存穿透
- 布隆过滤器通过算法,将数据库的数据二进制位(占用较少空间)存在过滤器中
- 存在:数据库一定存在该数据
- 不存在:数据库可能存在、也可能不存在该数据
- 好处
- 内存占用少
- 缺点
- 实现复杂
缓存雪崩
缓存雪崩是指同一时段大量的缓存key同时失效或者redis服务宕机,导致大量请求到达数据库,带来巨大压力
- 给不同的key的过期时间添加随机值
- 利用redis集群提高服务的可用性
- 给缓存业务添加降级限流策略
- 给业务添加多级缓存
缓存击穿
缓存击穿问题也叫热点key问题,就是一个被高并发访问并且缓存重建业务的key突然消失了,无数请求访问会在瞬间给数据库带来巨大的压力
-
缓存击穿之后,同一时间节点,可能有多个线程都在重建缓存,解决方案
-
互斥锁:发现有线程在重建缓存,新来线程休眠一会在获取锁
-
优点
- 没有额外内存消耗
- 保证一致性
- 实现简单
-
缺点
- 线程需要等待,性能受影响
- 可能有死锁的风险
-
-
逻辑过期:不给key设置过期时间,在value设置一个过期时间(逻辑时间),如果发现时间已经过期,当前线程会返回旧的数据,然后开启一个新的线程去重建缓存,在缓存没有重建后之前,别的线程访问也是旧的缓存数据
- 优点
- 线程无需等待,性能较好
- 缺点
- 不能保证一致性
- 有额外内存消耗
- 实现负复杂
- 优点
-
缓存穿透实现过程
- 1、从Redis查询id
- 2、判断redis是否存在数据
- 3、存在,返回数据给客户端
- 4、不存在,查询数据库
- 5、存在返回错误
- 6、存在写入reids
private Shop queryWithPassThrough(Long id) {
String key = "cache:shop:"+id;
//1、从Redis查询id
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2、判断redis是否存在数据
if(StrUtil.isNotBlank(shopJson)) {
//3、存在
return JSONUtil.toBean(shopJson, Shop.class);
}
//4、不存在
Shop shop = getById(id);
//5、存在返回错误
if(shop == null){
return null;
}
//6、存在写入reids
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop));
//7、返回
return shop;
}
互斥锁实现缓存击穿过程
- 1、从Redis查询id
- 2、判断redis是否存在数据
- 3、存在,返回数据给客户端
- 4、不存在,实现缓存重建
- 4、1 获取互斥锁
- 4、2 判断是否获取成功
- 4、3 失败,休眠并重试
- 5、存在返回错误
- 6、存在写入reids
- 7、释放互斥锁
- 8、返回数据给客户端
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public Result queryById(Long id) {
//缓存穿透
//Shop shop = queryWithPassThrough(id);
//互斥锁解决缓存击穿
Shop shop = queryWithMutex(id);
if(shop == null){
return Result.fail("店铺不存在");
}
return Result.ok(shop);
}
public Shop queryWithMutex(Long id){
String key = "cache:shop:"+ id;
//1、从Redis查询id
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2、判断redis是否存在数据
if(StrUtil.isNotBlank(shopJson)) {
//3、存在
return JSONUtil.toBean(shopJson, Shop.class);
}
//4、不存在,实现缓存重建
//4、1 获取互斥锁
String lockKey = "lock:shop:"+id;
Shop shop = null;
try {
boolean isLock = tryLock(lockKey);
//4、2 判断是否获取成功
if (!isLock) {
//4、3 失败,休眠并重试
Thread.sleep(50);
queryWithPassThrough(id);
}
//4、4 成功,根据id查询数据库
shop = getById(id);
//模拟创建延时
Thread.sleep(200);
//5、存在返回错误
if (shop == null) {
return null;
}
//6、存在写入reids
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop));
}catch (InterruptedException e){
throw new RuntimeException(e);
}finally {
//7、释放互斥锁
unLock(lockKey);
}
//8、返回
return shop;
}
private Shop queryWithPassThrough(Long id) {
String key = "cache:shop:"+id;
//1、从Redis查询id
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2、判断redis是否存在数据
if(StrUtil.isNotBlank(shopJson)) {
//3、存在
return JSONUtil.toBean(shopJson, Shop.class);
}
//4、不存在
Shop shop = getById(id);
//5、存在返回错误
if(shop == null){
return null;
}
//6、存在写入reids
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop));
//7、返回
return shop;
}
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);
}
}
逻辑过期时间实现缓存击穿过程
- 需要我们在项目启动前手动设置热点key
- 1、从Redis查询id
- 2、判断redis是否存在数据
- 3、未命中,返回null
- 4、命中,将json反序列化未对象
- 5、判断是否过期
- 5.1、未过期,返回数据
- 5.2、已过期,需要缓存重建
- 6、缓存重建
- 6、1 获取互斥锁
- 6、2 判断是否获取锁成功
- 6、3 成功,开启独立线程,实现缓存重建
- 6、4 失败,返回过期商品信息
//逻辑过期时间工具类
@Data
public class RedisData {
private LocalDateTime expireTime;
private Object data;
}
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
private void saveShop2Redis(Long id, long expireSeconds) throws InterruptedException {
//1、查询店铺数据
Shop shop = getById(id);
//模拟延迟
Thread.sleep(200);
//2、封装过期时间逻辑
RedisData redisData = new RedisData();
redisData.setData(shop);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
//3、写入redis
stringRedisTemplate.opsForValue().set("cache:shop:"+id, JSONUtil.toJsonStr(redisData));
}
private Shop queryWithLogicalExpire(Long id) {
String key = "cache:shop:"+id;
//1、从Redis查询id
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2、判断redis是否存在数据
if(StrUtil.isBlank(shopJson)) {
//3、未命中
return null;
}
//4、命中,将json反序列化未对象
RedisData redisData = JSONUtil.toBean(shopJson,RedisData.class);
JSONObject data = (JSONObject) redisData.getData();
Shop shop = JSONUtil.toBean(data, Shop.class);
LocalDateTime expireTime = redisData.getExpireTime();
//5、判断是否过期
if(expireTime.isAfter(LocalDateTime.now())){
//5.1、未过期,返回店铺信息
return shop;
}
//5.2、已过期,需要缓存重建
//6、缓存重建
//6、1 获取互斥锁
String lockKey = "lock:shop:"+id;
boolean isLock = tryLock(lockKey);
//6、2 判断是否获取锁成功
if(isLock){
//6、3 成功,开启独立线程,实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(()->{
try {
//重建缓存
saveShop2Redis(id,20L);
}catch (Exception e){
throw new RuntimeException(e);
}finally {
//释放锁
unLock(lockKey);
}
});
}
//6、4 失败,返回过期商品信息
return shop;
}