分布式锁-Redis
一、缓存常见问题
对于访问频率高、读多写少、一致性要求不高的数据适合做缓存。
1.1 缓存穿透
缓存穿透:缓存和数据库中都没有的数据,不断发起请求,如发起为id为-1的数据或id为特别大不存在的数据。
解决方案:
- 接口层增加校验,校验用户权限 & 对不合理的数据进行过滤
- 使用布隆过滤器
- 把查询结果为空的数据也进行缓存,设置较短的过期时间
1.2 缓存击穿
热点数据:缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来。
解决方案:
- 设置热点数据永不过期
- 二级缓存
- 互斥锁
互斥锁示例代码如下所示:
static Lock reenLock = new ReentrantLock();
public List<String> getData04() throws InterruptedException {
List<String> result = new ArrayList<String>();
// 从缓存读取数据
result = getDataFromCache();
if (result.isEmpty()) {
if (reenLock.tryLock()) {
try {
System.out.println("我拿到锁了,从DB获取数据库后写入缓存");
// 从数据库查询数据
result = getDataFromDB();
// 将查询到的数据写入缓存
setDataToCache(result);
} finally {
reenLock.unlock();// 释放锁
}
} else {
result = getDataFromCache();// 先查一下缓存
if (result.isEmpty()) {
System.out.println("我没拿到锁,缓存也没数据,先小憩一下");
Thread.sleep(100);// 小憩一会儿
return getData04();// 重试
}
}
}
return result;
}
1.3 缓存雪崩
缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是并发查多条数据。
解决方案:
- 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生
- 二级缓存
二、Redis实现分布式锁
2.1 锁超时
线程一获取到锁,但是未执行expire设置过期时间(setnx不支持设置过期时间)就Down掉了,此时就变成了死锁。
if(setnx(lock_sale_商品ID,1) == 1){
expire(lock_sale_商品ID,30)
try {
do something ......
} finally {
del(lock_sale_商品ID)
}
}
使用set 指令的可选参数,设置过期时间:
set(lock_sale_商品ID,1,30,NX)
2.2 锁误删
由于线程一执行过慢,锁自动释放了,线程二获取到锁,此时线程一执行完毕删除锁,造成锁误删。
解决方法:在 del 释放锁之前做一个判断,验证当前的锁是不是自己加的锁,将当前的线程 ID 当做 value。
//加锁
String threadId = Thread.currentThread().getId()
set(key,threadId ,30,NX)
//解锁
if(threadId .equals(redisClient.get(key))){
del(key)
}
存在问题:判断和释放锁是两个独立操作,不是原子性。
解决方法:使用lua脚本执行保证原子性。
优化:让获得锁的线程开启一个守护线程,用来给快要过期的锁续航。
三、Redission实现分布式锁
加锁原则:
- 锁的颗粒度要尽量小
- 锁的范围尽量要小
Redisson保证value的唯一性:UUID+ThreadID。
Redission分布式锁原理如下:
存在问题:master节点宕机,导致多个客户端对同一个分布式锁完成了加锁。
解决方法:使用RedLock(Redis Distributed Lock)。
三、RedLock实现分布式锁
算法思想:在多个redis实例上创建锁,必须在大多数redis节点(超过一半)上都成功创建锁,才能算这个整体的RedLock加锁成功。
CAP定理:在一个分布式系统中,Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可兼得。
分布式锁实现Demo:https://github.com/just-right/vote/tree/distributed