目录标题
- 思路
-
- [🔺 如何加锁、解锁](#🔺 如何加锁、解锁)
- [🔺 如果获取锁当前失败了,如何进行重试](#🔺 如果获取锁当前失败了,如何进行重试)
- [🔺 加锁、解锁:如何考虑锁的重入问题](#🔺 加锁、解锁:如何考虑锁的重入问题)
- [🔺 加锁、解锁的唯一性:防止误删除、独占排他性](#🔺 加锁、解锁的唯一性:防止误删除、独占排他性)
- [🔺 服务器宕机:Redis 宕机](#🔺 服务器宕机:Redis 宕机)
- [🔺 锁的自动续期:程序操作时间比加锁的时候长](#🔺 锁的自动续期:程序操作时间比加锁的时候长)
- [🔺 保证高可用:在集群模式下,会导致锁机制失效](#🔺 保证高可用:在集群模式下,会导致锁机制失效)
- 优化
- 代码实现
- 总结
思路
🔺 如何加锁、解锁
使用基本的命令 SETNX 以及 DEL 指令
🔺 如果获取锁当前失败了,如何进行重试
自旋重试
🔺 加锁、解锁:如何考虑锁的重入问题
利用 HINCRBY 的命令 +1 / -1
🔺 加锁、解锁的唯一性:防止误删除、独占排他性
加锁、解锁的时候,利用给每个锁加个唯一的标识来实现
🔺 服务器宕机:Redis 宕机
获取锁成功以后宕机,也就是在Redis里面存在了一条 k-v 键值对的锁了
加锁的时候,添加上过期时间
🔺 锁的自动续期:程序操作时间比加锁的时候长
先 HEXIST 判断有没有锁存在,如果存在,则使用定时任务进行自动续期
🔺 保证高可用:在集群模式下,会导致锁机制失效
优化
在上面可以得出,我们会有一些逻辑性的判断,再加上 Redis 本身是不支持事务的,所以我们就需要有一个Lua 脚本来辅助我们进行实现。
java
加锁:setnx key value:1. 独占排它锁
解锁:del key
重试:递归 or 循环
过期时间:expire:
2. 避免发生死锁(Redis客户端从Redis服务中获取到锁以后立刻宕机 / 锁重入)
3. 原子性:加锁与过期时间、判断锁与释放锁
4. 防止误删除 (加锁与解锁必须的原子性,即谁加锁就是谁解锁 + 先判断在删除)
5. 自动续期 (保证程序的运行时间与锁的存活时间保持一致)
6. 在集群模式下,会导致锁机制失效
1. 客户端10010,从主中获取到锁
2. 由于主还没来得及同步从数据,主宕机,从升级为主
3. 客户端10086,从新主中获取到锁
==========================(进一步优化)==================================
解决1,2,(宕机)3 问题:
加锁:set key value [ex seconds] [ px milliseconds] [nx | xx]
设 k - v,[过期时间 seconds 秒] [过期时间 milliseconds 毫秒] [只在键不存在 | 只在键存在, 才对键进行设置操作]
------------------------------------------------
解决2(锁重入):hash + lua脚本
hset lockName ownerLockName ownNum
加锁:
1. 判断锁是否存在且无人拥有? EXISTS key [key ...]
是:获取锁 HSET key field value
判断是不是自己的锁 HEXISTS key field
是:重入 HINCRBY key field increment
否:重试 递归或者循环
否:重试 递归或者循环
# lua脚本1.0:基本实现逻辑
if redis.call('EXISTS', 'lock') == 0
then
// redis.call('HSET', lock , uuid, 1) 等价于
redis.call('HINCRBY', 'lock', uuid ,1)
redis.call('expire', lock, 30)
return 1
elseif redis.call('HEXISTS', 'lock', uuid) == 1
then
redis.call('HINCRBY', 'lock', uuid ,1)
redis.call('expire', lock, 30)
return 1
else
return 0
end
# lua脚本2.0:进一步优化
if redis.call('EXISTS', 'lock') == 0 or redis.call('HEXISTS', 'lock', uuid) == 1
then
redis.call('HINCRBY', 'lock', uuid ,1)
redis.call('expire', lock, 30)
return 1
else
return 0
end
# lua脚本3.0:将形参放到脚本之中
if redis.call('EXISTS', KEYS[1]) == 0 or redis.call('HEXISTS', KEYS[1], ARGV[1]) == 1
then
redis.call('HINCRBY', KEYS[1], ARGV[1] ,1)
redis.call('expire', KEYS[1], ARGV[2])
return 1
else
return 0
end
key: lock
arg: uuid 30
# lua脚本4.0:转化为一行
if redis.call('EXISTS', KEYS[1]) == 0 or redis.call('HEXISTS',KEYS[1], ARGV[1]) == 1 then redis.call('HINCRBY', KEYS[1], ARGV[1] ,1) redis.call('expire', KEYS[1], ARGV[2]) return 1 else return 0 end
解锁:
1. 判断锁是否存在,且是否为自己的锁,不存在则返回 恶意释放锁nil;
2. 如果自己的锁存在,则 减1,判断减1 后的值是否为0,为0则释放锁,返回 全部解锁成功1;
3. 不为0,则代表已经对锁释放了一次,返回 释放锁一次执行成功0
# lua脚本1.0:基本实现
if redis.call('HEXISTS', KEYS[1], ARGV[1]) == 0
then
return nil
elseif redis.call('HINCRBY', KEYS[1], ARGV[1] , -1) == 0
then
return redis.call('del', KEYS[1])
else
return 0
end
key: lock
arg: uuid
# lua脚本2.0:基本实现
if redis.call('HEXISTS', KEYS[1], ARGV[1]) == 0 then return nil elseif redis.call('HINCRBY', KEYS[1], ARGV[1] , -1) == 0 then return redis.call('del', KEYS[1]) else return 0 end
-------------------------------------------
解决4问题:防止误删除 :给每个锁加个唯一的标识 + 解锁用lua脚本
127.0.0.1:6379> eval "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" 1 lock 2
(integer) 0
-------------------------------------------
解决5问题:定时任务(Timer定时器) + lua 脚本
判断自己的锁是否存在(hexist),如果存在则重置过期时间
if redis.call('HEXISTS', KEYS[1], ARGV[1]) == 1
then
return redis.call('expire',KEYS[1],ARGV[2])
else
return 0
end
key: lock
arg: uuid 30
if redis.call('HEXISTS', KEYS[1], ARGV[1]) == 1 then return redis.call('expire',KEYS[1],ARGV[2]) else return 0 end
6. 解决6 (在集群模式下,会导致锁机制失效 )
红锁算法:多个Redis,相互独立,没有主从关系
1. 应用程序获取到系统当前时间T1
2. 应用程序使用相同的 k-v 一次从多个Redis实例中获取到锁,其中获取锁的尝试时间需要设置,以便于尽快访问下一个结点
3. 计算 获取锁的消耗时间CT1 = 应用用程序获取到系统当前时间T2 - T1
成功获取到锁:获取锁的消耗时间CT1 < 应用程序总的锁定时间 ,且 半数以上的结点获取锁成功
4. 计算剩余锁定时间 = 应用程序总的锁定时间 - 获取锁的消耗时间CT1
5. 如果获取锁失败了,对所有的Redis节点释放锁
代码实现
java
public class RedisLockUtil implements Lock {
private StringRedisTemplate redisTemplate;
private String lockName;
private String uuid;
// 秒为单位
private long expire = 30;
public RedisLockUtil(StringRedisTemplate redisTemplate, String lockName, String uuid) {
this.redisTemplate = redisTemplate;
this.lockName = lockName;
this.uuid = uuid + ":" + Thread.currentThread().getId();
}
@Override
public void lock() {
this.tryLock();
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
try {
return this.tryLock(-1L, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
return false;
}
/**
* 加锁
*
* @param time
* @param unit
* @return boolean
*/
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
if (time != -1) {
this.expire = unit.toSeconds(time);
}
String script = "if redis.call('EXISTS', KEYS[1]) == 0 or redis.call('HEXISTS',KEYS[1], ARGV[1]) == 1 " +
"then " +
"redis.call('HINCRBY', KEYS[1], ARGV[1] ,1) " +
"redis.call('expire', KEYS[1], ARGV[2]) " +
"return 1 " +
"else " +
"return 0 " +
"end";
// 如果并没有抢占到锁,则稍等一会,在去抢锁
while (!this.redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuid, String.valueOf(expire))) {
try {
TimeUnit.MILLISECONDS.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 加锁成功,只有等到锁删除成功,定时任务才不会执行,否则开启定时任务自动续期
this.reNewExpire();
return true;
}
/**
* 解锁
*/
@Override
public void unlock() {
String script = "if redis.call('HEXISTS', KEYS[1], ARGV[1]) == 0 " +
"then " +
"return nil " +
"elseif redis.call('HINCRBY', KEYS[1], ARGV[1] , -1) == 0 " +
"then " +
"return redis.call('del', KEYS[1]) " +
"else " +
"return 0 " +
"end ";
Long flag = this.redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuid);
if (flag == null) {
throw new IllegalMonitorStateException();
}
}
@Override
public Condition newCondition() {
return null;
}
/**
* 锁的自动续期
*/
private void reNewExpire() {
String script = "if redis.call('HEXISTS', KEYS[1], ARGV[1]) == 1 " +
"then " +
"return redis.call('expire',KEYS[1],ARGV[2]) " +
"else " +
"return 0 " +
"end";
// 执行一次定时任务,定时任务的延期时间为 过期时间的1/3
new Timer().schedule(new TimerTask() {
@Override
public void run() {
// 如果续期成功,则准备是否再次续期
if (redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuid, String.valueOf(expire))) {
reNewExpire();
}
}
// 由于delay 是以毫秒为单位的,所以得乘以 1000 转化为 秒
}, this.expire * 1000 / 3);
}
}
java
@Component
public class LockClient {
@Autowired
private StringRedisTemplate redisTemplate;
private String uuid;
public LockClient(){
this.uuid = UUID.randomUUID().toString();
}
public RedisLockUtil getLockClient(String lockName) {
return new RedisLockUtil(redisTemplate, lockName, uuid);
}
}
java
@Service
public class DbStockService {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private LockClient lockClient;
/**
* Redis 乐观锁
*/
public void decrementStock() {
RedisLockUtil lockClient = this.lockClient.getLockClient("lock");
lockClient.lock();
try {
String stock = redisTemplate.opsForValue().get("stock");
Integer integer = Integer.valueOf(stock);
if (integer > 0) {
redisTemplate.opsForValue().set("stock", String.valueOf(integer - 1));
}
// 测试锁的可重入性
// test();
// 测试锁的自动续期
// try { TimeUnit.SECONDS.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); }
} finally {
lockClient.unlock();
}
}
/**
* 测试锁的可重入性
*/
public void test() {
RedisLockUtil lockClient = this.lockClient.getLockClient("lock");
lockClient.lock();
lockClient.unlock();
}
}
总结
加锁:
- setnx:独占排他 死锁、不可重入、原子性
- set kv ex 30nx:独占排他、死锁不可重入
- hash +lua脚本:可重入锁
- 判断锁是否被占用(exists),如果没有被占用则直接获取锁(hset/hincrby) 并设置过期时间(expire)
- 如果锁被占用,则判断是否当前线程占用的(hexists),如果是则重入(hincrby) 并重置过期时间(expire)
- 3.否则获取锁失败,将来代码中重试
- 重试:递归 循环
- Timer定时器+lua脚本:实现锁的自动续期
判断锁是否自己的锁(hexists),如果是自己的锁则执行(expire)设置过期时间
解锁:
- del:导致误删
- 先判断再删除同时保证原子性: lua脚本
- hash +lua脚本:可重入 T
- 判断当前线程的锁是否存在,不存在则返回nil,将来在代码中抛出异常
- 存在则直接减1 (hincrby -1),判断减1后的值是否为0,为0则释放锁(del),并返回1
- 不为0,则返回0