1.基于数据库表的实现
首先创建一张锁表
这里将方法名做唯一性约束
当我们想锁住某个方法时,可以根据方法名insert一条语句
INSERT INTO `aa`.`t_lock` (`t_id`, `t_method`, `t_created_time`, `f_memo`) VALUES ('1', 'payMethod', '2019-01-09 15:26:27', '支付方法');
这时如果有另外一个线程要调用支付的方法,由于违反了数据库唯一性的约束,会抛出异常,因此可以认定为拿不到锁,当拿到锁的方法执行完业务代码后,最后释放锁的时候
再执行一条delete语句就行。
delete from t_lock where t_method = "payMethod"
这个案例中使用method作为唯一性约束,实际总可根据具体业务自行定义,例如排期案例中,即可选取日期作为主键id,主键本身满足唯一性,同时每天的日期也是不一样的。
问题
1、数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。
2、这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。
3、这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。
4、这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。
解决
1.数据库可以考虑主备方式,主的一旦挂了,即可切换到从库。
2.可以在表里加一个失效时间的字段,通过另外一个定时任务工程扫表,判断过期时间来释放锁。
3.可以采用while循环方式,如果插入失败,捕获异常,同时在外层加个判断,如果没有拿到锁,继续执行,直到拿到锁的线程的释放锁,继续执行业务。
4.可以在表里再加个2个字段,譬如1个是主机ip,和当前线程,当线程锁住时同时插入这两个字段,如果要想拿到锁,根据线程名字和主机ip来查询,如果查询到了,直接分配。
最终数据表方案设计如下
CREATE TABLE `t_lock` ( `t_id` int(11) NOT NULL, `t_method` varchar(255) NOT NULL COMMENT '锁定的方法名', `t_created_time` datetime NOT NULL COMMENT '创建时间', `f_memo` varchar(255) DEFAULT NULL COMMENT '备注', `f_host_ip` varchar(255) NOT NULL COMMENT '主机ip', `f_thread_name` varchar(255) NOT NULL COMMENT '线程名', PRIMARY KEY (`t_id`), UNIQUE KEY `method_index` (`t_method`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
2.基于数据库排他锁
利用数据库的唯一性约束,通过增删来加锁解锁是一种方案,同时利用数据库自带的排他锁也能实现分布式锁。
同样参考上表
原始sql
select * from t_lock where t_method = "payMethod";
现在后面加上 for update
public boolean lock() { //开启事务 begin; while (true) { try { string result = "select * from t_lock where t_method = "payMethod" for update; if (result == null) { return true; } } catch(Exception e) { } } }
public void unock() {
//提交事务,释放排他锁
commit();
}
问题:
阻塞锁? for update
语句会在执行成功后立即返回,在执行失败时一直处于阻塞状态,直到成功。
锁定之后服务器宕机,无法释放?使用这种方式,服务宕机之后数据库会自己把锁释放掉。
同样还是无法直接解决数据库单点 还有可重入问题。
总结:
优点
直接借助数据库,容易理解。
缺点
会出现的问题多,在解决问题的过程中会使整个方案变得越来越复杂。
操作数据库需要一定的开销,性能问题需要考虑。不建议和业务表放在一个数据库实例
使用数据库的行级锁并不一定靠谱,尤其是当我们的锁表并不大的时候。