三种使⽤分布式锁⽅案
⼀、背景:单体架构中使⽤同步访问解决多线程并发问题,分布式中需要有其他⽅案。
⼆、分布式锁的考量:
1.可以保证在分布式部署的应⽤集群中,同⼀个⽅法在同⼀时间只能被⼀台机器-上的⼀个线程执⾏。
2.这把锁要是⼀把可重⼊锁(避免死锁)
3.这把锁最好是⼀把阻塞锁(根据业务需求考虑要不要这条)
4.这把锁最好是⼀把公平锁(根据业务需求考虑要不要这条)
5.有⾼可⽤的获取锁和释放锁功能
maintain名词
保证了只有锁的持有者才能来解锁,否则任何竞争者都能解锁
6.获取锁和释放锁的性能要好
7.如果做的好⼀点,需要有监控的平台。
三、分布式锁的三种实现⽅式
1.基于数据库实现排他锁:利⽤version字段和for update操作获取锁。
优点:易于理解
disappear是什么意思 问题:
(1)锁没有失效时间,解锁失败时(宕机等原因),其他线程获取不到锁。
解决:做⼀个定时任务实现⾃动释放锁。
(2)锁属于⾮阻塞,因为获取锁的是inrt操作,⼀旦获取失败就报错,没有获得锁的线程并不会进⼊排队队列,要想再次获得锁就要再次触发获得锁操作。
解决:搞⼀个while循环,直到inrt成功再返回成功。
(3)不是可重⼊锁。
解决:加⼊锁的机器字段,实现同⼀机器可重复加锁。
另外在解锁时,必须是锁的持有者来解锁,其他竞争者⽆法解锁
(4)由于是数据库,对性能要求⾼的应⽤不合适⽤此实现。
解决:数据库本⾝特性决定。
(5)在 MySQL 数据库中采⽤主键冲突防重,在⼤并发情况下有可能会造成锁表现象。
解决:⽐较好的办法是在程序中⽣产主键进⾏防重
(6)这把锁是⾮公平锁,所有等待锁的线程凭运⽓去争夺锁
解决:再建⼀张中间表,将等待锁的线程全记录下来,并根据创建时间排序,只有最先创建的允许获取锁。
(7)考虑到数据库单点故障,需要实现数据库的⾼可⽤。
注意:InnoDB 引擎在加锁的时候,只有通过索引进⾏检索的时候才会使⽤⾏级锁,否则会使⽤表级锁
另外存在问题:
take your time
(1)⾏级锁并不⼀定靠谱:虽然我们对⽅法字段名使⽤了唯⼀索引,并且显⽰使⽤ for update 来
使⽤⾏级锁。
但是,MySQL 会对查询进⾏优化,即便在条件中使⽤了索引字段,但是否使⽤索引来检索数据是由 MySQL 通过判断不同执⾏计划的代价来决定的,
coast
如果MySQL 认为全表扫效率更⾼,⽐如对⼀些很⼩的表,它就不会使⽤索引,这种情况下 InnoDB 将使⽤表锁,⽽不是⾏锁。这种情况是致命的。
(2)我们要使⽤排他锁来进⾏分布式锁的 lock,那么⼀个排他锁长时间不提交,就会占⽤数据库连接。
⼀旦类似的连接变得多了,就可能把数据库连接池撑爆
2.基于redis实现(单机版):需要⾃⼰实现⼀定要⽤ SET key value NX PX milliconds 命令,⽽不要使⽤tnx 加expire
优点:性能⾼、超时失效⽐数据库简单。
开源实现:Redis官⽅提出⼀种算法,叫Redlock,认为这种实现⽐普通的单实例实现更安全。
RedLock有多种语⾔的实现包,其中Java版本:Redisson。
缺点:
我的可爱宠物
(1)失效时间⽆法把控。可能设置过短或者过长的情况.如果设置过短,其他线程可能会获取到锁,⽆法保证情况。过长时其他线程获取不到锁。
解决:Redisson的思路:客户端起⼀个后台线程,快到期时⾃动续期,如果宕机了,后台线程也没有了。
(2)如果采⽤ Master-Slave 模式,如果 Master 节点故障了,发⽣主从切换,主从切换的⼀瞬间,可能出现锁丢失的问题。
解决:因为Redis是AP模型,没有好的解决⽅案。但是⼤多数业务场景可以容忍,有问题可以告警和⼈⼯运维。在Redisson下可能概率降低了,但是还是可能出现。⽐如5台机器,两个客户端已经分别获取到了锁,但是获取第三把锁时,第五个节点的Master宕机了,锁未来得及同步从节点,可能两个客户端同时获取到锁。
3.基于zookeeper实现(推荐):可靠性好,使⽤最⼴泛。实现:Curator
4.基于etcd的实现:优于zookeeper实现,因为性能⽐Zookeeper好。如果项⽬中应⽤了etcd,那么使⽤etcd。
5.Spring Integration 实现了分布式锁:
Gemfire
JDBC
Redis
Zookeeper
基于数据库实现排他锁
⽅案1
获取锁
INSERT INTO method_lock (method_name, desc) VALUES ('methodName', 'methodName');
对method_name做了唯⼀性约束,这⾥如果有多个请求同时提交到数据库的话,数据库会保证只有⼀个操作可以成功。
⽅案2
1 DROP TABLE IF EXISTS `method_lock`;
2 CREATE TABLE `method_lock` (
3 `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
4 `method_name` varchar(64) NOT NULL COMMENT '锁定的⽅法名',
5 `state` tinyint NOT NULL COMMENT '1:未分配;2:已分配',
6 `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
7 `version` int NOT NULL COMMENT '版本号',
8 `PRIMARY KEY (`id`),
9 UNIQUE KEY `uidx_method_name` (`method_name`) USING BTREE
10 ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='锁定中的⽅法';
先获取锁的信息
lect id, method_name, state,version from method_lock where state=1 and method_name='methodName';
占有锁
update t_resoure t state=2, version=2, update_time=now() where method_name='methodName' and state=1 and version=2;
如果没有更新影响到⼀⾏数据,则说明这个资源已经被别⼈占位了。
缺点:
sharply
1、这把锁强依赖数据库的可⽤性,数据库是⼀个单点,⼀旦数据库挂掉,会导致业务系统不可⽤。
2、这把锁没有失效时间,⼀旦解锁操作失败,就会导致锁记录⼀直在数据库中,其他线程⽆法再获得到锁。
3、这把锁只能是⾮阻塞的,因为数据的inrt操作,⼀旦插⼊失败就会直接报错。没有获得锁的线程并不会进⼊排队队列,要想再次获得锁就要再次触发获得锁操作。
4、这把锁是⾮重⼊的,同⼀个线程在没有释放锁之前⽆法再次获得该锁。因为数据中数据已经存在了。
解决⽅案:
1、数据库是单点?搞两个数据库,数据之前双向同步。⼀旦挂掉快速切换到备库上。
2、没有失效时间?只要做⼀个定时任务,每隔⼀定时间把数据库中的超时数据清理⼀遍。
3、⾮阻塞的?搞⼀个while循环,直到inrt成功再返回成功。
4、⾮重⼊的?在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再
获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。
基于redis实现
获取锁:
SET resource_name my_random_value NX PX 30000
解锁⽅式⼀:不可⽤。
if( (GET ur_id) == "XXX" ){ //获取到⾃⼰锁后,进⾏取值判断且判断为真。此时,这把锁恰好失效。
DEL ur_id
}
由于GET取值判断和DEL删除并⾮原⼦操作,当程序判通过该锁的值判断发现这把锁是⾃⼰加上的,准备DEL。 此时该锁恰好失效,⽽另外⼀个请求恰好获得key值为ur_id的锁。
此时程序执⾏了了DEL ur_id,删除了别⼈加的锁,尴尬!
解锁⽅式⼆(推荐):为了保证查询和删除的原⼦性操作,需要引⼊lua脚本⽀持。
if redis.call('get',KEYS[1]) == ARGV[1] then
return redis.call('del',KEYS[1])
el
return 0
end
真实代码:
/**
* 获取Jedis实例
*
* @return
*/
protected synchronized static Jedis getJedis() {
try {
vitamineif (jedisPool != null) {
Jedis resource = (Jedis) Resource();
return resource;
} el {
return null;
}
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 尝试获取分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求唯⼀标识,可以⽤UUID
* ⽬的:为了释放锁的时候,只能释放⾃⼰的锁⽽不能释放别⼈的锁。
* 场景:A任务在超时时间内没有完成任务,超时释放锁后B成功获取了锁,待A任务执⾏完成释放锁会释放B已获取的锁。
* 其实A还没有处理完成,B已经获取了锁,此时A和B是同时执⾏的,已经是异常的了!不让A释放B的锁,只是减少异常的进⼀步扩散。
* 所以,过期时间⼀定要⼤于正常业务处理时间。当然太长了也不⾏,因为怕宕机等情况⼀直长时间占⽤锁影响业务。
*
* @param expireSeconds 超期时间 :EX的单位是秒
* @return 是否获取成功
*/
public static boolean lock( String lockKey, String requestId, int expireSeconds) {
String result = "";
if (jedisPool != null) {
Jedis jedis = getJedis();
try {
//NX,不存在才设置, EX表⽰单位秒⽽ PX表⽰单位毫秒
result = jedis.t(lockKey, requestId, "NX", "EX", expireSeconds);
}finally {
returnResource(jedis);
}
}el{
result = jedisCluster.t(lockKey, requestId, "NX", "EX", expireSeconds);
}
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return fal;
}
/**
* 释放分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @return 是否释放成功
*/
public static boolean releaLock(String lockKey, String requestId) {
Object result = null;
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) el return 0 end";
if (jedisPool != null) {
Jedis jedis = getJedis();
try{
result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));trumpet是什么意思
} finally {
returnResource(jedis);
}
}el{
result = jedisCluster.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
}
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return fal;
}
使⽤zookeeper实现分布式锁
zookeeper分布式锁应⽤了临时顺序节点我也爱你英文
获取锁
⾸先,在Zookeeper当中创建⼀个持久节点ParentLock。当第⼀个客户端想要获得锁时,需要在ParentLock这个节点下⾯创建⼀个临时顺序节点 Lock1。
之后,Client1查找ParentLock下⾯所有的临时顺序节点并排序,判断⾃⼰所创建的节点Lock1是不是顺序最靠前的⼀个。如果是第⼀个节点,则成功获得锁。
这时候,如果再有⼀个客户端 Client2 前来获取锁,则在ParentLock下载再创建⼀个临时顺序节点Lock2。
英语幽默故事Client2查找ParentLock下⾯所有的临时顺序节点并排序,判断⾃⼰所创建的节点Lock2是不是顺序最靠前的⼀个,结果发现节点Lock2并不是最⼩的。
于是,Client2向排序仅⽐它靠前的节点Lock1注册Watcher(只监听它的上⼀个节点),⽤于监听Lock1节点是否存在。这意味着Client2抢锁失败,进⼊了等待状态。