分布式锁概述
前言
我们的系统都是分布式部署的,日常开发中,秒杀下单、抢购商品等等业务场景,为了防⽌库存超卖,都需要用到分布式锁。
分布式锁其实就是,控制分布式系统不同进程共同访问共享资源的一种锁的实现。如果不同的系统或同一个系统的不同主机之间共享了某个临界资源,往往需要互斥来防止彼此干扰,以保证一致性。
业界流行的分布式锁实现,一般有这3种方式:
- 基于数据库实现的分布式锁。
- 基于Redis实现的分布式锁。
- 基于Zookeeper实现的分布式锁。
那下面我们就来聊聊基于数据库实现的分布式锁。
基于数据库的分布式锁
数据库悲观锁实现的分布式锁
可以使用select … for update 来实现分布式锁。我们自己的项目,分布式定时任务,就使用类似的实现方案,我给大家来展示个简单版的哈!
表结构如下:
CREATE TABLE `t_resource_lock` (
`key_resource` varchar(45) COLLATE utf8_bin NOT NULL DEFAULT '资源主键',
`status` char(1) COLLATE utf8_bin NOT NULL DEFAULT '' COMMENT 'S,F,P',
`lock_flag` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '1是已经锁 0是未锁',
`begin_time` datetime DEFAULT NULL COMMENT '开始时间',
`end_time` datetime DEFAULT NULL COMMENT '结束时间',
`client_ip` varchar(45) COLLATE utf8_bin NOT NULL DEFAULT '抢到锁的IP',
`time` int(10) unsigned NOT NULL DEFAULT '60' COMMENT '方法生命周期内只允许一个结点获取一次锁,单位:分钟',
PRIMARY KEY (`key_resource`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin
加锁lock方法的伪代码如下:
@Transcational //一定要加事务
public boolean lock(String keyResource,int time){
resourceLock = 'select * from t_resource_lock where key_resource ='#{keySource}' for update';
try{
if(resourceLock==null){
//插入锁的数据
resourceLock = new ResourceLock();
resourceLock.setTime(time);
resourceLock.setLockFlag(1); //上锁
resourceLock.setStatus(P); //处理中
resourceLock.setBeginTime(new Date());
int count = "insert into resourceLock";
if(count==1){
//获取锁成功
return true;
}
return false;
}
}catch(Exception x){
return false;
}
//没上锁并且锁已经超时,即可以获取锁成功
if(resourceLock.getLockFlag=='0'&&'S'.equals(resourceLock.getstatus)
&& new Date()>=resourceLock.addDateTime(resourceLock.getBeginTime(,time)){
resourceLock.setLockFlag(1); //上锁
resourceLock.setStatus(P); //处理中
resourceLock.setBeginTime(new Date());
//update resourceLock;
return true;
}else if(new Date()>=resourceLock.addDateTime(resourceLock.getBeginTime(,time)){
//超时未正常执行结束,获取锁失败
return false;
}else{
return false;
}
}
解锁unlock方法的伪代码如下:
public void unlock(String v,status){
resourceLock.setLockFlag(0); //解锁
resourceLock.setStatus(status); S:表示成功,F表示失败
//update resourceLock;
return ;
}
整体流程:
try{
if(lock(keyResource,time)){ //加锁
status = process();//你的业务逻辑处理。
}
} finally{
unlock(keyResource,status); //释放锁
}
其实这个悲观锁实现的分布式锁,整体的流程还是比较清晰的。就是先select … for update 锁住主键key_resource那个记录,如果为空,则可以插入一条记录,如果已有记录判断下状态和时间,是否已经超时。这里需要注意一下哈,必须要加事务哈。
2.2 数据库乐观锁实现的分布式锁
除了悲观锁,还可以用乐观锁实现分布式锁。乐观锁,顾名思义,就是很乐观,每次更新操作,都觉得不会存在并发冲突,只有更新失败后,才重试。它是基于CAS思想实现的。我以前的公司,扣减余额就是用这种方案。
搞个version字段,每次更新修改,都会自增加一,然后去更新余额时,把查出来的那个版本号,带上条件去更新,如果是上次那个版本号,就更新,如果不是,表示别人并发修改过了,就继续重试。
大概流程如下:
- 查询版本号和余额。
select version,balance from account where user_id ='666';
假设查到版本号是oldVersion=1。
逻辑处理,判断余额。
if(balance<扣减金额){
return;
}
left_balance = balance - 扣减金额;
进行扣减余额。
update account set balance = #{left_balance} ,version = version+1 where version
= #{oldVersion} and balance>= #{left_balance} and user_id ='666';
大家可以看下这个流程图哈:
这种方式适合并发不高的场景,一般需要设置一下重试的次数。