分布式锁

虚幻大学 xuhss 332℃ 0评论

? 优质资源分享 ?

学习路线指引(点击解锁) 知识定位 人群定位
? Python实战微信订餐小程序 ? 进阶级 本课程是python flask+微信小程序的完美结合,从项目搭建到腾讯云部署上线,打造一个全栈订餐系统。
?Python量化交易实战? 入门级 手把手带你打造一个易扩展、更安全、效率更高的量化交易系统

分布式锁

本文整理自黑马程序员相关资料

问题的引入

在平时单服务的情况下,我们使用互斥锁可以保证同一时刻只有一个线程执行自己的业务。原理是,在JVM内部维护了一个锁监视器,锁监视器保证了同一时刻只有一个线程获取到锁。但是如果开启了多个服务,就会有多个JVM,从而有多个不同的锁监视器,每个锁监视器监视自己JVM内部的线程,因此一个JVM内部的线程获取到锁,并不影响其他JVM内部的线程获取锁。从而导致并发安全问题。因此,我们需要独立于JVM之外的锁监视器对所有的线程统一管理。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qKui62eb-1655313525737)(https://img2022.cnblogs.com/blog/2157285/202206/2157285-20220615203231200-214525175.png)]

概念

满足分布式系统或集群模式下多进程可见并且互斥的锁。

常见分布式锁的实现比较

MySQL Redis Zookeeper
互斥 利用Mysql本身的互斥锁机制 利用setnx这样的互斥命令 利用节点的唯一性和有序性实现互斥
高可用
高性能 一般 一般
安全性 断开连接,自动释放锁 利用锁超时时间,到期释放 临时节点,断开连接自动释放

基于Redis的分布式锁

最基本的分布式锁

获取锁:

利用Redis的SETNX保证互斥的特性,同时设置锁过期时间,避免服务宕机不能执行释放锁的操作而导致死锁。

释放锁:

删除对应的键即可

流程图如下所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K85auJYr-1655313525740)(https://img2022.cnblogs.com/blog/2157285/202206/2157285-20220615203246583-294637886.png)]

保证释放锁的线程是持有锁的线程本身

前面提到的最基本的分布式锁存在着一些问题。如果获取锁的线程1阻塞,在该线程阻塞期间,锁超时释放了,这时线程2就可以获取到锁,接着执行自己的业务。线程1在完成自己的业务后释放锁。这时线程3也获得了锁执行自己的业务,这样就造成了线程2和线程3都获取到了锁,从而造成了线程安全问题。如下图所示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fSkkQxhq-1655313525742)(https://img2022.cnblogs.com/blog/2157285/202206/2157285-20220615203258627-2068695710.png)]

为了解决未持有锁的线程释放锁这个问题,在锁中存入线程标识,在释放锁之前先判断锁标识是否是本身线程。如果标识是自己,则释放锁。其流程图如下所示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DHCKzNGc-1655313525743)(https://img2022.cnblogs.com/blog/2157285/202206/2157285-20220615203309233-259587849.png)]

保证释放锁的原子性

由于前面加入了判断,判断与释放是两步。有可能在判断时持有锁的线程1阻塞,直到超时释放锁,线程2拿到了锁,线程1被唤醒并执行释放锁,导致线程3也拿到了锁。造成了两个线程同时持有锁的线程安全问题。如下所示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CDNnDvL9-1655313525744)(https://img2022.cnblogs.com/blog/2157285/202206/2157285-20220615203325269-822454774.png)]

为了解决这个问题,使用Lua脚本,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。

释放锁的业务流程如下所示

-- 这里的 KEYS[1] 就是锁的key,这里的ARGV[1] 就是当前线程标示
-- 获取锁中的标示,判断是否与当前线程标示一致
if (redis.call('GET', KEYS[1]) == ARGV[1]) then
  -- 一致,则删除锁
  return redis.call('DEL', KEYS[1])
end
-- 不一致,则直接返回
return 0

到目前为止,一个基于Redis的基本的分布式锁就完成了。但还是存在着以下问题

  • 不可重入:同一线城无法多次获取统一把锁
  • 不可重试:获取锁只尝试一次就返回,没有重试机制
  • 超时释放问题:锁超时释放虽然可以避免死锁,但是如果业务执行耗时较长,也会导致锁释放,存在安全隐患
  • 主从一致性问题:如果Redis提供了主从集群,主从同步存在延迟,当主节点宕机时,从节点没有同步主节点中的锁数据。其他线程就会拿到锁

Redisson分布式锁简单介绍

Redisson可重入锁原理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fohoQ7Dx-1655313525745)(https://img2022.cnblogs.com/blog/2157285/202206/2157285-20220615203338012-2144150095.png)]

获取锁的Lua脚本

local key = KEYS[1]; -- 锁的key
local threadId = ARGV[1]; -- 线程唯一标识
local releaseTime = ARGV[2]; -- 锁的自动释放时间
-- 判断是否存在
if(redis.call('exists', key) == 0) then
    -- 不存在, 获取锁
    redis.call('hset', key, threadId, '1'); 
    -- 设置有效期
    redis.call('expire', key, releaseTime); 
    return 1; -- 返回结果
end;
-- 锁已经存在,判断threadId是否是自己
if(redis.call('hexists', key, threadId) == 1) then
    -- 不存在, 获取锁,重入次数+1
    redis.call('hincrby', key, threadId, '1'); 
    -- 设置有效期
    redis.call('expire', key, releaseTime);   
    return 1; -- 返回结果
end;
return 0; -- 代码走到这里,说明获取锁的不是自己,获取锁失败

释放锁的Lua脚本

local key = KEYS[1]; -- 锁的key
local threadId = ARGV[1]; -- 线程唯一标识
local releaseTime = ARGV[2]; -- 锁的自动释放时间
-- 判断当前锁是否还是被自己持有
if (redis.call('HEXISTS', key, threadId) == 0) then
    return nil; -- 如果已经不是自己,则直接返回
end;
-- 是自己的锁,则重入次数-1
local count = redis.call('HINCRBY', key, threadId, -1);
-- 判断是否重入次数是否已经为0 
if (count > 0) then
    -- 大于0说明不能释放锁,重置有效期然后返回
    redis.call('EXPIRE', key, releaseTime);
    return nil;
else  -- 等于0说明可以释放锁,直接删除
    redis.call('DEL', key);
    return nil;
end;

Redisson分布式锁原理

  • 可重入:利用hash结构记录线程id和重入次数
  • 可重试:利用信号量和PubSub功能实现等待、唤醒,获取锁失败的重试机制
  • 超时续约:利用watchDog,每隔一段时间(releaseTime/3),重置超时时间。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-buYCQPfq-1655313525746)(https://img2022.cnblogs.com/blog/2157285/202206/2157285-20220615203351840-388352262.png)]

转载请注明:xuhss » 分布式锁

喜欢 (0)

您必须 登录 才能发表评论!