前言
大家好,我是飓风,今天我们来聊聊分布式锁的原理、以及基于 mysql 怎么来实现分布式锁。
那么大家现在能不能想一想,分布式锁的使用场景都有哪些呢?下面我列举一些分布式锁的场景:
记住一点,一定是在分布式的环境下,所以肯定是多个服务,或者多个进程来操作一个共享资源。
- 扣减库存
- 订单支付,检查订单是否进行了重复支付的操作
- 缓存击穿/缓存雪崩,防止大并发对 DB 的操作
上面的场景大家有没有发现一个共同点,那就是在分布式的环境下,需要一种机制,来保证对共享资源【库存、支付状态、db 中的某个数据】的互斥访问,那么怎么来达到互斥访问的目的呢,那么就需要分布式锁来实现了。
分布式锁的特性
- 互斥性
必须,即保证不同节点不同线程的互斥访问。
- 超时机制
必须,即锁拥有的时长,也就是锁要有超时机制,防止其中一个节点一直拥有该锁造成死锁,那么其他节点,将永远也获取不到锁了,另一种情况就是设置了锁的超时时间,但是业务执行了的时间超过了设置的超时时间,那么锁自动释放了,那么此时互斥性就没了。
- 提供阻塞和非阻塞接口
必须,所谓阻塞接口,就是没有获取到锁,那么就会一直等待锁的到来,不能中断。
非阻塞接口,就是尝试获取锁,锁没有获取到,就立即返回没有获取到锁,需要自己来重试,同时还可以支持一个获取锁的超时时间,在这个超时时间内没有获取到,那么还是需要自己重试的,减少 cpu 的浪费。
- 可重入性
可选,当同一个节点同一个线程,想再次获取到该锁的时候,应该直接获取,不用在重新竞争了。
- 公平锁和非公平锁
可选,这个其实很好理解,获取锁的优先级问题,公平就是排队,非公平就是有优先级,级别高的可以插队。
- 其他
最好是高可用的锁方案和高性能的锁接口
实现--mysql 唯一索引
利用 mysql 唯一索引的特性,这个唯一的索引列就是分布式环境下互斥的资源,如果某个节点先插入了这个唯一索引对应的列值,那么其他节点就会插入失败,也就是获取锁失败了,也就达到了互斥性。
创建表信息
表信息用户维护一些锁的信息。
CREATE TABLE distributed_lock (
id bigint(11) NOT NULL AUTO_INCREMENT,
lock_resource varchar(100) NOT NULL DEFAULT '' COMMENT '共享资源,可能某个方法或者数据行',
lock_desc varchar(100) NOT NULL DEFAULT '' COMMENT '锁描述',
update_time datetime NOT NULL,
create_time datetime NOT NULL,
PRIMARY KEY (id ),
UNIQUE KEY unique_idx_lock_resource (lock_resource )
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
互斥性的实现
利用 mysql 唯一索引来实现,我们要库某个 sku 的扣减库存操作。
获取锁,此时要对 sku=10000 的商品进行扣减库存的 SQL,只要执行成功,那么就获取锁成功了。
INSERT INTO distributed_lock(lock_resource,lock_count,lock_desc,update_time,create_time )VALUES ('10000',1,'库存锁',NOW(),NOW());
释放锁:
DELETE FROM distributed_lock WHERE lock_resource = '10000';
大家想一想这样,如果按照上面的方式释放锁,会有什么问题?
如果其他节点由于自己的错误原因,误删了这个锁,会怎么样,那么我的任务还没有执行完,我的锁没了,那么其他节点就能获取到该锁,那么互斥性就没了。
所以解决这个问题,就是在删除删除的锁的时候,要知道这是自己的锁,才能删除,如果不是,不会进行锁的删除,也就是锁的释放。
此时 需要给表 加一列为 ' lock_owner' ,也就是锁的拥有者。
DELETE FROM distributed_lock WHERE lock_resource = '10000' and lock_owner = '';
阻塞等待实现
什么是阻塞呢? 其实阻塞的概念很简单,比如你在进行 io 操作,还没有准备好读或者写操作,那么就需要一直等待,等待就绪,才能开始相应的读写操作,锁也是一样的,如果没有在进行获取锁的时候,获取锁的方法一直没有返回你想要的锁,那么此时你的的程序或者线程需要一直等待,阻塞这里,等待锁的返回。下面我们来看看阻塞等待的代码实现:
@Override
public void lock(String resource) {
// 这里通过一个死循环,来模拟了阻塞的实现,lockResource 进行资源的锁定方法 while (!lockResource(resource)){
//等待一会再去获取
LockSupport.parkNanos(WAIT_TIME);
}
}
非阻塞的实现
什么是非阻塞呢?所谓非阻塞,就是你获取锁的时候,没有获取到,也就是获取失败了,不用在一直傻傻等待了,获取锁的方法,会直接告诉你获取锁失败了,要不要继续获取锁,你自己来决定,下面来看下非阻塞的代码实现:
//lockResource 进行资源的锁定方法
@Override
public boolean nonLock(String resource) {
return lockResource(resource);
}
//加了一个等待获取锁的超时时间
@Override
public boolean nonLock(String resource,long timeout) {
long lastTime = System.currentTimeMillis() + timeout;
while (!lockResource(resource)){
if (System.currentTimeMillis() > lastTime) {
return false;
}
}
return true;
}
可重入性的实现
这里的可重入性和你在 java 中的锁 synchronized 和 lock 接口的可重入性的意思是一样,就是如果你已经获取该锁了,如果还需要该锁,那么不需要重新获取,直接获取就可以了,下面是代码的实现:
// 判断是不是自己的锁,如果是更新库内对应 count 的值 + 1
if (Objects.equals(uniqueLock.getLockOwner(),LockUtil.getLockOwner())){
return uniqueLockService.incCount(uniqueLock.getId());
}
释放锁的实现
记得一定要释放的是自己的锁哦
@Override
public boolean unLock(String resource) {
//这里要注意,释放锁,要保证是自己的锁才可以
final UniqueLock uniqueLock = uniqueLockService.findByResource(resource);
if (uniqueLock == null) return false;
// 是自己的锁,在进行操作 if (Objects.equals(uniqueLock.getLockOwner(),LockUtil.getLockOwner())){
// 如果大于 0 说明 count 要减 1if (uniqueLock.getLockCount() > 0){
return uniqueLockService.deCount(uniqueLock.getId());
}else {
// ==0 ,删除记录就可以了
return uniqueLockService.deleteById(uniqueLock.getId());
}
}
return false;
}
超时时间的实现
锁拥有的最大时间,防止程序出现意外,比如释放锁失败了,而一直拥有该锁,那么其他进程也就无法获取到该锁,那么业务也就无法进行下去了,这里我们可以通过一个定时任务来实现,判断表内,每个锁执行的时间,是否超过该锁设置的最大持有的时长,如果超过了这个时长,就认为锁释放失败了,此时要删除这个锁。
自动续期
没执行完,只要是自己拥有了该锁,那么可以自动续期,如果设置锁的超时时间是 10s 超时,如果 10s 还没有执行完成,那么自动续期的任务,就会检查到超时了,那么就自动续期一个时长如 30s,这里我们可以更新表中该条锁的时长+30s。
只要该锁没有被释放,那么就可以自动续期,所以一定要在你的 finally 中释放锁,来保证锁释放一定会执行。
此时你可能还会疑问,如果在释放锁本身的动作,出现了异常呢?我们该怎么实现呢?
其实也很简单,如果释放锁本身动作出现了异常或者返回失败了,只要我们捕获到该异常或者根据返回标识,然后通知自动续期的任务锁释放失败了,不要续期了,同时也可以帮助我删除掉该锁吧。
总结
今天我们了解了分布式锁以及它的特性。
- 分布式锁一定是在分布式环境下,对相同的资源进行操作,来会起作用。
- 分布式锁一定是互斥
- 分布式锁要提供阻塞和非阻塞实现
- 分布式锁要有超时时间,不能造成死锁
- 分布式锁的自动续期,防止业务还没有执行完,锁超时了,结果锁被强制释放了
说明:
这里在说下锁超时和自动续期
自动续期的任务监听到锁没有释放,那么会自动续期锁的超时时间,那么如果还没来的急续期,判断锁超时的任务,判断锁已经超时了,就会强制释放该锁,也就是删除该锁 ,实际上,业务还没有执行完成,所以判断锁超时的任务,可以在锁超时的时长上+5s,一个延迟时间,那么在这个延迟时间内,自动续期的任务就会得到执行,接着更新表中锁的超时时间。
思考:
这里我们没有给锁超时的任务检查代码 和 自动续期的代码实现,大家可以想想怎么去实现?
还有一个思考题,如果没有锁超时的任务检查,只有自动续期的检查,如果释放锁失败了,通知自动续期任务不要进行续期了,同时自动续期任务来释放锁,也就是删除锁,是否可以呢?
源码