缓存穿透是指 查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,我们没有将这次查询的 null 写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。
在流量大时,可能 DB 就挂掉了,要是有人利用不存在的 key 频繁攻击我们的应用,这就是漏洞。
解决方法:缓存空结果、并且设置短的过期时间。
缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到 DB,DB 瞬时压力过重雪崩。
解决方法:原有的失效时间基础上增加一个随机值,比如 1-5 分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
对于一些设置了过期时间的 key,如果这些 key 可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。
这个时候,需要考虑一个问题:如果这个 key 在大量请求同时进来前正好失效,那么所有对这个 key 的数据查询都落到 db,我们称为缓存击穿。
解决方法:加锁。大量并发只让一个人去查,其他人等待,查到之后释放锁,其他人获取到锁,先查缓存,就会有数据,不用去查数据库。
/**
* 从数据库查询
* 分布式锁:自己实现的
*/
public Map<String, List<Test>> getmDbWithRedisLock() {
// 1、占本分布式锁。去redis占坑,同时设置过期时间
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
if (lock) {
// 加锁成功....执行业务【内部会判断一次redis是否有值】
Map<String, List<Test>> dataFromDB = null;
try {
/**
* 1、首先还是要先查询redis是否有数据,有数据直接返回,因为可能已经先有人拿到了锁查询了数据
* 2、查询数据库获取数据库数据
* 3、将查询数据库的数据设置redis
*/
dataFromDB = formDb();
} finally {
// 2、查询UUID是否是自己,是自己的lock就删除
// 查询+删除 必须是原子操作:lua脚本解锁
String luaScript = "if redis.call('get',KEYS[1]) == ARGV[1]\n" +
"then\n" +
" return redis.call('del',KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end";
// 删除锁
redisTemplate.execute(new DefaultRedisScript<Long>(luaScript, Long.class),
Arrays.asList("lock"), uuid);
}
return dataFromDB;
} else {
// 加锁失败....重试
// 休眠100ms重试
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getCatalogJsonFromDbWithRedisLock();// 自旋的方式
}
}
官方文档:https://github.com/redisson/redisson/wiki
当我们在设计分布式锁的时候,我们应该考虑分布式锁至少要满足的一些条件,同时考虑如何高效的设计分布式锁,这里我认为以下几点是必须要考虑的。
在分布式高并发的条件下,我们最需要保证,同一时刻只能有一个线程获得锁,这是最基本的一点。
在分布式高并发的条件下,比如有个线程获得锁的同时,还没有来得及去释放锁,就因为系统故障或者其它原因使它无法执行释放锁的命令,导致其它线程都无法获得锁,造成死锁。
所以分布式非常有必要设置锁的有效时间,确保系统出现故障后,在一定时间内能够主动去释放锁,避免造成死锁的情况。
对于访问量大的共享资源,需要考虑减少锁等待的时间,避免导致大量线程阻塞。
所以在锁的设计时,需要考虑两点。
1、锁的颗粒度要尽量小。比如你要通过锁来减库存,那这个锁的名称你可以设置成是商品的ID,而不是任取名称。这样这个锁只对当前商品有效,锁的颗粒度小。
2、锁的范围尽量要小。比如只要锁2行代码就可以解决问题的,那就不要去锁10行代码了。
我们知道ReentrantLock是可重入锁,那它的特点就是:同一个线程可以重复拿到同一个资源的锁。重入锁非常有利于资源的高效利用。
<dependency>
<groupId>org.redissongroupId>
<artifactId>redissonartifactId>
<version>3.16.1version>
dependency>
@Configuration
public class MyRedissonConfig {
/**
* 所有对Redisson的使用都是通过RedissonClient对象
*
* @return
* @throws IOException
*/
@Bean(destroyMethod = "shutdown")
RedissonClient redisson(@Value("${spring.redis.host}") String host) throws IOException {
// 1、创建配置
Config config = new Config();
// 集群模式
// config.useClusterServers()
// .addNodeAddress("127.0.0.1:7004", "127.0.0.1:7001");
config.useSingleServer().setAddress("redis://" + host + ":6379");
// 2、根据config创建出RedissonClient实例
return Redisson.create(config);
}
}
结果死锁问题:
1.watch dog 在当前节点存活时每10s给分布式锁的key续期 30s;
2.watch dog 机制启动,且代码中没有释放锁操作时,watch dog 会不断的给锁续期;
3.如果程序释放锁操作时因为异常没有被执行,那么锁无法被释放,所以释放锁操作一定要放到 finally {} 中;
public Map<String, List<Test>> getJsonFromDbWithRedissonLock() {
// 1、锁的名字。锁的粒度:越细越快
RLock lock = redisson.getLock("json-lock");
lock.lock();
Map<String, List<Test>> dataFromDB = null;
try {
Thread.sleep(30000);
// 加锁成功....执行业务【内部会再判断一次redis是否有值】
dataFromDB = formDb();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 2、查询UUID是否是自己,是自己的lock就删除
// 查询+删除 必须是原子操作:lua脚本解锁
lock.unlock();
}
return dataFromDB;
}
保证一定能读到最新数据,修改期间,写锁是一个排他锁(互斥锁)。读锁是一个共享锁
读锁:共享锁
写锁:互斥,排他锁
写锁没释放读就必须等待
读读:相当于无锁,并发读,只会在redis中记录好,所有当前的读锁。他们都会同时加锁成功
写读:等待写锁释放
写写:阻塞方式
读写,有读锁。写也需要等待。
只要有写的存在,都必须等待
使用:
1、获取同一把锁
2、获得写锁:lock.writeLock()
3、获得读锁:lock.ReadLock()
/**
* 读锁
*
* @date: 2021/8/14 12:50
*/
@ResponseBody
@GetMapping(value = "readLock")
public String readRedsson() {
RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
RLock rLock = readWriteLock.readLock();
String s = "";
try {
System.out.println("读锁加锁成功~~~~~~~~~~~~~~~~~");
rLock.lock();
s = (String) redisTemplate.opsForValue().get("ReadWriteValue");
} catch (Exception e) {
} finally {
rLock.unlock();
System.out.println("读锁释放成功~~~~~~~~~~~~~~~~~~~");
}
return s;
}
/**
* 写锁
*
* @param:
* @return: void
* @date: 2021/8/14 12:54
*/
@ResponseBody
@GetMapping(value = "writeLock")
public String writeRedsson() {
RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
RLock rLock = readWriteLock.writeLock();
String s = "";
try {
System.out.println("写锁加锁成功~~~~~~~~~~~~~~~~~");
rLock.lock();
Thread.sleep(30000);
s = UUID.randomUUID().toString();
redisTemplate.opsForValue().set("ReadWriteValue", s);
} catch (Exception e) {
} finally {
rLock.unlock();
System.out.println("写锁释放成功~~~~~~~~~~~~~~~~~~~");
}
return s;
}
闭锁:
闭锁是计数的一个状态。
等到计数减完的时候,才能释放闭锁
/**
* 下班 锁门
* 1、下班没人了
* 5个部门全部走完,我们可以锁大门
*/
@ResponseBody
@GetMapping(value = "lockDoor")
public String lockDoor() throws InterruptedException {
RCountDownLatch door = redisson.getCountDownLatch("door");
door.trySetCount(5);
door.await();//等待闭锁都完成
return "下班了。。。。锁门";
}
@ResponseBody
@GetMapping(value = "gogogo/{id}")
public String gogogo(@PathVariable("id") Long id) {
RCountDownLatch door = redisson.getCountDownLatch("door");
door.countDown();//计数减一
return id + "部门的人都走完了....";
}
业务场景:
可以做分布式限流操作,秒杀
车库停车
redis放入park设置总量5
acquire 如果有车位大于1车位减1,如果没有车位一直阻塞等待
tryAcquire 如果有车位大于1车位减1,如果没有车位直接返回false
release 释放也就是在原有的值基础上加1
@ResponseBody
@GetMapping(value = "park")
public String park() throws InterruptedException {
//停车,车位减1
RSemaphore park = redisson.getSemaphore("park");
//阻塞等待
park.acquire();//获取一个信号,获取一个值,占一个车位
return "ok";
}
@ResponseBody
@GetMapping(value = "park1")
public String trypark() throws InterruptedException {
//停车,车位减1
RSemaphore park = redisson.getSemaphore("park");
//尝试获取,不成功直接返回
boolean b = park.tryAcquire();//获取一个信号,获取一个值,占一个车位
return "ok》》》》》" + b;
}
@ResponseBody
@GetMapping(value = "go")
public String go() throws InterruptedException {
//开走,车位加1
RSemaphore park = redisson.getSemaphore("park");
park.release();//释放一个车位
return "ok";
}
推荐文章
Spring Cloud Alibaba 系列学习笔记
SpringCloud Alibaba Nacos
SpringCloud Alibaba Sentinel
@SentinelResource注解总结,异常、降级兜底
SpringCloud Alibaba Sentine 规则持久化
SpringCloud Alibaba RocketMQ
Seata1.4.2分布式事务整合nacos+SpringCloudAlibaba觉得对您有帮助就留下个宝贵的吧!