一、基于数据库实现
1、方案1:基于数据库唯一性约束实现
说明:可以基于数据库唯一键特性实现分布式锁。
步骤1:建表
|
此方案中InnoDB 特性不是必须的
步骤2:当我们想要锁住某个方法时,执行以下SQL:
|
说明:因为我们对method_name做了唯一性约束,这里如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,可以执行方法体内容。
步骤3:当方法执行完毕之后,想要释放锁的话,需要执行以下Sql:
|
方案总结
上面这种简单的实现有以下几个问题:
问题1:这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。
问题2:这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。
问题3:这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。
问题4:这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。
问题5:这把锁是非公平锁,所有等待锁的线程凭运气去争夺锁。
当然,我们也可以有其他方式解决上面的问题。
问题1:数据库是单点?搞两个数据库,数据之前双向同步。一旦挂掉快速切换到备库上。
问题2:没有失效时间?只要做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍。
问题3:非阻塞的?搞一个while循环,直到insert成功再返回成功。
问题4:非重入的?在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。
问题5:非公平的?再建一张中间表,将等待锁的线程全记录下来,并根据创建时间排序,只有最先创建的允许获取锁
2、方案2:基于数据库排他锁实现
说明:除了可以通过增删操作数据表中的记录以外,其实还可以借助数据中自带的锁来实现分布式的锁。我们还用刚刚创建的那张数据库表。可以通过数据库的排他锁来实现分布式锁。 基于MySql的InnoDB引擎,可以使用以下方法来实现加锁操作:
步骤1:建表(建表语句中绿色的是关键)
|
步骤2:使用下面的方式加锁
//注意1:上面是伪代码 //注意2:经过测试,如果select未能获取锁,会阻塞,直到成获取锁,但是好像有获取不到锁时阻塞等待的超时时间(所以上面的java代码会进行循环获取):
|
上面是在java代码中通过connection.setAutoCommit(false)+connection.commit()不进行自动提交,而自己控制事务,如果使用客户端测试,可以:
|
说明:在查询语句后面增加for update,数据库会在查询过程中给数据库表增加排他锁。当某条记录被加上排他锁之后,其他线程无法再在该行记录上增加排他锁。我们可以认为获得排它锁的线程即可获得分布式锁,当获取到锁之后,可以执行方法的业务逻辑。
步骤3:执行完方法之后,再通过以下方法解锁:
|
通过connection.commit();操作来释放锁。
方案总结
这种实现可以解决【基于数据库唯一性约束实现】方案中以下几个问题:
这种方法可以有效的解决上面提到的
1、无法释放锁的问题(问题2):
(如果由于异常等原因未能执行commit操作,也会自动释放排他锁,释放后可以继续由其他事务获取):
阻塞锁的问题:for update语句会在执行成功后立即返回,在执行失败时一直处于阻塞状态,直到成功。
2、阻塞锁的问题(问题3)
锁定之后服务宕机,无法释放锁的问题:使用这种方式,服务宕机之后数据库会自己把锁释放掉。
这种实现仍然存在的问题
问题1:无法直接解决数据库单点
问题4:可重入
问题5:公平锁的问题。
二、基于数据库实现总结
总结一下使用数据库来实现分布式锁的方式:
1、这两种方式都是依赖数据库的一张表,一种是通过表中的记录的存在情况(唯一键)确定当前是否有锁存在,另外一种是通过数据库的排他锁来实现分布式锁。
2、数据库实现分布式锁的 优点
1)直接借助数据库,容易理解。
3、数据库实现分布式锁的 缺点
1)会有各种各样的问题,在解决问题的过程中会使整个方案变得越来越复杂。
2)操作数据库需要一定的开销,性能问题需要考虑。