一、分布式锁的应用场景
锁的机制主要目的是为了解决再多线程情况下的数据安全一致性。
仅有一台应用服务器的情况,例如查询某个账户余额并进行扣款,使用java自带的线程锁便可以解决问题。
- synchronized
- lock
- db lock
若有多台应用服务器,逻辑分布在多个应用服务上,需要通过中间渠道来保证操作的唯一性,此时便需要采用分布式锁。我们需 要怎么样的分布式锁?
-
可以保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行。
-
这把锁要是一把可重入锁(避免死锁)
-
这把锁最好是一把阻塞锁(根据业务需求考虑要不要这条)
-
这把锁最好是一把公平锁(根据业务需求考虑要不要这条)
-
有高可用的获取锁和释放锁功能
-
获取锁和释放锁的性能要好
二、分布式锁的集中实现方式
1、基于数据库方式
CREATE TABLE `DistributedLock` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(64) NOT NULL DEFAULT '' COMMENT '锁名',
`desc` varchar(1024) NOT NULL DEFAULT '备注信息',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存数据时间,自动生成',
PRIMARY KEY (`id`),
UNIQUE KEY `uidx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';
1、利用索引唯一性
//数据库中的每一条记录就是一把锁,利用的mysql唯一索引的排他性
lock(name,desc){
insert into DistributedLock(`name`,`desc`) values (#{name},#{desc});
}
unlock(name){
delete from DistributedLock where name = #{name}
}
锁重入:可增加可重入功能(避免再次获取锁导致死锁)
增加字段进程识别信息(ip、服务名称、线程id) 与 重入计数count,如果是同一个进程同一个线程则允许重入。
-
获取:再次获取锁的同时更新count(+1).
-
释放:更新count-1,当count==0删除记录。
可靠性
主从mysql:mysql宕机,立刻切换。
锁的持有者挂掉:定时任务清楚持有一定时间的锁。
性能
db操作都有一定性能损耗
阻塞锁
有此需求的业务线需要使用自旋多次尝试获取锁的实现。
2、利用select … where … for update 排他锁
boolean lock(){
connection.setAutoCommit(false)
while(true){
try{
result = select ... from DistributedLock where name=lock for update;
if(result==null){
return true;
}
}catch(Exception e){
connection.commit();
}
sleep(*);
}
return false;
}
void unlock(){
connection.commit();
其他附加功能与实现一基本一致,这里需要注意的是“where name=lock ”,name字段必须要走索引,否则会锁表。有些情况下,比如表不大,mysql优化器会不走这个索引,导致锁表问题。
2、乐观锁 version
所谓乐观锁与前边最大区别在于基于CAS思想,是不具有互斥性,不会产生锁等待而消耗资源,操作过程中认为不存在并发冲突,只有update version失败后才能觉察到。我们的抢购、秒杀就是用了这种实现以防止超卖。
通过增加递增的版本号字段实现乐观锁
-
select ...,version
-
update table set version+1 where version=xx(备注:这里的version应该设置为version自增, 而不是account= oldAccount, 可以防止集群环境下 a+a-a的情况)