缓存穿透:
概念:指查询一个一定不存在的数据,由于缓存是不命中的,将去查询数据库,但是数据库也无此记录,我们没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都要去数据库查询,失去缓存的意义!
风险:利用不存在的数据进行攻击,数据库瞬间压力增大,最终导致崩溃。
解决:null结果缓存,并加如短暂过期时间。
缓存雪崩:(开发中一般不会出现)
概念:是指在我们设置缓存时,key值采用相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬间压力过重雪崩。
解决:原有的失效时间基础上增加一个随机,比如1-5分组随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
缓存击穿:
概念:对于一些设置了过期时间的key,如果这些key可能会在某些时间点被高并发地访问,是一种非常“热点”的数据。如果这个key在大量请求同时进来的时候刚好失效,那么所有对这个key的数据查询都转义到DB上,DB就会压力过大,我们称为击穿。
解决:加锁,大量数据并发只让一个去查询,其他人等待,直到以后释放锁,其他人才能获取到锁,先查缓存,就会有数据,就不用去查询DB。
说明:缓存穿透,缓存雪崩只做简单解释,解决方案已经在上述中说明,相对简单,本篇文章只对缓存击穿提供对应的解决方案和代码使用,根据适用场景提供了三种方案
方案1.可以使用本地锁,这种只对单体项目或者是对数据要求不太高的负载项目,即单体项目一把锁(只设置一次redis),负载项目每个负载工程一把锁(负载下的每个工程一把锁,n个工程n把锁,设置n次redis)
org.springframework.boot
spring-boot-starter-data-redis
2.1.7.RELEASE
spring.redis.host=127.0.0.1
spring.redis.port=6379
控制层(controller):
/**
* 查询商品
* @return {@link String}
* @throws
* @author 李庆伟
* @date 2021/1/5 16:59
*/
@PostMapping("getCommoditysDemoOne")
public String getCommoditysDemoOne(){
String val = commoditysService.getCommoditysDemoOne();
if(StringUtils.isNotEmpty(val)){
return "ok";
}
return "error";
}
接口(service):
/**
* 查询商品
* @return {@link String}
* @throws
* @author 李庆伟
* @date 2021/1/5 16:59
*/
String getCommoditysDemoOne();
实现(impl):
@Autowired
StringRedisTemplate stringRedisTemplate;
/**
* 查询商品 锁中的this是当前实例的意思
* @return {@link String}
* @throws
* @author 李庆伟
* @date 2021/1/5 16:59
*/
public String getCommoditysDemoOne() {
String val = stringRedisTemplate.opsForValue().get("commoditys");
if(StringUtils.isEmpty(val)){
synchronized (this){
val = stringRedisTemplate.opsForValue().get("commoditys");
if(StringUtils.isNotEmpty(val)){
return val;
}
//模拟商品数据开始
List
方案2.可以使用分布式锁,它能解决单体项目或者分布式项目,适用于高数据要求负载项目,即负载项目所有负载工程抢占一把锁(只设置一次redis)[这种分布式锁不推荐使用]
org.springframework.boot
spring-boot-starter-data-redis
2.1.7.RELEASE
控制层(controller):
/**
* 查询商品
* @return {@link String}
* @throws
* @author 李庆伟
* @date 2021/1/5 16:59
*/
@PostMapping("getCommoditysDemoTwo")
public String getCommoditysDemoTwo(){
String val = commoditysService.getCommoditysDemoTwo();
if(StringUtils.isNotEmpty(val)){
return "ok";
}
return "error";
}
接口(service):
/**
* 查询商品
* @return {@link String}
* @throws
* @author 李庆伟
* @date 2021/1/5 16:59
*/
String getCommoditysDemoTwo();
实现(impl):
@Autowired
StringRedisTemplate stringRedisTemplate;
/**
* 查询商品
* @return {@link String}
* @throws
* @author 李庆伟
* @date 2021/1/5 16:59
*/
public String getCommoditysDemoTwo() {
String val = stringRedisTemplate.opsForValue().get("hello");
if(StringUtils.isEmpty(val)){
//1.抢占分布式锁 去redis占, setIfAbsent true表示占用redis锁成功
String uuid = UUID.randomUUID().toString().replace("-","");
//Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock",uuid);//没有设置超时时间
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock",uuid,300,TimeUnit.SECONDS);//
if(lock){
log.info("线程抢占锁成功,进来了。。。。。。");
//2.给锁设置过期时间
stringRedisTemplate.expire("lock",20, TimeUnit.SECONDS);
//占锁成功,业务数据放入redis
try {
stringRedisTemplate.opsForValue().set("hello","world");//业务数据
} finally {
// 问题思考A:a.如果我们在执行业务代码的时候直接抛异常了,还没走下面的删除锁操作,会出现什么问题
// b.如果我们在执行完代码,还没有走下面的删除锁操作,会出现什么问题
// A问题解决:给锁设置一个自动过期时间,即使中途出现了问题,过期时间到后还是会自动删除锁
// 给锁设置过期时间方法:stringRedisTemplate.expire("lock",30, TimeUnit.SECONDS);
// 问题思考B:a.如果我们在刚拿到锁的时候,还没来的及设置锁的过期时间就断电了,有怎么办
//
// B问题解决:抢占锁和设置锁过期时间必须是同步完成,具有原子性
// Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock",uuid,300,TimeUnit.SECONDS);
// 注:有时候setIfAbsent只能设置key value,不能设置超时时间,那是因为可以设置超时时间的必须要求
// pring-boot-starter-data-redis为2.1版本以上
//删除锁(不合理的删除锁)
//String lockVal = stringRedisTemplate.opsForValue().get("lock");//不合理的删除锁
//if(lockVal != null && uuid.equals(lockVal)){//不合理的删除锁
// stringRedisTemplate.delete("lock");//不合理的删除锁
//}//不合理的删除锁
// 问题思考C:a.删除锁问题,如果我们设置锁超时时间为20秒,结果我们的业务代码处理超过了20秒,这个时候锁已过期,
// 其他线程又会抢占锁,我们在删除锁的时候就会删除的不是自己的锁
// Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock",uuid,300,TimeUnit.SECONDS);
// 注:有时候setIfAbsent只能设置key value,不能设置超时时间,那是因为可以设置超时时间的必须要求
// pring-boot-starter-data-redis为2.1版本以上
// b.删除锁问题,假如我们设置锁超时时间为20秒,结果我们的业务代码处理超过了18秒,我们查询锁释放存在用了2秒,
// 返回结果刚好锁是自己的锁,我们这个时候在去删除锁,我们在删除锁的过程中,锁已经失效了,其他的线程有生产了新的锁,
// 这样我们还是会把别人的锁删除掉
// C问题解决: a.我们在删除锁的时候先去查询一下,看是不是自己创建的锁,如果是就删除,如果不是就不删除
// b.所以要处理a b问题,必须使 获取对比值和删除锁为原子操作
//删除锁(正确的删除锁方法)
String str = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
stringRedisTemplate.execute(new DefaultRedisScript(str,Long.class),Arrays.asList("lock"),uuid);
}
return stringRedisTemplate.opsForValue().get("hello");
} else {
log.info("线程抢占锁失败,。。。。。。");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
//占锁失败 循环等待
return getCommoditysDemoTwo();
}
}else {
log.info("完成查询 "+Thread.currentThread().getId());
return val;
}
}
方案3.可以使用分布式锁,它能解决单体项目或者分布式项目,适用于高数据要求负载项目,即负载项目所有负载工程抢占一把锁(只设置一次redis)[推荐使用]
org.springframework.boot
spring-boot-starter-data-redis
2.1.7.RELEASE
org.redisson
redisson
3.14.1
配置类:
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author 李庆伟
* @date 2021/1/11 11:06
*/
@Configuration
public class RedissonConfig {
/*
* 本机地址可以直接写下面两行代码
* 默认连接地址 127.0.0.1:6379
* RedissonClient redisson = Redisson.create();
* return redisson;
*/
@Bean
public RedissonClient redissonClient(){
//是不是本机地址都可以这样配置(非集群版配置)
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);
return redisson;
}
}
控制层(controller):
/**
* @return {@link String}
* @throws
* @author 李庆伟
* @date 2021/1/5 16:59
*/
@GetMapping("getCommoditysDemoThr")
public String getCommoditysDemoThr(){
String val = commoditysService.getCommoditysDemoThr();
if(StringUtils.isNotEmpty(val)){
return "ok";
}
return "error";
}
接口(service):
/**
* @return {@link String}
* @throws
* @author 李庆伟
* @date 2021/1/5 16:59
*/
String getCommoditysDemoThr();
实现(impl):
/**
* @return {@link String}
* @throws
* @author 李庆伟
* @date 2021/1/5 16:59
*/
public String getCommoditysDemoThr() {
String val = stringRedisTemplate.opsForValue().get("hello");
if(StringUtils.isEmpty(val)){
RLock lock = redissonClient.getLock("hello-lock");//获取锁
lock.lock();//加锁,看门狗机制,锁会自动续期 默认设置时间为30秒,续期时间每20秒开始 每次加10秒,该方法是阻塞方法
//lock.lock(30,TimeUnit.SECONDS);//加锁,这种一旦设置了时间不会自动续期,工作中建议使用这种,加锁时间一般大于业务处理时间,该方法是阻塞方法
//boolean lockVal = lock.tryLock(); //方法是非阻塞方法
try {
log.info("业务处理啦 "+Thread.currentThread().getId());
stringRedisTemplate.opsForValue().set("hello","world");
Thread.sleep(30000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();//解锁
}
return stringRedisTemplate.opsForValue().get("hello");
} else {
return val;
}
}
这种方法相比于上面一种更简单方便!
锁说明:
重用锁:案例2和3已经应用。
读写锁(写读锁):写的时候读等待,读的时候写等待,读的时候在读不等待,写的时候阻塞
@Autowired
StringRedisTemplate stringRedisTemplate;
@Autowired
RedissonClient redissonClient;
/**
* 写锁,写的时候读不了
* @return {@link String}
* @throws
* @author 李庆伟
* @date 2021/1/5 16:59
*/
@GetMapping("writeDemoFour")
public String writeDemoFour(){
RReadWriteLock lock = redissonClient.getReadWriteLock("myReadWriteLock");
RLock rLock = lock.writeLock();
rLock.lock();
try {
log.info("这个是写锁。。。。。。。。。正在写中");
Thread.sleep(30000);
stringRedisTemplate.opsForValue().set("readDemoFour","readDemoFour");
log.info("这个是写锁。。。。。。。。。readDemoFour");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rLock.unlock();
return "readDemoFour";
}
}
/**
* 读锁,写完之后才能读
* @return {@link String}
* @throws
* @author 李庆伟
* @date 2021/1/11 18:01
*/
@GetMapping("readDemoFour")
public String readDemoFour(){
RReadWriteLock lock = redissonClient.getReadWriteLock("myReadWriteLock");
RLock rLock = lock.readLock();
rLock.lock();
try {
log.info("这个是读锁。。。。。。。。。正在读中");
String val = stringRedisTemplate.opsForValue().get("readDemoFour");
log.info("这个是读锁。。。。。。。。。readDemoFour = "+val);
} finally {
rLock.unlock();
return stringRedisTemplate.opsForValue().get("readDemoFour");
}
}
公平锁:假如100个线程来抢锁,第一个抢到释放后,由第二个线程来继承锁,按照顺序来。(暂时没有遇到这方面的需求,没有举例)
信号量:适用于做秒杀,停车等需求
@Autowired
StringRedisTemplate stringRedisTemplate;
@Autowired
RedissonClient redissonClient;
/**
* 停车位模拟-设置停车位数量
* @return {@link String}
* @throws
* @author 李庆伟
* @date 2021/1/11 18:01
*/
@GetMapping("carNoDemoFive")
public String carNoDemoFive() {
stringRedisTemplate.opsForValue().set("carNo","5");
return "ok";
}
/**
* 停车位模拟-停车
* @return {@link String}
* @throws
* @author 李庆伟
* @date 2021/1/11 18:01
*/
@GetMapping("parkDemoFive")
public String parkDemoFive() throws InterruptedException {
RSemaphore rs = redissonClient.getSemaphore("carNo");
//rs.acquire();//这个没有返回值,这个是阻塞线程
//return "ok";
boolean c = rs.tryAcquire();//占位且非阻塞线程
return c+"";
}
/**
* 停车位模拟-车开走
* @return {@link String}
* @throws
* @author 李庆伟
* @date 2021/1/11 18:01
*/
@GetMapping("goDemoFive")
public String goDemoFive(){
RSemaphore rs = redissonClient.getSemaphore("carNo");
rs.release();//这个没有返回值
return "ok";
}