分布式锁

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云原生网关 MSE Higress,422元/月
注册配置 MSE Nacos/ZooKeeper,182元/月
简介: 在如今很多分布式应用中,都会用到分布式锁的场景。在分布式模型下,数据只有一份时需要利用锁的技术控制某一时刻修改数据的进程数。

一、分布式锁的特点

  1. 在同一时间内只能被同一个线程执行
  2. 这把锁需要是一把可重入锁,避免死锁
  3. 这把锁最好是一把阻塞锁
  4. 获取锁和释放锁的性能要高
  5. 具备锁失效机制,防止死锁,无法清除锁

针对分布式锁的实现,目前比较常用的有以下几种方案:

二、基于数据库实现分布式锁

(一)、基于数据库insert和delete实现

  1. 创建一张锁表,通过操作该表中的数据来实现,当我们要锁住某个方法或资源时,就在表中增加一条记录,要释放锁的时候删除这条记录。
CREATETABLEIFNOTEXISTS'methodlock'('method_name'varchar(128)notnulldefault''comment'方法名称',primarykey('methodlock')) ENGINE=InnoDB DEFAULT CHARSET=utf8;

当我们要锁住某个方法时,执行以下SQL:

insertinto methodLock (method_name)values('method_name');

因为我们对method_name做了主键约束,这里如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,就可以执行我们的业务逻辑。

当方法执行完毕之后,想要释放锁的话,需要执行以下Sql:

deletefrom methodLock where method_name='method_name';

这种简单的数据插入和删除数据的实现会出现以下几个问题:

  • 锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致应用不可用。
  • 一旦解锁失败,就会导致锁记录一直在数据库中,其他线程无法再次获得锁。
  • 这把锁是非阻塞的,插入数据失败就会直接报错,没有获得锁的线程并不会进入到排队队列,想再次获得锁就要再次出发获得锁的操作。
  • 锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁,因为数据库中数据已经存在。

(二)、基于数据库排他锁实现

借助数据库中自带的锁来实现分布式锁,还用刚刚创建的表,可以通过数据库的排他锁来实现分布式锁。基于MySql的InnoDB引擎,可以试用以下方法来实现加锁操作:

publicbooleanlock(){
connection.setAutoCommit(false);
while(true){
try{
result=select*frommethodlockwheremethod_name='methodname'forupdate;
if(result!=null) returntrue;
         }catch(Exceptione){
         }
   } 
returnfalse;
}

在查询语句后面增加for update ,数据库会在查询过程中给数据库表增加排他锁(InnoDB引擎在加锁的时候,只有通过索引进行检索的时候才会使用行级锁,否则会使用表级锁。我们这里肯定希望使用行级锁)当某条记录被加上排他锁之后,其他线程无法再在改航记录上增加排他锁。

我们可以任务获得排他锁的线程即是获得分布式锁的线程,获取到锁之后,可以执行业务逻辑,业务逻辑执行完之后再通过以下方法解锁:

publicvodiunlock(){
connection.commit();
}

这种方法可以有效的解决上面提到的无法释放锁和阻塞锁的问题。for update语句会在执行成功后立即返回,在执行失败时一直处于阻塞状态,直到成功。锁定之后服务宕机,无法释放?使用这种方式,服务宕机之后数据库会自己把锁释放掉。但是还是无法直接解决数据库单点和可重入问题。

还有一个问题需要注意就是我们要使用排他锁来进行分布式锁的lock,那么一个排他锁长时间不提交,就会占用数据库连接。一旦类似的连接变得多了,就可能把数据库连接池撑爆。

三、基于redis实现分布式锁

基于redis分布式缓存实现锁机制,采用的是redis的特性

StringlocakKey="LOCK";
Stringvalue=Thread.currentThread().getId().toString();
//加锁和设置过期时间放在一个事务中,防止加锁成功后,程序崩溃,对KEY没有设置上过期时间,导致死锁。try {
Booleanresult=stringRedisTemplate.opsForValue().setIfAbsent(lockKey, 
value, 10, TimeUnit.SECONDS);
if(!result){
returnfalse;   
            }
//业务逻辑    }catch (Exceptione){
e.printStackTrace();    
}finally {
//释放锁相应的锁,为防止误删锁,需要判断value是不是自己的value,value是自己线程的IDif(value.equals(stringRedisTemplate.opsForValue().get(lockKey))){
stringRedisTemplate.opsForValue().delete(lockKey);
    }
}
returntrue;
}


上述代码还是有问题,因为线程1在还没有执行完成的时候,此时锁已经到达过期时间,此时线程2则会加锁成功。例如线程一执行需要15秒,但是锁的时间只有10秒,线程一未执行完,锁超期时间到达,自动解锁,线程2自动加锁。针对此种情况可单独开辟一个线程,对锁进行续命,每3秒钟获取value值,如果value值和当前线程的值一直则进行自动续期。

  • 使用redission框架实现redis分布式锁
@AutowiredRedissonredisson;
@RequestMapping("/deduct_stock_redisson")
publicStringdeductStockRedisson() {
StringlockKey="lock_key";
RLockrlock=redisson.getLock(lockKey);
try {
rlock.lock();
//业务逻辑实现,扣减库存        ....
    } catch (Exceptione) {
e.printStackTrace();
    } finally {
rlock.unlock();
    }
return"end";
}
  • 多个线程去执行lock操作,仅有一个线程能够加锁成功,其它线程循环阻塞。
  • 加锁成功,锁超时时间默认30s,并开启后台线程,加锁的后台会每隔10秒去检测线程持有的锁是否存在,还在的话,就延迟锁超时时间,重新设置为30s,即锁延期。
  • 对于原子性,Redis分布式锁底层借助Lua脚本实现锁的原子性。锁延期是通过在底层用Lua进行延时,延时检测时间是对超时时间timeout /3

