实现方式
基于数据库的锁实现也有两种方式,一是基于数据库表,另一种是基于数据库排他锁。
数据库表的增删
思路
具体使用的方法,当需要锁住某个方法时,往该表中插入一条相关的记录。这边需要注意,方法名是有唯一性约束的,如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,可以执行方法体内容。
执行完毕,需要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/