最近重新系统的学习一下redis,看了尚硅谷雷丰阳老师讲解的视频,在高并发场景下使用Redis,常常遇到会遇到一些问题,而且面试也经常会问到,好记性不如烂笔头,遂记录如下:
只是最基础的,查缓存,有就存,没有就查db。复现“缓存穿透、缓存雪崩、缓存击穿”场景代码:
@Slf4j
@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {
@Autowired
StringRedisTemplate stringRedisTemplate;
@Override
public Map<String, List<Catelog2Vo>> getCatelogJson() {
String catelogkey = "CatelogJson";
//拿缓存
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
String reidsJson = ops.get(catelogkey);
//缓存中没值
if (StringUtils.isBlank(reidsJson)) {
//从db获取 ,方法略
Map<String, List<Catelog2Vo>> catelogJsonFromDb = this.getCatelogJsonFromDb();
//返回db中查询到的数据
ops.set(catelogkey, JSON.toJSONString(catelogJsonFromDb));
return catelogJsonFromDb;
} else {
//返回缓存获取到的数据
return JSON.parseObject(reidsJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {
});
}
}
}
为了解决上述常见的“3种缓存问题”,优化如下:
- 解决【缓存穿透】:null 结果缓存,并加入短暂的过期时间
- 解决【缓存雪崩】:原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
- 解决【缓存击穿】:
加“本地进程锁” (synchronized
关键字、JUC包下的各种Lock
锁类),大量并发时只让一个线程去查,其他线程等待,查到以后释放锁,其他线程获取到锁,先查缓存,就会有数据,不用去db查。
@Slf4j
@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {
@Autowired
StringRedisTemplate stringRedisTemplate;
@Override
public Map<String, List<Catelog2Vo>> getCatelogJson() {
//拿缓存
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
String reidsJson = ops.get(CATELOG_KEY);
//缓存中没值
if (StringUtils.isBlank(reidsJson)) {
log.info("缓存不命中....查询数据库");
//从 本地获取
Map<String, List<Catelog2Vo>> catelogJsonFromDb = this.getCatelogJsonByLock();
String s = JSON.toJSONString(catelogJsonFromDb);
//等概率 生成 [1-1024]的随机数 ,方法略
int i = RandomUtils.sumBinary(RandomUtils::getZeroOrOne, 9) + 1;
//db数据放入缓存中 ,设置过期时间: 3600秒 + [1-1024]的随机数
ops.set(CATELOG_KEY, s, 30 * 60 + i, TimeUnit.SECONDS);
//返回db中查询到的数据
return catelogJsonFromDb;
} else {
log.info("缓存命中....直接返回...");
//返回缓存获取到的数据
return JSON.parseObject(reidsJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {
});
}
}
/**
* 从本地进程锁中获取 数据
*
* @return 菜单分类数据
*/
private synchronized Map<String, List<Catelog2Vo>> getCatelogJsonByLock() {
/*
只要是同一把锁,就能锁住需要这个锁的所有线程
1、使用 synchronized 方法 、synchronized(this)、JUC(Lock),SpirngBoot所有的组件在容器中都是单例的,所以单服务的情况下可以锁住。
2、synchronized、JUC(Lock),都是本地锁,只能锁住本地线程。但是在分布式场景下,想要锁住所有,则必须使用分布式锁。TODO
*/
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
String reidsJson = ops.get(CATELOG_KEY);
//缓存中没值
if (StringUtils.isBlank(reidsJson)) {
return this.getCatelogJsonFromDb();
} else {
//返回缓存获取到的数据
return JSON.parseObject(reidsJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {
});
}
}
/**
* 从数据库获取 菜单json数据
*
* @return 菜单分类数据
*/
private Map<String, List<Catelog2Vo>> getCatelogJsonFromDb() {
log.info("查询数据库...");
//db 查询略。
}
}
并发50 ,压测一段时间,结果看控制台打印如下图:
“查询数据库”的操作,执行了2次。没有锁住,为什么?
缓存的保存时,需要建立连接,中间花费的时间虽然很短暂,但毕竟需要时间。
在“db查到数据”,还没来得及将结果存入缓存中,就释放掉了锁;
在高并发时,下一个线程拿到锁就会进来重新判断一遍缓存是否有数据,没有,然后又进来“db查数据”,因此”打印了2次db查询数据库“。
“确认缓存有没有”、“db操作” 与 “存redis”的操作 放在一把锁内,保证 原子性。
“确认缓存有没有”、“db操作” 与 “存redis”的操作 放在一把锁内,保证 原子性。
@Slf4j
@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {
@Autowired
StringRedisTemplate stringRedisTemplate;
@Override
public Map<String, List<Catelog2Vo>> getCatelogJson() {
//拿缓存
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
String reidsJson = ops.get(CATELOG_KEY);
//缓存中没值
if (StringUtils.isBlank(reidsJson)) {
log.info("缓存不命中....查询数据库");
//返回db中查询到的数据,并保存到缓存
return this.getCatelogJsonByLock();
} else {
log.info("缓存命中....直接返回...");
//返回缓存获取到的数据
return JSON.parseObject(reidsJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {
});
}
}
/**
* 从本地进程锁中获取 数据
*
* @return 菜单分类数据
*/
private synchronized Map<String, List<Catelog2Vo>> getCatelogJsonByLock() {
/*
只要是同一把锁,就能锁住需要这个锁的所有线程
1、使用 synchronized 方法 、synchronized(this)、JUC(Lock),SpirngBoot所有的组件在容器中都是单例的,所以单服务的情况下可以锁住。
2、synchronized、JUC(Lock),都是本地锁,只能锁住本地线程。但是在分布式场景下,想要锁住所有,则必须使用分布式锁。TODO
*/
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
String reidsJson = ops.get(CATELOG_KEY);
//缓存中没值
if (StringUtils.isBlank(reidsJson)) {
Map<String, List<Catelog2Vo>> catelogJsonFromDb = this.getCatelogJsonFromDb();
String s = JSON.toJSONString(catelogJsonFromDb);
//等概率 生成 [1-1024]的随机数
int i = RandomUtils.sumBinary(RandomUtils::getZeroOrOne, 9) + 1;
//db数据放入缓存中 ,设置过期时间: 3600秒 + [1-1024]的随机数
ops.set(CATELOG_KEY, s, 30 * 60 + i, TimeUnit.SECONDS);
return catelogJsonFromDb;
} else {
//返回缓存获取到的数据
return JSON.parseObject(reidsJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {
});
}
}
/**
* 从数据库获取 菜单json数据
*
* @return 菜单分类数据
*/
private Map<String, List<Catelog2Vo>> getCatelogJsonFromDb() {
log.info("查询数据库...");
//db 查询略。
}
}
压测(并发50 ),控制台部分截图如下: “查询数据库”的操作只执行了一次。锁住了。
- 启动3个一样的服务,利用
--Server.port=xxx
,分别设置3个不同的端口号- 3个服务,均设置 VM 参数
-Xmx100m
,避免占用本地过多空间。- 启动网关服务,端口为:88
压测某个业务接口,走网关,通过网关分发到集群(3个实例)
redis缓存数据清空,确认还没存入缓存
设置,并发100,持续10次
结果:3个实例,分别命中一次“查询数据库”
只要是同一把锁,就能锁住需要这个锁的所有线程:
synchronized
标注方法 、synchronized(this){}
代码块、JUC(Lock)
” 这些锁 ,因为 SpirngBoot所有的组件在容器中都是单例的,所以单服务的情况下可以锁住。synchronized
关键字、JUC(Lock)
” 这些锁 ,都是本地锁,只能锁住本地线程。但是在分布式场景下,想要锁住所有,则必须使用"分布式锁"。