采用redis实现分布式锁,还会存在锁同步的问题,当主Redis加锁了,开始执行线程,若还未将锁通过异步同步的方式同步到从Redis节点,主节点就挂了,此时会把某一台从节点作为新的主节点,此时别的线程就可以加锁了,这样就出错了,怎么办?

  • 使用zookeeper集群替代redis集群,实现同步。
  • 使用红锁redlock算法,搭建多个redis,一般是奇数个,但是redis之间没有任何关系。

image.png

redis1

超过半数redis节点加锁成

功才算加锁成功

加锁

Java

redis2

加锁

Client

加锁

redis3

假设有3个redis节点,这些节点之间既没有主从,也没有集群关系。客户端用相同的key和随机值在3个节点上请求锁,请求锁的超时时间应小于锁自动释放时间。当在2个(超过半数)redis上请求到锁的时候,才算是真正获取到了锁。如果没有获取到锁,则把部分已锁的redis释放掉。

@RequestMapping("/deduct_stock_redlock")
publicStringdeductStockRedlock() {
StringlockKey="lock_key";
//TODO 这里需要自己实例化不同redis实例的redisson客户端连接,这里只是伪代码用一个redisson客户端简化了RLockrLock1=redisson.getLock(lockKey);
RLockrLock2=redisson.getLock(lockKey);
RLockrLock3=redisson.getLock(lockKey);
// 向3个redis实例尝试加锁RedissonRedLockredLock=newRedissonRedLock(rLock1, rLock2, rLock3);
booleanisLock;
try {
// 500ms拿不到锁, 就认为获取锁失败。10000ms即10s是锁失效时间。isLock=redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS);
System.out.println("isLock = "+isLock);
if (isLock) {
//业务逻辑处理            ...
        }
    } catch (Exceptione) {
    } finally {
// 无论如何, 最后都要解锁redLock.unlock();
    }
}

如果采用以上redlok需要提高并发,可采用

四、基于zookeeper实现分布式锁

Zookeeper是基于临时有序节点实现分布式锁,当客户端需要加锁时候去zookeeper中创建一个目录,并生成一个瞬时有序节点,判断是否能获取锁的方式很简单,自己的节点是否是最小的一个。当使用完成后删除自己的临时节点,然后通知。


image.png


五、三种方案的比较

“没有银弹”,上面三种方案,无论那种方案都不可能做到完美。就像CAP一样,在复杂性、可靠性、性能等方面无法同时满足,所以,根据自己的需求选择适合自己的方案才是王道。

从理解的难易程度角度(从低到高)

数据库 > 缓存 > Zookeeper

从实现的复杂性角度(从低到高)

Zookeeper >= 缓存 > 数据库

从性能角度(从高到低)

缓存 > Zookeeper >= 数据库

从可靠性角度(从高到低)

Zookeeper > 缓存 > 数据库

相关文章
|
1月前
|
机器学习/深度学习 算法 数据可视化
基于MVO多元宇宙优化的DBSCAN聚类算法matlab仿真
本程序基于MATLAB实现MVO优化的DBSCAN聚类算法,通过多元宇宙优化自动搜索最优参数Eps与MinPts,提升聚类精度。对比传统DBSCAN,MVO-DBSCAN有效克服参数依赖问题,适应复杂数据分布,增强鲁棒性,适用于非均匀密度数据集的高效聚类分析。
|
7月前
|
人工智能 关系型数据库 分布式数据库
让数据与AI贴得更近,阿里云瑶池数据库系列产品焕新升级
4月9日阿里云AI势能大会上,阿里云瑶池数据库发布重磅新品及一系列产品能力升级。「推理加速服务」Tair KVCache全新上线,实现KVCache动态分层存储,显著提高内存资源利用率,为大模型推理降本提速。
|
机器学习/深度学习 算法 PyTorch
Stable Diffusion 介绍与入门
Stable Diffusion 介绍与入门,简单的介绍
2155 2
Stable Diffusion 介绍与入门
|
机器学习/深度学习 计算机视觉
人脸关键点
【6月更文挑战第20天】
468 5
|
算法 JavaScript 决策智能
基于禁忌搜索算法的TSP路径规划matlab仿真
**摘要:** 使用禁忌搜索算法解决旅行商问题(TSP),在MATLAB2022a中实现路径规划,显示优化曲线与路线图。TSP寻找最短城市访问路径,算法通过避免局部最优,利用禁忌列表不断调整顺序。关键步骤包括初始路径选择、邻域搜索、解评估、选择及禁忌列表更新。过程示意图展示搜索效果。
|
机器学习/深度学习 人工智能 算法
Diffusion 和Stable Diffusion的数学和工作原理详细解释
扩散模型的兴起可以被视为人工智能生成艺术领域最近取得突破的主要因素。而稳定扩散模型的发展使得我们可以通过一个文本提示轻松地创建美妙的艺术插图。所以在本文中,我将解释它们是如何工作的。
3744 2
Diffusion 和Stable Diffusion的数学和工作原理详细解释
|
机器学习/深度学习 人工智能 编解码
AI绘画提示词创作指南:DALL·E 2、Midjourney和 Stable Diffusion最全大比拼 ⛵
随着Diffusion Model的普及,AI绘画只需要你输入文本描述,模型就能在几分钟内生成精准匹配的精美图像。本文从使用步骤、费用和商用等角度对3个主流平台进行比较:DALL·E2、Midjourney、Stable Diffusion。
2904 2
AI绘画提示词创作指南:DALL·E 2、Midjourney和 Stable Diffusion最全大比拼 ⛵
|
JavaScript
VSCode 开发 Vue 语法提示
VSCode 开发 Vue 语法提示
311 0