重学Redis解决方案(五)—— 缓存击穿问题及解决思路及封装工具类
缓存击穿
缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
解决方案
常见的解决方案有两种:
- 互斥锁
- 逻辑过期
逻辑分析:假设线程1在查询缓存之后,本来应该去查询数据库,然后把这个数据重新加载到缓存的,此时只要线程1走完这个逻辑,其他线程就都能从缓存中加载这些数据了,但是假设在线程1没有走完的时候,后续的线程2,线程3,线程4同时过来访问当前这个方法, 那么这些线程都不能从缓存中查询到数据,那么他们就会同一时刻来访问查询缓存,都没查到,接着同一时间去访问数据库,同时的去执行数据库代码,对数据库访问压力过大
使用互斥锁解决缓存击穿问题
因为锁能实现互斥性。假设线程过来,只能一个人一个人的来访问数据库,从而避免对于数据库访问压力过大,但这也会影响查询的性能,因为此时会让查询的性能从并行变成了串行,我们可以采用tryLock方法 + double check来解决这样的问题。
假设现在线程1过来访问,他查询缓存没有命中,但是此时他获得到了锁的资源,那么线程1就会一个人去执行逻辑,假设现在线程2过来,线程2在执行过程中,并没有获得到锁,那么线程2就可以进行到休眠,直到线程1把锁释放后,线程2获得到锁,然后再来执行逻辑,此时就能够从缓存中拿到数据了。
核心思路:相较于原来从缓存中查询不到数据后直接查询数据库而言,现在的方案是 进行查询之后,如果从缓存没有查询到数据,则进行互斥锁的获取,获取互斥锁后,判断是否获得到了锁,如果没有获得到,则休眠,过一会再进行尝试,直到获取到锁为止,才能进行查询
如果获取到了锁的线程,再去进行查询,查询后将数据写入redis,再释放锁,返回数据,利用互斥锁就能保证只有一个线程去执行操作数据库的逻辑,防止缓存击穿
操作锁的代码:
核心思路就是利用redis的setnx方法来表示获取锁,该方法含义是redis中如果没有这个key,则插入成功,返回1,在stringRedisTemplate中返回true, 如果有这个key则插入失败,则返回0,在stringRedisTemplate返回false,我们可以通过true,或者是false,来表示是否有线程成功插入key,成功插入的key的线程我们认为他就是获得到锁的线程。
package com.luoxiao.hmdp.service.impl;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.luoxiao.hmdp.dto.Result;
import com.luoxiao.hmdp.entity.Shop;
import com.luoxiao.hmdp.mapper.ShopMapper;
import com.luoxiao.hmdp.service.IShopService;
import com.luoxiao.hmdp.utils.CacheClient;
import com.luoxiao.hmdp.utils.RedisConstants;
import com.luoxiao.hmdp.utils.RedisData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 解决缓存穿透
*/
@Override
public Result queryShopById(Long id) {
//互斥锁解决缓存击穿
Shop shop = queryWithMutex(id);
Shop shop = cacheClient.queryWithLogicalExpire(RedisConstants.CACHE_SHOP_KEY, id, this::getById,Shop.class);
if (shop == null){
return Result.fail("店铺不存在");
}
return Result.ok(shop);
}
/**
* 缓存击穿解决方案,互斥锁
*/
private Shop queryWithMutex(Long id) {
String key = RedisConstants.CACHE_SHOP_KEY + id;
//redis中存在
String shopJson = redisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(shopJson)){
return JSONUtil.toBean(shopJson, Shop.class);
}
//判断命中的是否是空值
if (shopJson == null) {
return null;
}
//互斥锁
Shop shop = null;
String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
try {
boolean isLock = tryLock(lockKey);
//判断是否获取成功
if (!isLock){
//失败休眠重试
Thread.sleep(50);
return queryWithMutex(id);
}
//redis中不存在
shop = getById(id);
Thread.sleep(200);
if (shop == null){
//设置空值
redisTemplate.opsForValue().set(key, "",RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
redisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop),RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
} catch (Exception e) {
throw new RuntimeException(e);
}finally {
//释放锁
unLock(lockKey);
}
return shop;
}
/**
* 获取锁
*/
private boolean tryLock(String key){
Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
/**
* 释放锁
*/
private void unLock(String key){
redisTemplate.delete(key);
}
}
使用逻辑过期解决缓存击穿问题
需求:修改根据id查询商铺的业务,基于逻辑过期方式来解决缓存击穿问题
思路分析:当用户开始查询redis时,判断是否命中,如果没有命中则直接返回空数据,不查询数据库,而一旦命中后,将value取出,判断value中的过期时间是否满足,如果没有过期,则直接返回redis中的数据,如果过期,则在开启独立线程后直接返回之前的数据,独立线程去重构数据,重构完成后释放互斥锁。
步骤一、添加一个实体类用于保存数据和逻辑过期时间
@Data
public class RedisData<T> {
/**
* 逻辑过期时间
*/
private LocalDateTime expireTime;
/**
* 数据对象
*/
private T data;
}
步骤二、利用单元测试进行缓存预热
/**
* 将店铺信息存入到redis中
*/
private void saveShopRedis(Long id, Long expireSeconds){
Shop shop = getById(id);
//封装逻辑过期时间
RedisData redisData = new RedisData();
redisData.setData(shop);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
//写入Redis
redisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}
@Test
void testSaveShop(){
Shop shop = shopService.getById(1L);
cacheClient.setWithLogicalExpire(CACHE_SHOP_KEY + 1L, shop, 10L, TimeUnit.SECONDS);
}
步骤三:正式代码
package com.luoxiao.hmdp.service.impl;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.luoxiao.hmdp.dto.Result;
import com.luoxiao.hmdp.entity.Shop;
import com.luoxiao.hmdp.mapper.ShopMapper;
import com.luoxiao.hmdp.service.IShopService;
import com.luoxiao.hmdp.utils.CacheClient;
import com.luoxiao.hmdp.utils.RedisConstants;
import com.luoxiao.hmdp.utils.RedisData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
/**
* 解决缓存击穿
*/
@Override
public Result queryShopById(Long id) {
//设置逻辑过期解决缓存击穿
Shop shop = queryWithLogicalExpire(id);
Shop shop = cacheClient.queryWithLogicalExpire(RedisConstants.CACHE_SHOP_KEY, id, this::getById,Shop.class);
if (shop == null){
return Result.fail("店铺不存在");
}
return Result.ok(shop);
}
public Shop queryWithLogicalExpire( 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<Shop> redisData = JSONUtil.toBean(json, RedisData.class);
Shop shop = redisData.getData();
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){
threadPoolTaskExecutor.submit( ()->{
try{
//重建缓存
this.saveShop2Redis(id,20L);
}catch (Exception e){
throw new RuntimeException(e);
}finally {
unlock(lockKey);
}
});
}
// 6.4.返回过期的商铺信息
return shop;
}
/**
* 获取锁
*/
private boolean tryLock(String key){
Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
/**
* 释放锁
*/
private void unLock(String key){
redisTemplate.delete(key);
}
}
Redis工具类封装
- 方法1:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间
- 方法2:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓
存击穿问题
- 方法3:根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
- 方法4:根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题
将逻辑进行封装
package com.luoxiao.hmdp.utils;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.luoxiao.hmdp.entity.Shop;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
@Slf4j
@Component
public class CacheClient {
private StringRedisTemplate redisTemplate;
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
public CacheClient(StringRedisTemplate redisTemplate, ThreadPoolTaskExecutor threadPoolTaskExecutor) {
this.redisTemplate = redisTemplate;
this.threadPoolTaskExecutor = threadPoolTaskExecutor;
}
/**
* 存入redis
* @param key key
* @param value 对象
*/
public void set(String key, Object value){
redisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),RedisConstants.CACHE_SHOP_TTL,TimeUnit.MINUTES);
}
/**
* 存入redis
* @param key key
* @param value 对象
* @param time 时间
*/
public void set(String key, Object value,Long time){
redisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,TimeUnit.MINUTES);
}
/**
* 存入redis
* @param key key
* @param value 对象
* @param unit 时间单位
*/
public void set(String key, Object value, TimeUnit unit){
redisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),RedisConstants.CACHE_SHOP_TTL,unit);
}
/**
* 存入redis
* @param key key
* @param value 对象
* @param time 时间
* @param unit 时间单位
*/
public void set(String key, Object value, Long time, TimeUnit unit){
redisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,unit);
}
/**
* 逻辑过期
* @param key key
* @param value 对象
*/
public void setWithLogicalExpire(String key, Object value){
RedisData<Object> data = new RedisData<>();
data.setExpireTime(LocalDateTime.now().plusSeconds(RedisConstants.CACHE_SHOP_TTL));
data.setData(value);
redisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(data));
}
/**
* 逻辑过期
* @param key key
* @param value 对象
* @param time 时间
* @param unit 时间单位
*/
public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit){
RedisData<Object> data = new RedisData<>();
data.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
data.setData(value);
redisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(data));
}
/**
* 缓存穿透解决方案,缓存未命中设置空值(带有逻辑过期的查询)
* @param keyPrefix key前缀
* @param id id
* @param dbFallback 查询接口
* @param type 返回类型
* @return R 返回对应类型
*/
public <R,ID> R queryWithLogicalExpire(String keyPrefix, ID id,Function<ID,R> dbFallback, Class<R> type) {
return queryWithLogicalExpire(keyPrefix, id, dbFallback,RedisConstants.CACHE_SHOP_TTL,TimeUnit.SECONDS,type);
}
/**
* 缓存穿透解决方案,缓存未命中设置空值(带有逻辑过期的查询)
* @param keyPrefix key前缀
* @param id id
* @param dbFallback 查询接口
* @param type 返回类型
* @return R 返回对应类型
*/
public <R,ID> R queryWithLogicalExpire(String keyPrefix, ID id,Function<ID,R> dbFallback, Long time, TimeUnit unit, Class<R> type) {
//redis中存在
String key = keyPrefix + id;
String shopJson = redisTemplate.opsForValue().get(key);
if (StrUtil.isBlank(shopJson)){
return null;
}
//命中判断过期时间
String redisDataJson = redisTemplate.opsForValue().get(key);
RedisData<R> redisData = JSONUtil.toBean(redisDataJson, RedisData.class);
R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
//未过期
if (redisData.getExpireTime().isAfter(LocalDateTime.now())){
return r;
}
//已过期,重建缓存
//获取互斥锁
String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
boolean isLock = tryLock(lockKey);
if (isLock){
//成功,开启独立线程重建缓存
threadPoolTaskExecutor.submit(()->{
try {
//查询数据库
R r1 = dbFallback.apply(id);
//写入redis
this.setWithLogicalExpire(key, r1,time,unit);
} catch (Exception e) {
throw new RuntimeException(e);
}finally {
//释放锁
unLock(lockKey);
}
});
}
return r;
}
/**
* 未命中设置空值,解决缓存穿透
* @param keyPrefix key前缀
* @param id id
* @param dbFallback 查询接口
* @param type 返回类型
* @return R 返回对应类型
*/
public <R,ID> R queryWithPassThrough(String keyPrefix, ID id, Function<ID,R> dbFallback, Class<R> type){
return queryWithPassThrough(keyPrefix,id,dbFallback,RedisConstants.CACHE_SHOP_TTL,TimeUnit.MINUTES,type);
}
/**
* 未命中设置空值,解决缓存穿透
* @param keyPrefix key前缀
* @param id id
* @param dbFallback 查询接口
* @param time 时间
* @param unit 时间单位
* @param type 返回类型
* @return R 返回对应类型
*/
public <R,ID> R queryWithPassThrough(String keyPrefix, ID id, Function<ID,R> dbFallback,Long time, TimeUnit unit,Class<R> type){
String key = keyPrefix + id;
//从redis中查询缓存
String json = redisTemplate.opsForValue().get(key);
//缓存存在且不为空字符串,返回对象
if (StrUtil.isNotBlank(json)){
return JSONUtil.toBean(json,type);
}
//判断命中是否是空值
if (json != null ){
return null;
}
//不存在,根据id查询数据库
R r = dbFallback.apply(id);
if (r == null) {
redisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL,TimeUnit.SECONDS);
return null;
}
this.set(key, r,time,unit);
return r;
}
/**
* 获取锁
*/
private boolean tryLock(String key){
Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
/**
* 释放锁
*/
private void unLock(String key){
redisTemplate.delete(key);
}
}
代码中使用工具类
@Override
public Result queryShopById(Long id) {
//未命中设置空值解决缓存穿透
//Shop shop = queryWithPassThrough(id);
//Shop shop = cacheClient.queryWithPassThrough(RedisConstants.CACHE_SHOP_KEY, id, this::getById, Shop.class);
//互斥锁解决缓存击穿
//Shop shop = queryWithMutex(id);
//设置逻辑过期解决缓存击穿
//Shop shop = queryWithLogicalExpire(id);
Shop shop = cacheClient.queryWithLogicalExpire(RedisConstants.CACHE_SHOP_KEY, id, this::getById,Shop.class);
if (shop == null){
return Result.fail("店铺不存在");
}
return Result.ok(shop);
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· winform 绘制太阳,地球,月球 运作规律
· 上周热点回顾(3.3-3.9)