Mysql分布式锁设计

        最近开发电商库存相关项目,其中最为重要的一个功能之一是分布式锁的实现。本文就项目组中用到的基于MySQL实现的分布式锁,做一些思考和总结。

1、常见的分布式锁实现方案

  • 基于数据库实现的分布式锁
  • 基于Redis实现的分布式锁
  • 基于Zookeeper实现的分布式锁

        在讨论使用分布式锁的时候往往首先排除掉基于数据库的方案,本能的会觉得这个方案不够“高级”。从性能的角度考虑,基于数据库的方案性能确实不够优异,但就目前笔者所在项目组来说,几乎所有项目的项目都是基于MySQL实现的分布式锁,所以采用哪种方案是要基于使用场景来看的,选择哪种方案,合适最重要,本文也仅就MySQL实现分布式锁展开讨论。

2、一把极为简单的MySQL分布式锁

        最容易想到的基于MySQL的分布式锁就是通过数据库的唯一键约束,来达到抢占锁资源的目的,本文也从这把最为简单的分布式锁讲起。在MySQL中创建一张表如下,为资源ID设置唯一键约束。

        当需要获取锁时,往数据库中一条记录,因为有唯一键约束,插入成功则代表获取到了锁。 

        释放锁时删除这条记录即可。

显然,这把锁因为太过简单,所以存在很多问题。

  • 没有锁失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。
  • 不可重入,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。
  • 非阻塞,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。

        当然还有比如锁可靠性完全依赖于数据库,这是基于MySQL分布式锁的必然缺陷,这里不做讨论。下面将一步步优化这把最为简单的锁。

3、锻造一把好锁

3.1、锁超时失效

        通常想到的方法是做一个定时清理过期资源的程序,每隔一定时间把数据库中的超时数据清理一遍。这种做法最为简单直接,但也有一些相应的弊端,比如增加了程序复杂性(需要专门实现并配置定时任务),锁的超时时间也不方便灵活配置。本文试图说明一种笔者认为更为合理的方式。

        前文所述的拿锁方式非常简单粗暴,数据插入不成功则拿锁失败,但此时数据库中的锁记录可能已经超时,所以需要在插数失败之后做进一步动作,以判断之前拿锁的线程是否已经超时。只需取出当前锁记录,比较锁记录时间与当前时间差值是否已超出锁等待时间,如未超出,获取锁失败,如超出,修改锁记录时间为当前时间,拿锁成功。代码如下:

        这里需要强调,修改锁记录时间必须通过CAS操作,因为可能存在多个线程同时争抢一把已经过期的锁,如果不通过CAS操作,可能多个线程同时获取到锁。上述只是代码片段,程序中可以提供方法给调用者灵活修改锁的超时等待时间,同时也不再需要专门配置定时清理过期记录的任务。

3.2、锁可重入

        有了前面解决超时失效问题的思路,很容易想到的方案是在表中加个字段记录当前获得锁的机器和线程信息,当线程再次获取锁的时候先查询数据库,如果当前机器和线程信息在数据库可以查到的话,直接把锁分配给该线程即可。这种方式多了一步查询操作,对锁性能有一定影响,是否可以把成功获取锁的线程和其获取到的锁放到一个容器里呢某个线程需要拿锁时,先在容器中找下自己是否已经拿到过锁,拿到了那就不必和数据库打交道了。那就这么干,代码如下:

        同样需要强调的是放入锁记录的容器必须是线程安全的,同时只有第一个往容器中成功添加所记录的线程,才能往数据库插入锁记录,很大程度上降低了争抢锁记录的线程与MySQL打交道的频率,能有效提升性能。

3.3、阻塞式获取锁

        阻塞一个while循环,直到tryLock成功实这也不失为一种解决方式,emm……总觉得不够优雅。而且通过轮训的方式,会占用较多的CPU资源。

        能否借助MySQL的悲观锁实现呢助 for update 关键字来给被查询的记录添加行锁中悲观锁,这样别的线程就没有办法对这条记录进行任何操作,从而达到保护共享资源的目的。

采用这种方式需要注意:

  • MySQL默认是会自动提交事务的,应该手动禁止一下: SET AUTOCOMMIT = 0;
  • 行锁是建立在索引的基础上的,如果查询时候不是走的索引的话,行锁会升级为表锁进行全表扫描;
  • 申请锁操作:先往数据库中插入一条锁记录,然后select * from distribute_lock where resource_id = for update; 只要可以查的出来就是申请成功的,没有获取到的会被阻塞,阻塞的超时时可以通过设置 MySQL 的 innodb_lock_wait_timeout 来进行设置。
  • 释放锁操作:COMMIT; 事务提交之后别的线程就可以查询到这条记录。

来源:软件开发随心记

声明:本站部分文章及图片转载于互联网,内容版权归原作者所有,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

上一篇 2022年8月2日
下一篇 2022年8月2日

相关推荐