目录
一、锁,你了解多少?
二、设计分布式锁应该考虑的东西
三、可重入锁你知道吗?
四、分布式锁的选型实现
五、Redis实现分布式锁的坑你发现了吗
一、锁,你了解多少?
有本地锁:synchronize、lock等,锁在当前进程内,分布式集群部署下依旧存在锁失效问题
还有分布式锁:redis、zookeeper等实现,虽然还是锁,但是多个进程共用的锁标记,可以用Redis、Zookeeper、Mysql等都可以
二、设计分布式锁应该考虑的东西
排他性
在分布式应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行
容错性
分布式锁一定能得到释放,比如客户端奔溃或者网络中断
满足可重入、高性能、高可用
注意分布式锁的开销、锁粒度
三、可重入锁你知道吗?
单节点可重入锁
可重入锁: JDK指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的,synchronized 和 ReentrantLock 都是可重入锁
分布式下的可重入锁
进程单位,当一个线程获取对象锁之后,其他节点的同个业务线程可以再次获取本对象上的锁
四、分布式锁的选型实现
实现分布式锁 可以用 Redis、Zookeeper、Mysql数据库这几种 , 性能最好的是Redis且是最容易理解
分布式锁离不开 key - value 设置
key 是锁的唯一标识,一般按业务来决定命名,比如想要给一种商品的秒杀活动加锁,key 命名为 “seckill_商品ID” 。value就可以使用固定值,比如设置成1。
短链码可以:short_link:code:xxxx
基于redis实现分布式锁,文档:http://www.redis.cn/commands.html#string
加锁 SETNX key value
setnx 的含义就是 SET if Not Exists,有两个参数 setnx(key, value),该方法是原子性操作
如果 key 不存在,则设置当前 key 成功,返回 1;
如果当前 key 已经存在,则设置当前 key 失败,返回 0
解锁 del (key)
得到锁的线程执行完任务,需要释放锁,以便其他线程可以进入,调用 del(key)配置锁超时 expire (key,30s)
客户端奔溃或者网络中断,资源将会永远被锁住,即死锁,因此需要给key配置过期时间,以保证即使没有被显式释放,这把锁也要在一定时间后自动释放
综合伪代码
methodA(){
String key = "short_link:code:abcdef"
if(setnx(key,1) == 1){
expire(key,30,TimeUnit.MILLISECONDS)
try {
//做对应的业务逻辑
} finally {
del(key)
}
}else{
//睡眠100毫秒,然后自旋调用本方法
methodA()
}
}
五、Redis实现分布式锁的坑你发现了吗
上述伪代码最大的问题:多个命令之间不是原子性操作,如setnx和expire之间,如果setnx成功,但是expire失败,且宕机了,则这个资源就是死锁
可以使用原子命令解决:设置和配置过期时间 setnx / setex
如: set key 1 ex 30 nx
java代码里面String key = "short_link:code:abcdef"
redisTemplate.opsForValue().setIfAbsent(key,1,30,TimeUnit.MILLISECONDS)
但是实际业务应用情况还有更多问题:
业务超时,如何避免其他线程勿删
业务执行时间过长,如何实现锁的自动续期
分布式锁的核心:
是保证多个指令原子性,加锁使用setnx setex 可以保证原子性,那解锁使用判断和设置等怎么保证原子性
多个命令的原子性:采用 lua脚本+redis, 由于【判断和删除】是lua脚本执行,所以要么全成功,要么全失败
流程:
* 先判断是否有,如没这个key,则设置key-value,配置过期时间,加锁成功
* 如果有这个key,判断value是否是同个账号,是同个账号则返回加锁成功
* 如果不是同个账号则加锁失败
代码实现:
//key1是短链码,ARGV[1]是accountNo,ARGV[2]是过期时间
String script = "if redis.call('EXISTS',KEYS[1])==0 then redis.call('set',KEYS[1],ARGV[1]); redis.call('expire',KEYS[1],ARGV[2]); return 1;" +
" elseif redis.call('get',KEYS[1]) == ARGV[1] then return 2;" +
" else return 0; end;";
Long result = redisTemplate.execute(new
DefaultRedisScript<>(script, Long.class), Arrays.asList(code), value,100);
演示效果:同一个code下,不同账号进行加锁。同一个账号则不加锁。
code=aabbcc进入方法,账号是123,加锁成功,返回1
code=aabbcc进入方法,账号是123,加锁成功,返回2
这样就实现了同一个code,同一个账号的可重入锁
code=aabbcc进入方法,账号是567,加锁失败
实际业务代码中,可result 是否大于0来判断加锁失败和成功,然后走对应的业务逻辑即可。