Redis缓存数据 | 黑马点评
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |
目录
一、什么是缓存
缓存就是数据交换的缓冲区是临时存储数据的地方一般读写性能较高。
缓存的作用降低后端负载提高读写效率降低响应时间
缓存的成本数据一致性成本、代码维护成本、运维成本
二、添加Redis缓存操作
三、缓存更新策略
缓存的更新策略
业务场景
- 低一致性使用内存淘汰机制例如店铺类型查询的缓存
- 高一致性主动更新并以超时剔除作为兜底方案例如店铺详情查询的缓存
主动更新策略
操作缓存和数据库时要考虑3个问题
1、删除缓存还是更新缓存
更新缓存每次更新数据库都更新缓存无效写操作很多但是一直没查就很亏
删除操作更新数据库的时删除缓存下次查询才有推荐
2、如何保证缓存与数据库操作同时成功或失败
单体系统将缓存和数据库操作放到一个事务里面
分布式系统利用tcc等分布式事务方案
3、先操作缓存还是先操作数据库重点面试题
先删除缓存a在删除完缓存后正在改数据库这时候来了线程查询数据库然后更新到缓存这时候如果a更新完数据库缓存和数据库就是不一致了
先改数据库如果缓存过期失效了a在去数据库查的过程中b改了数据库然后更新缓存这时候a查到数据再更新缓存。缓存和数据库就不一致了。但是这种情况发送的概率远远小于前者。
因为后面的情况发生概率小要满足很多条件所以推荐用后者。推荐先改数据库再改缓存。
案例
我们要加上事务注解保证他们同时成功或同时失败删之前也要判断一下看id是不是空
四、缓存穿透
1、是什么
缓存穿透是指客户端请求的数据在缓存和数据库都没有这样缓存永远不会生效这些请求都会打到数据库。
2、解决方案
1缓存空对象
第一次他随便乱打个没有的id来查询缓存和数据库我们就会返回null然后把null也存到缓存中。
这样再他下一次带这个id来请求的时候就会从缓存中拿到null。
优点简单实现维护方便
缺点
额外的内存消耗缓存了很多没用的数据可以通过设置时间来缓解
可能造成短期的不一致。因为是随便乱打的id发的请求我们帮这个id存缓存了以后要是真的有这个id了到时候拿这个id来查也是null也是控制缓存的时间来缓解也可以当我们新增数据的时候主动插入缓存替换掉null
2布隆过滤器
在请求到缓存前加层布隆过滤器如果这个数据存不存在直接拒绝不给继续请求。
布隆过滤器是怎么知道在不在呢
是通过hash算法算出哈希值将这些哈希值换成2进制位保存到布隆过滤器判断数据是否存在的时候就判断对应的位置是0还是1。这种统计不是百分百准确
不存在的时候百分百不存在说存在不一定存在可以起到一定过滤作用。还是有一定击穿风险
优点内存占用少没有存多余的key
缺点实现复杂存在误判的可能。
3其他策略
- 做好数据的基础格式校验
- 增强id复杂度避免被猜测id的规律
- 加强用户权限校验
- 做好热点参数的限流
3、实践
以缓存null值为例在查询的接口做更改
当我们查到数据不存在的时候将这个id作为keyvalue为空值存储到redis中。
在判断完商品是否存在后加上判断查出来的是否为空值为空就返回不存在
五、缓存雪崩
1、是什么
缓存雪崩是指同一时段大量的缓存key同时失效或redis服务宕机导致大量请求打崩数据库
2、解决方案
给不同的key的TTL(过期时间)添加随机值可以设置成30分钟到40分钟之间的随机数
利用Redis集群提高服务器的可用性哨兵机制
给缓存业务添加降级限流策略比如当服务器出现问题的时候拒绝服务牺牲部分服务来保证安全
给业务添加多级缓存nginx、redis、jvm都可以添加缓存最后才到数据库
六、缓存击穿
1、是什么
缓存击穿问题也叫热点Key问题就是一个被高并发访问并且缓存重建业务较复杂的Key突然失效了无数请求访问会瞬间给数据库带来巨大冲击。
高并发访问可以理解为做活动的商品同一时间有无数的请求来访问这个商品。
缓存重建比较复杂就是缓存可能会过期失效失效的时候重新添加到缓存的时间很久可能业务非常复杂要多表查询运算才能得到的结果
2、解决方案
互斥锁
如果查到缓存中没有就加锁查数据库冲击写入缓存之后再释放锁。这个期间内所有访问的请求都没有拿到锁只能等待重试直到缓存更新完毕读取缓存。
优点没有额外的内存消耗、保证缓存和数据库一致性实现简单
缺点线程需要等待性能受影响用户体验不好。可能有死锁的风险
逻辑过期
设置缓存过期时间的时候不是真正的设置而是设置在value里面如果查询缓存发现逻辑删除时间过期了就new一个新的线程加锁来查询数据库更新缓存更新完毕后才释放锁自己返回旧数据。这个期间内其他线程都没有拿到锁都返回旧数据直到更新完毕
优点线程不用等待性能好。
缺点不保证一致性有额外内存消耗实现复杂。
3、实践-互斥锁
用什么锁
我们平时用synchronized和lock锁的时候如果没有拿到锁就要等待而我们这次是没有拿到锁和拿到锁都有不同的操作要执行。所有用不了这两个
我们可以用redis里面String数据类型的setnx命令来设置锁当key不存在的时候才可以赋值存在就不能赋值了
加锁setnx lock 1
释放锁del lock
为了避免死锁我们通常还会设置上有效期
加锁和释放锁的方法
实现代码
最后开启JMeter做测试模拟1000个线程发请求发现能扛住200qps
4、实践-逻辑删除
要用逻辑删除肯定要多个属性过期时间我们就可以采用这种方式多增加一个类这个类有过期时间属性然后多一个object类存放要存入redis的数据这样就不用在原来的类上改动
编写添加到redis的方法
这个plusSeconds就是设置过期时间用户参数传进来
重写查询的方法
七、缓存工具封装
我们发现缓存的操作还是挺复杂的我们将来使用的时候不可能每次都这样重新写一遍。 我们可以将这些解决方案封装成工具。
我们封装四个方法1和3可以是平时添加缓存用的2和4一般是缓存热点数据用防止击穿问题
1、封装存储方法将任意java对象序列化为json并存储在string类型的key中并且设置ttl过期时间
2、封装存储方法逻辑过期将任意java对象序列化为json并存储在string类型的key中并且设置逻辑过期时间用于处理缓存击穿问题
3、查询缓存空值解决击穿根据指定key查询缓存并反序列化为指定类型利用缓存空值的方式解决缓存穿透
4、查询缓存逻辑删除解决穿透根据指定key查询缓存并反序列化为指定类型需要用逻辑过期解决缓存击穿
工具类
@Slf4j
@Component
public class CacheClient {
@Autowired
private StringRedisTemplate stringRedisTemplate;
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
public CacheClient(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
/**
* 向缓存中添加 key
* */
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)));
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、根据 Id 查询 Redis
String json = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
// 2、判断 shopJSON 是否为空
if (StrUtil.isNotBlank(json)) {
// 3、存在直接返回
R r = JSONUtil.toBean(json, type);
return r;
}
// 增加对空字符串的判断
if(json != null){
return null;
}
// 4、不存在查询数据库
R r = dbFallback.apply(id);
// 5、不存在返回错误
if(r == null){
// 店铺不存在时缓存空值
stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
// 6、存在写入 Redis
this.set(key, r, time, unit);
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、根据 Id 查询 Redis
String json = stringRedisTemplate.opsForValue().get(key);
// 2、判断 shopJSON 是否为空
if (StrUtil.isBlank(json)) {
// 3、存在直接返回
return null;
}
// 命中需要先把 json 反序列化为对象
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
LocalDateTime expireTime = redisData.getExpireTime();
R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
// 判断是否过期
if(expireTime.isAfter(LocalDateTime.now())){
return r;
}
// 已过期需要缓存重建
// 获取互斥锁
String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
boolean isLock = tryLock(lockKey);
if (isLock) {
// 开辟独立线程
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
// 缓存重建
R r1 = dbFallback.apply(id);
this.setWithLogicalExpire(key, r1, time, unit);
}catch (Exception e) {
throw new RuntimeException(e);
} finally {
unLock(lockKey);
}
});
}
// 6、存在写入 Redis
this.set(key, r, time, unit);
return r;
}
// 获取锁
private boolean tryLock(String key){
Boolean isTrue = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", RedisConstants.LOCK_SHOP_TTL, TimeUnit.SECONDS);
return BooleanUtil.isTrue(isTrue);
}
// 释放锁
private void unLock(String key){
stringRedisTemplate.delete(key);
}
}
调用的service
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private CacheClient cacheClient;
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
@Override
public Result queryById(Long id) {
// 解决缓存穿透
// Shop shop = cacheClient.queryWithPassThrough(CACHE_SHOP_KEY, id, Shop.class, this::getById, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
// 互斥锁解决缓存击穿
// Shop shop = queryWithMetux(id);
// 使用逻辑过期时间解决缓存击穿问题
// Shop shop = queryWithLogicalExpire(id);
Shop shop = cacheClient.queryWithLogicalExpire(CACHE_SHOP_KEY, id, Shop.class, this::getById, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
if(shop == null){
return Result.fail("店铺不存在");
}
// 7、返回
return Result.ok(shop);
}
public Shop queryWithLogicalExpire(Long id){
// 1、根据 Id 查询 Redis
String shopJson = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
// 2、判断 shopJSON 是否为空
if (StrUtil.isBlank(shopJson)) {
// 3、存在直接返回
return null;
}
// 命中需要先把 json 反序列化为对象
RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
LocalDateTime expireTime = redisData.getExpireTime();
Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
// 判断是否过期
if(expireTime.isAfter(LocalDateTime.now())){
return shop;
}
// 已过期需要缓存重建
// 获取互斥锁
String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
boolean isLock = tryLock(lockKey);
if (isLock) {
// 开辟独立线程
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
// 缓存重建
this.saveShop2Redis(id, 20L);
}catch (Exception e) {
throw new RuntimeException(e);
} finally {
unLock(lockKey);
}
});
}
// 6、存在写入 Redis
stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
return shop;
}
}