话不多说直接入正题,首先了解Redis 缓存穿透,击穿是什么概念;
缓存击穿:指单个热点key大量请求,而redis缓存由于某种原因刚好失效,直接请求DB(该数据存在数据库),导致数据库承受不住压力宕机,服务因此被干懵了;
缓存穿透:大量请求数据库一定不存在的数据,由于缓存不起作用,数据库被大量请求,又被干懵了
一般这种问题的解决方式,我采用的是加锁,用互斥锁让并发线程排队,实现对数据库压力的减少!但是直接锁每一个线程会造成资源浪费,假设30个并发线程,其实只需要第一个线程拿到锁,请求数据库,再set到redis里去,其余29个线程直接拿redis 的数据就可以了!
对于缓存穿透是博主采用的直接暴力给空对象,设置过期时间,让其尽量少走数据库!但是这种情况只是对请求的key是一定的,但实际生产中,对于大量的随机key是无效的,后续优化采用业内常用的布隆过滤器方法~
请求直接上代码
@Override
public User queryById(int id) throws InterruptedException {
User user= (User) redisTemplate.opsForValue().get(id+"");
if (null==user)
{
//排队拿到锁,请求数据库
if (reentrantLock.tryLock())
{
try {
System.out.println(Thread.currentThread().getName()+"拿到锁请求数据库--》");
user=deptDao.queryById(id);
if (user==null)
{
//防止缓存穿透 设置空对象
redisTemplate.opsForValue().set(id+"",new User(),30, TimeUnit.MINUTES);
}else {
redisTemplate.opsForValue().set(id+"",user);
}
}
finally {
reentrantLock.unlock();
}
}else{
user =(User)redisTemplate.opsForValue().get(id+"");
if (null==user)
{
System.out.println(Thread.currentThread().getName()+"等待--》");
Thread.sleep(100);
return queryById(id);
}
}
}
这个方法的好处就是不需要让每个线程都请求数据,高并发时仅有少数几条数据请求数据库,博主自己测试500并发请求,也只有偶尔两条线程请求数据库,按照这个比例数据库完成能hold住,上述代码是service 层;
废话不多说直接看结果!
首先controller 模拟500并发请求service
@GetMapping("/dept/get/{id}")
public List<User> queryById (@PathVariable int id) throws InterruptedException {
List<User> list=new CopyOnWriteArrayList<>();
for (int i = 0; i <500 ; i++) {
new Thread(()->{
try {
User user= deptService.queryById(id);
list.add(user);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"Thead"+i).start();
}
Thread.sleep(5000);
System.out.println("当前主线程"+list.size());
return list;
}
因为代码测试是不是所有的并发请求都拿到正确的值,所以采用list集合装所有访问结果,但是由于并发是同时抢cpu资源,肯定会造成主线程结束,其余并发请求未结束,造成list的size 一直为0 !为此主线程需等待5s,手动设置主线程最后结束,才能拿到所有的请求结果!
模拟测试:缓存击穿
redis里只有ID为2,3,4的数据,所以设置请求ID为1(数据库存在),
浏览器输入请求地址http://localhost:8001/dept/get/1
运行结果:
可以看到500并发线程,仅有不到5条请求直接落到了DB上,其余的都是在等待0.1秒后,直接请求redis了!效果还是可以的!
为验证请求的数据是否正确,前台也返回出请求的结果的结果集合!
注:本人是刚毕业的小白,若有错误请指正,谢谢!