实现方式

基于数据库的锁实现也有两种方式,一是基于数据库表,另一种是基于数据库排他锁。

数据库表的增删

思路

具体使用的方法,当需要锁住某个方法时,往该表中插入一条相关的记录。这边需要注意,方法名是有唯一性约束的,如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,可以执行方法体内容。

执行完毕,需要delete该记录。

实质

插入:插入意向锁(表级别锁)

删除:排他锁

优化

对于上述方案可以进行优化,如应用主从数据库,数据之间双向同步。

一旦挂掉快速切换到备库上;做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍;

使用 while 循环,直到 insert 成功再返回成功,虽然并不推荐这样做;

还可以记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了,实现可重入锁。

核心问题的解决

  • 数据库是单点?

搞两个数据库,数据之前双向同步,一旦挂掉快速切换到备库上;

  • 没有失效时间?

只要做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍;

  • 非阻塞的?

搞一个 while 循环,直到 insert 成功再返回成功;

  • 非重入的?

在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了;

  • 非公平的?

再建一张中间表,将等待锁的线程全记录下来,并根据创建时间排序,只有最先创建的允许获取锁; 比较好的办法是在程序中生产主键进行防重。

MVCC

乐观锁 - 基于表字段版本号做分布式锁

思路

这个策略源于 mysql 的 mvcc 机制,使用这个策略其实本身没有什么问题,唯一的问题就是对数据表侵入较大,我们要为每个表设计一个版本号字段,然后写一条判断 sql 每次进行判断,增加了数据库操作的次数,在高并发的要求下,对数据库连接的开销也是无法忍受的。

数据库排他锁

悲观锁 - 基于数据库排他锁做分布式锁

原理

我们还可以通过数据库的排他锁来实现分布式锁。

基于 MySql 的 InnoDB 引擎,可以使用以下方法来实现加锁操作:

  • 伪代码
public void lock(){
    connection.setAutoCommit(false)
    int count = 0;
    while(count < 4){
        try{
            select * from lock where lock_name=xxx for update;
            if(结果不为空){
                //代表获取到锁
                return;
            }
        }catch(Exception e){
        }
        //为空或者抛异常的话都表示没有获取到锁
        sleep(1000);
        count++;
    }
    throw new LockException();
}
  • 注意

InnoDB 引擎在加锁的时候,只有通过索引进行检索的时候才会使用行级锁,否则会使用表级锁。

这里我们希望使用行级锁,就要给要执行的方法字段名添加索引,值得注意的是,这个索引一定要创建成唯一索引,否则会出现多个重载方法之间无法同时被访问的问题。

重载方法的话建议把参数类型也加上。当某条记录被加上排他锁之后,其他线程无法再在该行记录上增加排他锁。

我们可以认为获得排他锁的线程即可获得分布式锁,当获取到锁之后,可以执行方法的业务逻辑,执行完方法之后,通过 connection.commit() 操作来释放锁。

核心问题的解决

  • 阻塞锁

for update 语句会在执行成功后立即返回,在执行失败时一直处于阻塞状态,直到成功;

  • 服务器宕机

锁定之后服务宕机,无法释放?使用这种方式,服务宕机之后数据库会自己把锁释放掉。

  • 数据库单点 & 可重入问题

但是还是无法直接解决数据库单点和可重入问题。

虽然我们对方法字段名使用了唯一索引,并且显示使用 for update来使用行级锁。

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

优缺点

优点是直接借助数据库,简单容易理解。

缺点是操作数据库需要一定的开销,性能问题需要考虑。

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

参考资料

https://juejin.im/entry/5a502ac2518825732b19a595

http://blog.jobbole.com/113707/

https://runnerliu.github.io/2018/05/06/distlock/