分布式锁实现的几种方式
写在前面:本篇文章主要搬自本人的云笔记,不当之处,欢迎指正,共同学习;
1、数据库锁的方式
1.1、基于表记录的实现方式
准备条件:创建表ABC,并做唯一索引version;
//1、线程A加锁:
insert into ABC(version)values (versionID);
//2、线程A释放锁:
delete from ABC where version =versionID;
如果数据库中有该锁记录,线程B再保存数据进行加锁就会发生唯一索引异常,这时就成功实现了分布式锁;
缺点:只适用于并发量不大的业务场景,这里xxl-job的管理后台上使用了这种方式;
容易发生死锁,但是可以起一个定时任务去清理它;
1.2、基于乐观锁的实现方式
乐观锁主要根据加版本号的方式实现,原来如下:A、B线程执行更新操作之前先查询版本号,然后根据版本号进行更新,如果版本号被线程A更新了,
这时线程B再用该版本号执行更新就会更新失败,从而成功实现加锁;这里需要注意此种锁为不可重入锁,只能重新发起业务请求;
//1、查询出数据库中的版本号
select ID,version from ABC;//获取到versionID
//2、对该条记录进行更新
update ABC set business = business-1 , version = versionID+1 where version=versionID and id =ID;
这时如果线程B再用原来的versionID进行更新,就失败了;
缺点:需要维护版本字段,造成数据库表结构的冗余;
只适用于并发不高,写操作少的情况;
1.3、基于悲观锁的实现方式
//1、由于MySQL是默认自动事务提交的,这里需要先关闭事务自动提交
SET AUTOCOMMIT = 0;(有没人知道使用spring事务的时候,怎么不关闭自动提交呢?)
//2、对于InnoDB的数据表来说,支持表级锁和行级锁,当查询时不走索引的时候加的就是表级锁;对于MYISAM(不支持事务)的数据表,只支持表级锁;
select * from ABC where a.version =versionID for update;//行级锁
select * from ABC where a.version >versionID for update;//表级锁
select * from ABC where a.business =businessID for update;//表级锁
//3、读锁属于共享锁,A线程对该条记录上共享锁后,B线程也可以上共享锁,但不能上排它锁;
写锁属于排它锁,A线程对该条记录上排它锁后,B线程不能再上任何锁;
//4、这里多补充一点Oracel中有for update nowait语法,使用该语法不会像for update当该行被锁定时一直等待,而是直接报错;
MySQL中可以是通过配置 innodb_lock_wait_timeout的时间指定该查询会话等待的时间,超时就直接报错
2、基于Redis实现分布式锁
优点:redis是单线程执行的,所以它天然的支持分布式锁
缺点:超时时间的设置不当,会对服务性能有较大影响(我们的项目设置的8s)
实现代码如下:
//1、加锁基于set key value [EX seconds|PX milliseconds] [nx|xx];
EX 表示超时时间精度是秒
PX 表示超时时间精度是毫秒
NX 表示只有当该key 不存在时才保存
xx 表示只有当该key存在时才覆盖
public class RedisTool {
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
/**
* 尝试获取分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @param expireTime 超期时间
* @return 是否获取成功
*/
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
2、解锁使用Lua脚本,保证操作的原子性
public class RedisTool {
private static final Long RELEASE_SUCCESS = 1L;
/**
* 释放分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @return 是否释放成功
*/
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
3、基于zookeeper实现分布式锁
3.1、排他锁的原理,主要应用zk上到临时节点
1、多个client共同去zk上预定义好的/lock节点下创建一个相同的临时节点/abc;
2、哪个节点创建成功之后,就说明该会话拿到了锁;
3、其他创建失败的client,继续保持对该节点的监听;
4、当之前拿到锁的client会话结束或者主动释放了锁,这时其他的client再次去/lock下创建/abc,以便获取锁
缺点:每一次只有一个client注册成功,其他的client都会失败,然后触发监听时还要通知每一个client,再次去竞争锁,这里如果请求量大的话,会引发羊群效应,这里可以尝试使用共享锁
3.2、共享锁的原理,主要应用zk上的临时有序节点
1、当多个请求到来时,同样去zk上预定义好的/lock节点下创建节点,但是是临时有序节点/abc0000001,这样每个请求都可以创建成功
2、当创建成功后,并不是每个节点都能够拿到锁,只有当前有序节点之前没有其他节点,即当前节点的序号是最小的时候,才说明了该节点拿到了锁;
3、拿到锁的请求,会话结束后,删除掉对应的临时节点即可;
4、其他没有拿到的锁的请求只需要保持对它前一个节点的监听,当它前面的节点不存在了,自己变成了最小的序号了,这就说明自己拿到了锁
未完待续。。。。。。
总结
整理不宜,转载请指明出处,共同进步吧!