理论系列-分布式锁
文章目录
一)介绍
一般环境下,为了实现并发控制,常使用synchronized
和ReentrantLock
。而在分布式环境下,本地锁无法满足需求,即需要分布式锁,实现多服务竞争锁时,只有一个能成功,在其完成前,其他服务只能等待。
二)特点
- 互斥性:只有一个服务获取锁
- 可重入性
- 锁超时:获取锁的服务出故障,能自动超时删除锁
- 高效和高可用
- 支持阻塞和非阻塞:
lock
和trylock
- 支持公平和非公平锁:按请求加锁的顺序获取锁或无序获取锁
三)常见实现方案
MySQL
Zookeeper
Redis
- 谷歌的
Chubby
四)了解
1. MySQL分布式锁
建表resourceLock
字段 | 类型 | 说明 |
---|---|---|
id | int(11) unsigned | 主键。非空&自增 |
resource_name | 字符串 | 资源名。非空&默认’’ |
node_info | 字符串 | 机器信息。默认空 |
count | int(11) | 锁次数。非空&默认’0’ |
阻塞lock
public void lock(resource) {
while (true) {
if (lock.lock(resource)) {
return;
}
// 休眠3秒
Thread.sleep(3*1000);
}
}
lock.lock(resource) {
queryResult = "SQL select * from resourceLock where resource_name="+resource;
currentNodeInfo = "...";//构造,如IP+线程
if (queryResult != null) {
// 已有服务占用资源
if (currentNodeInfo.equals(queryResult.node_info)) {
// 当前服务占用:可重入
"SQL count++";
return true;
} else {
// 其他服务占用
return false;
}
} else {
// 未被占用
"SQL insert into resourceLock ..."
return true;
}
}
非阻塞trylock
// 不加超时,立马返回结果
public boolean trylock(resource) {
return lock.lock();
}
// 加超时
public boolean trylock(resource, long timeout) {
long endTimeout = System.currentTimeMills + timeout;
while (true) {
if (lock.lock(resource)) {
return;
}
// 超时判断
if (System.currentTimeMills >= endTimeout) {
return false;
}
}
}
解锁unlock
1. 先查询`resource_name`,如果有值,则比较`node_info`
相同,上锁解锁同一个服务,可执行,count--
若count减1后等于0,删除节点
不相同,不允许执行unlock
2. 无值,代表该资源上无锁
lock.unlock(资源resource) {
queryResult = "SQL select * from resourceLock where resource_name="+resource;
currentNodeInfo = "...";//构造,如IP+线程
if (queryResult != null) {
// 当前资源有锁
if (currentNodeInfo.equals(queryResult.node_info)) {
// 解锁和上锁同一个服务
if (quertResult.count > 1) {
// 执行sql:count--
...
} else {
// 执行sql:删除该行
..
}
} else {
// 解锁和上锁服务不同
return false;
}
} else {
// 当前资源无锁
return false;
}
}
锁超时
开启一个定时任务,计算处理时间,取一个大于该时间的定时检测(如5倍),若锁仍未释放,认为上锁服务挂了,直接释放锁。
或者另开一个服务,专门用户超时访问。每隔5*平均处理时间,前后两次访问,服务为同一个,可认为上锁服务挂了,直接释放锁。
总结说明
说明:node_info可使用机器IP和线程
lock.lock的所有操作,应该是原子性,需加事务
lock.unlock也要加事务
lock.lock(resource)
内部是一个sql。
1. 先查询`resource_name`,如果有值,则比较`node_info`
相同,可重入锁,`count++`
不相同,休眠,等待下次请求
2. 如果无值,插入新的数据,占用锁
lock.unlock(resource)
1. 先查询`resource_name`,如果有值,则比较`node_info`
相同,上锁解锁同一个服务,可执行,count--
若count减1后等于0,删除节点
不相同,不允许执行unlock
2. 无值,代表该资源上无锁
适用场景
优点
理解简单
缺点
实现繁琐,包括:超时、事务等。
性能局限于数据库,并发高时不适用
乐观锁如何实现?
待定
2 Zookeeper分布式锁
基础:Paxos算法
实现原理
/lock加锁目录
/lock/resource_name/临时有序节点(服务名):值为重入次数
使用
InterProcessMutex lock = new InterProcessMutex(client, lock_path);
try {
lock.acquire();
// 业务
} finally {
lock.release();
}
3 Redis分布式锁
实现原理
set, nx, 上锁和解锁同一个服务
1. k-v不存在才加锁
问题1:超时
2. ex+Lua脚本
问题2:业务时间 > 超时时间
Redission
集群主挂
解决
集群都执行setnx,超半数认为成功
解锁,集群全部请求setnx
逐步深入
1. setnx,key是唯一标识,value为(服务,加锁次数)
- 成功:"OK"
- 失败:null
2. del
问题一:锁超时。
获取锁的服务,挂了,没释放锁
需要一个超时时间,自动释放锁:expire
问题二:并发环境,非原子操作不安全,如setnx和expire分开执行,甚至setnx后挂了。
Lua脚本
问题三:业务时间 > 超时时间。获取锁的服务还没执行完,锁被释放了
上锁和解锁为同一个服务:Lua实现
快过期,业务未执行完:续期。获取锁的线程开启一个守护线程
定时检测(小于超时).检测到锁快超时,但服务仍在使用,续时
分布式锁的问题
GC
服务A,获取锁
服务A,执行GC(stop-the-world),…,结束(锁释放,服务B获取锁)
服务A恢复,锁
结果:A和B同时持有锁
时钟跳跃
长时间业务处理
如IO等
Chubby分布式锁
待定
参考
https://juejin.im/post/5bbb0d8df265da0abd3533a5
https://juejin.im/post/5b16148a518825136137c8db