分布式锁的学习

锁是什么

锁的意义在于多个线程(单点)或者进程(分布式)对于共享的资源进行修改操作时,要保证一个先后顺序。

比如多个人买票,同一个座位只能卖给同一个人。

实现锁的基本原理是设置一个所有人都能及时查看、修改的标记,然后所有人在操作资源之前查看修改该标记。

实现锁的方式

java原生

synchronize 关键字

synchronize 方法 当前对象

在同步方法调用前,加入了 monitor.enter,退出时加入了monitor.exit。而每一个方法对应一个对象监视器( Monitor );并且获取这个对象是排他的。

synchronize 同步静态方法 当前Class对象

synchronize 块 {}中对象

Lock接口

设置一个由volitile 修饰的变量,并保证每个线程都可以对查看,原子修改。

基于数据库

唯一约束(主键)

在数据库中添加一个锁表,对于同样的一个操作,使用一个确定的主键操作锁表:加锁时在锁表中添加一条记录,然后执行业务逻辑,释放锁时删除对应的记录即可。

可能有一些问题:

数据库挂了就不可用;

锁没有失效时间,可能死锁;

是非阻塞式的,一旦插入失败就不会排队等待;

非重入式的,当前线程没有释放锁之前没办法再次获得锁。

for update语句

主要依赖以下SQL:

1
select * from lock where lock_name=xxx for update;

在查询后面增加 for update ,数据库会在查询过程中给数据表增加排它锁,如果查询没有 commit,其他线程始终查询不到结果。

注意:

1、InnoDB引擎在加锁的时候,只有通过索引进行检索的时候才会使用行级锁,否则会使用表级锁。这里我们希望使用行级锁,就要给method_name添加索引。重载方法的话建议把参数类型也加上。

2、MySql会对查询进行优化,即便在条件中使用了索引字段,但是否使用索引来检索数据是由 MySQL 通过判断不同执行计划的代价来决定的,如果 MySQL 认为全表扫效率更高,比如对一些很小的表,它就不会使用索引,这种情况下 InnoDB 将使用表锁,而不是行锁。

3、要使用排他锁来进行分布式锁的lock,那么一个排他锁长时间不提交,就会占用数据库连接。一旦类似的连接变得多了,数据库连接池就爆了。

版本号实现的乐观锁

普通操作是:先查询是否有修改标记,在可执行时修改资源和修改标记,完成;

但是查询修改标记和修改之间可能被其他线程 修改A->B,并释放了资源B->A。这就是 ABA问题。

加一个版本号后,第一次查询标记时查询到当前版本号,在进行操作时,除了核对修改标记还要进行版本号的核对;同事修改资源值,并将版本号增加1。

但是:

效率低下,多次连接数据库;

多个资源保证数据一致性时需要给每个资源一张资源表。

基于Redis

Redis中有API : setnx+getset

将key设置为value,并返回1;如果给定key已经存在,则不做任何动作并返回 0。

死锁问题:

如果有线程获得锁后没有释放,变死锁了。

粗暴的解决方案是超时立刻删除锁并重新设置,但是获取到重新设置之间可能有其他线程机内并修改。

所以需要设置一个超时时间。

线程 当前 设置值 获取值<当前 结果

a–> 100 100+15 = 115 get 80<100 ok

b–> 116 116+15=131 get 115<116 ok

c–> 117 117+15=161 get 161<117 ×

其他方式

加锁 jedis.set(“”,””,””,””,””);

1
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);

释放锁 lua脚本

1
2
3
4
5
6
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;

错误的释放的例子:

1
2
3
4
5
// 判断加锁与解锁是不是同一个客户端
if (requestId.equals(jedis.get(lockKey))) {
// 若在此时,这把锁突然不是这个客户端的,则会误解锁
jedis.del(lockKey);
}

redisson框架

暂时没用过!

REDLOCK

获取锁:

1、获取当前系统时间;

2、获取N个节点(设置较短的超时时间);

3、计算获取锁花费时间 = 当前时间-步骤1的时间,获取客户端个数超过一半+1,并且获取锁的时间小于锁的超时时间,才能获得锁;

4、客户端获取锁的时间需要重新计算,应该为为设置锁的超时时间减去步骤3计算出获取锁花费的时间;

5、客户端获取失败删除所有客户端的锁。

释放锁:向所有redis节点发起释放锁的操作。

存在的问题:

1、锁会自动失效,如果客户端长期阻塞导致锁过期,那么它接下来访问共享资源就不安全了(没有了锁的保护);

2、第4步成功获取了锁之后,如果由于获取锁的过程消耗了较长时间,重新计算出来的剩余的锁有效时间很短了,那么我们还来得及去完成共享资源访问吗?如果我们认为太短,是不是应该立即进行锁的释放操作?那到底多短才算呢?又是一个选择难题;

3、系统时间可能会有较大的跳跃,那么锁会快速过期

基于Zookeeper

Zookeeper的节点主要有四种:

持久节点;

持久节点顺序节点;

临时节点;

临时节点顺序节点;

加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个是否为第一个。当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。

使用zookeeper的主要代码框架自动解决!但是性能可能不及redis。

小结

分布式锁的关键是每一个获取锁,释放锁的操作是不是原子的,会不会在获取和释放的判断过程中被其他线程修改。实际上分布式锁的选择也要根据业务特点进行选择。