目录
什么是缓存
简单来讲就是,数据交换的缓冲区,是存储数据的临时地方,读写性能较高。
添加Redis缓存
作用模型--流程。应当把每次未命中的数据及时写入Redis缓存。
public Result queryById1(Long id) {
String key = CACHE_SHOP_KEY + id;
//1、从redis中查
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2、判断是否存在
if(shopJson != null){
//3、存在,直接返回
Shop shop = JSONUtil.toBean(shopJson,Shop.class);
return Result.ok(shop);
}
//4、不存在,查询数据库
Shop shop = getById(id);
if(shop == null){
//5、数据库不存在,返回错误
return Result.fail("数据错误!");
}
//6、数据库存在,写入redis
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop));
//7、返回
return Result.ok(shop);
}
缓存更新策略
缓存更新策略,有三种。
主动更新策略
目前软件系统采取的都是主动更新策略,能够对数据及时更新,保持一致。主动更新策略中有不同的更新方式。
方法一
在实际开发中,缓存更新策略,还是采取的方法一:由缓存的调用者,在更新缓存的同时,将数据写入数据库。
1、更新数据
采用删除缓存,再写入的策略。避免无效数据的更新。
2、保证数据库和缓存的操作同时成功或失败。
在单体系统中,将操作放在一个事务中。
在分布式系统中,利用TCC等分布式方案。
3、操作顺序
先操作数据库,再更新缓存。理由如下:
先删除缓存,后续访问的对象,获取到的数据都是旧数据,且因为是写入缓存的操作快于更新数据库操作。而且访问缓存的速度极快,这会导致很多对象获取的数据是缓存中的旧数据。发生概率极大。
@Override
@Transactional
public Result update(Shop shop) {
Long id = shop.getId();
if (id == null) {
return Result.fail("店铺id不能为空");
}
// 1.更新数据库
updateById(shop);
// 2.删除缓存
stringRedisTemplate.delete(CACHE_SHOP_KEY + id);
return Result.ok();
}
方法三
在Redis中又有两种不同方式的持久化策略,这里简单说一下。分别是AOF和RDB。
缓存遭到的三种攻击
1、缓存穿透
解决思路
(1)缓存空对象
当查询数据库,发现也没数据时,给Redis传一个null值,然后把null值返回给前端。
(2)布隆过滤
//空值解决
@Override
public Result queryById1(Long id) {
String key = CACHE_SHOP_KEY + id;
//1、从redis中查
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2、判断是否存在
if(StrUtil.isNotBlank(shopJson)){
//3、存在,直接返回
Shop shop = JSONUtil.toBean(shopJson,Shop.class);
return Result.ok(shop);
}
//判断命中的是否是空值
if(shopJson != null){
//返回错误信息
return Result.fail("店铺不存在");
}
//4、不存在,查询数据库
Shop shop = getById(id);
if(shop == null){
//5、数据库不存在,返回错误
//返回空值
stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
return Result.fail("店铺不存在!");
}
//6、数据库存在,写入redis
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop));
//7、返回
return Result.ok(shop);
}
2、雪崩
解决思路
1、设置随机的TTL值--在保存expireTime时,采取随机获取。
2、利用Redis集群提高服务的可用性--哨兵模式
3、给缓存业务添加降级限流策略
4、给业务添加多级缓存
3、缓存击穿
相对于缓存穿透而言,缓存击穿针对的是某个key是小范围的,而缓存穿透则是查询多个key未命中,是大范围的。 可根据这一点区分两者,避免造成知识混乱。
解决思路
1、互斥锁
2、逻辑过期
//互斥锁
public Shop queryWithMutex1(Long id) {
String key = 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 = LOCK_SHOP_KEY + id;
Shop shops = new Shop();
try {
boolean isLock = tryLock(lockKey);
// 4.2.判断是否获取成功
if (!isLock) {
// 4.3.获取锁失败,休眠并重试
Thread.sleep(50);
return queryWithMutex1(id);
}
// 4.4.获取锁成功,根据id查询数据库
Shop shop = getById(id);
// 5.不存在,返回错误
if (shop == null) {
// 将空值写入redis
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
// 返回错误信息
return null;
}
// 6.存在,写入redis
shops = shop;
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL,TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
// 7.释放锁
unlock(lockKey);
}
// 8.返回
return shops;
}
//逻辑过期
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
public Shop queryWithLogicalExpire1(Long id) {
String key = CACHE_SHOP_KEY + 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);
Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), 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_KEY + id;
boolean isLock = tryLock(lockKey);
// 6.2.判断是否获取锁成功
if (isLock){
// 6.3.成功,开启独立线程,实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
// 查询数据库
// 重建缓存
this.saveShop2Redis(id,20L);
} catch (Exception e) {
throw new RuntimeException(e);
}finally {
// 释放锁
unlock(lockKey);
}
});
}
// 6.4.返回过期的商铺信息
return shop;
}
private Shop saveShop2Redis(Long id,Long expireSeconds){
//查询店铺
Shop shop = getById(id);
//封装过期时间
RedisData redisData = new RedisData();
redisData.setData(shop);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
//写入Redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,JSONUtil.toJsonStr(redisData));
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);
}