添加缓存业务流程及代码实现
业务流程
说明:
1、先从redis中进行查询,redis中如果有对应的数据则直接返回;如果没有再进入数据库查询
2、从数据库查询到的数据判断是否为空,非空写入redis再返回
代码实现
ShopServiceImpl.getByIdWithCache()
private StringRedisTemplate stringRedisTemplate;
public ShopServiceImpl(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public Result getByIdWithCache(Long id) {
String key = CACHE_SHOP_KEY + id; //cache:shop:id
//1、根据id在redis中进行查询
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2、如果查询到了,直接返回商铺的信息
if(!StrUtil.isBlank(shopJson)){
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
//3、如果没有查到,在数据库中进行查找
Shop shop = query().eq("id", id).one();
//4、数据如果为空,直接返回
if(shop == null){
return Result.fail("无此店铺");
}
//5、数据不为空,将数据放至redis中,并设置过期时间
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES); //TTL过期时间
return Result.ok(shop);
}
缓存更新策略
主要有三种:内存淘汰(redis内存不够用时剔除缓存)、超时剔除(等缓存过期更新)、主动更新(修改数据库时更新缓存,常用)
业务场景:
低一致性需求:使用redis自带的内存淘汰即可。如店铺类型的查询
高一致性需求:主动更新,并以超时剔除作为兜底方案。如店铺详情的查询
主动更新策略
推荐采取Cache Aside Pattern,更新数据库,更新缓存
删除缓存还是更新缓存?
写多读少时,会存在大量用不到的缓存,如果每次写都修改缓存,可能造成缓存还未用到就已经失效了,造成了大量的无效写。因此在更新数据库时,将对应的redis缓存删除,再次查询时生成新的缓存。
如何保证缓存操作和数据库操作同时成功、同时失败?
在单体系统中,将二者的操作放在一个事务中;在分布式系统中,利用TCC等分布式事务方案
在保证操作原子性的基础上,应该先操作数据库还是先操作缓存?
应该先更新数据库中的数据,再删除缓存。考虑在并发情况下,如果先删除了redis的缓存,在完成写数据库之前有其他线程读取该条数据,则会重新生成旧的数据的redis缓存,并且由于数据库写数据的时间原大于redis删除数据的时间,造成此种情况的可能性非常大,数据不一致性的概率更大;而如果先更新数据库,那么redis删除数据的操作可以很快的接着数据库的写操作进行,造成数据不一致的概率小。
实现数据库和缓存的双写一致
ShopServiceImpl.updateByIdWithCache()
@Override
@Transactional
public Result updateByIdWithCache(Shop shop) {
//先判断id是否为空
Long id = shop.getId();
if(id == null) return Result.fail("该商铺不存在");
//1、更新数据库
updateById(shop);
//2、删除缓存
stringRedisTemplate.delete(CACHE_SHOP_KEY + id); //cache:shop:id
return Result.ok("更新成功");
}
缓存穿透及解决方法
什么是缓存穿透?
当查询的数据不存在时,即redis和数据库上都查不到对应的数据时;造成的redis失效,使得所有的这些空数据请求全部打到数据库上,造成缓存穿透。
如何解决
主要的有两种:布隆过滤器、缓存空对象。
预防方案:增强id的复杂度避免猜测到id的规律、做好数据的基础格式校验、加强用户权限校验、做好热点参数的限流
缓存空对象解决缓存穿透
业务流程
说明:
在基本的缓存业务上增加了两个逻辑判断
1、此时redis命中后需要判断是否为空
2、在数据库中如果查询的商铺数据为空,将空值写至redis中并返回
代码实现
对ShopServiceImpl.getByIdWithCache()进行优化,增加了redis空对象,防止缓存穿透
@Override
public Result getByIdWithCache(Long id) {
String key = CACHE_SHOP_KEY + id;
//1、根据id在redis中进行查询
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2、如果查询到了,直接返回商铺的信息
if(!StrUtil.isBlank(shopJson)){
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
//为防止缓存穿透在redis中增加了空值对象,需要判断是否为空
if(shop == null) return Result.fail("不存在该商铺");
return Result.ok(shop);
}
//3、如果没有查到,在数据库中进行查找
Shop shop = query().eq("id", id).one();
//4、数据如果为空,在redis中记录空值并返回
if(shop == null){
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "null", CACHE_SHOP_TTL, TimeUnit.MINUTES);
return Result.fail("无此店铺");
}
//5、数据不为空,将数据放至redis中,并设置过期时间
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
return Result.ok(shop);
}
缓存击穿及解决方法
什么是缓存击穿?
热点key突然失效了,在redis缓存中没有对应的数据,即没有命中缓存;而此时大量的请求都在请求这个数据,如果需要联表查询、或者获取数据比较慢时,无法及时返回数据,导致了大量的请求都越过redis缓存直接从数据库获取数据,无数请求访问给数据库带来巨大的冲击。
解决缓存击穿的两种思路
互斥锁解决缓存击穿:对商户的id加互斥锁,当有多个请求获取该商铺数据时,只允许互斥的访问数据库
逻辑过期解决缓存击穿:给商铺进行预热,提前将热点商铺信息放至redis中,设为永不过期。同时,为了保证数据的一致性,设置一个逻辑过期时间,在时间到了之后加锁并开辟一个新的线程进行更新;在更新数据时,未获取到锁的其他请求,获取redis中之前的旧数据返回,直至新的数据库数据更新至redis中
互斥锁解决缓存击穿
业务流程
代码实现
ShopServiceImpl.queryWithMutex()
/**
* 在防止缓存穿透(缓存空对象)的基础上也实现了防止缓存击穿(互斥锁)
* @param id
* @return
*/
@Override
public Shop queryWithMutex(Long id) {
String key = CACHE_SHOP_KEY + id;
//1、根据id在redis中进行查询
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2、如果查询到了,直接返回商铺的信息
if(StrUtil.isNotBlank(shopJson)){
return JSONUtil.toBean(shopJson, Shop.class);
}
/**
* ADD 如果未命中,则在此处进行缓存重建,在查询数据库前进行加锁
*/
Shop shop = null;
try {
//TODO ADD1尝试获取互斥锁
boolean hasLock = tryLock(LOCK_SHOP_KEY + id);
//TODO 获取锁之后应该再查一遍redis中是否有数据,可能其他请求在使用锁时就已经将数据存到redis中了
//TODO ADD2判断是否获取成功
if(!hasLock){
Thread.sleep(50);
//TODO ADD3如果失败休眠后重试
return queryWithMutex(id); //此处用递归就是有一个刷新redis的效果,再次获取锁之前去redis中再查一遍
}
//TODO ADD4获取锁成功,进入数据库查询
//3、如果没有查到,在数据库中进行查找
shop = query().eq("id", id).one();
Thread.sleep(500); //模拟缓存重建时间很长的情况下
//4、数据如果为空,将空对象放至redis中
if(shop == null){
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "null", CACHE_SHOP_TTL, TimeUnit.MINUTES);
return null;
}
//5、数据不为空,将数据放至redis中,并设置过期时间
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
}catch (InterruptedException e) {
throw new RuntimeException();
}finally {
//TODO ADD5释放互斥锁
unLock(LOCK_SHOP_KEY + id);
}
return shop;
}
//利用redis加临时互斥锁
private boolean tryLock(String key){
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS); //10s后过期
return BooleanUtil.isTrue(flag);
}
private void unLock(String key){
stringRedisTemplate.delete(key);
}
逻辑过期解决缓存击穿
业务流程
代码实现
RedisData.java
/**
* 基于逻辑过期解决缓存击穿
* 将过期时间和实际对象进行封装
*/
@Data
public class RedisData {
private LocalDateTime expireTime;
private Object data;
}
ShopServiceImpl.queryWithLogicalExpire()
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
public Shop queryWithLogicalExpire(Long id){
String key = CACHE_SHOP_KEY + id;
//1、根据id在redis中进行查询
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2、如果未命中,直接返回空值
if(StrUtil.isBlank(shopJson)){
return null;
}
//3、命中,需要把json反序列化为对象
RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
JSONObject data = (JSONObject) redisData.getData();
LocalDateTime expireTime = redisData.getExpireTime();
Shop shop = JSONUtil.toBean(data, Shop.class);
//4、判断逻辑逻辑时间是否过期
if(LocalDateTime.now().isAfter(expireTime)){
//5、未过期,直接返回商铺信息
return shop;
}
String lockKey = LOCK_SHOP_KEY + id;
//6、过期,尝试获取互斥锁
boolean isLock = tryLock(lockKey);
//7、如获取到互斥锁,开启一个新的线程,进行缓存重建
//获取到锁后应进行DoubleCheck,再次查询Redis
if(isLock){
//重新查数据库并将新数据写到redis中
CACHE_REBUILD_EXECUTOR.submit(()->{
try {
//8、缓存重建
saveShop2Redis(id, 20L);
}catch (Exception e){
throw new RuntimeException();
}finally {
//8、释放锁
unLock(lockKey);
}
});
}
//9、返回redis中的旧数据
return shop;
}
@Override //提前做数据预热,将热点店铺信息先存入redis中
public void saveShop2Redis(Long id, Long expireSeconds) throws InterruptedException {
//1、查询店铺数据
Shop shop = query().eq("id", id).one();
Thread.sleep(200); //模拟数据库联表查询的耗时
//2、封装逻辑过期时间
RedisData redisData = new RedisData();
redisData.setData(shop);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
//3、写入Redis,不设过期时间
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}
缓存雪崩及解决方法
什么是缓存雪崩?
同一时间缓存中大量的key同时失效,或者redis服务宕机,导致大量请求到达数据库,带来巨大压力
解决方法
给不同key的TTL添加随机值、利用Redis集群提高服务的可用性、给缓存业务增加降级限流策略、给业务添加多级缓存
标签:shop,缓存,return,数据库,redis,查询,id From: https://www.cnblogs.com/Gw-CodingWorld/p/16797450.html