当前位置: 首页 > news >正文

分布式锁的特点

在分布式系统中,“并发安全”是绕不开的坎——多个服务节点同时操作同一资源时,很容易出现数据错乱。比如电商系统的库存扣减,若不加以控制,就可能出现超卖;分布式任务调度时,若多个节点同时执行同一任务,会造成资源浪费。

而分布式锁,就是解决这类问题的“钥匙”。它能像单机系统中的锁一样,保证在分布式环境下,同一时刻只有一个节点能操作临界资源。但分布式锁的实现远比单机锁复杂,既要保证互斥性,还要应对节点崩溃、网络中断等异常场景。

今天这篇文章,就从“核心特性-实现方式-使用场景-问题解决”四个维度,把分布式锁讲透。重点解析数据库、Redis、ZooKeeper三种主流实现方案的优劣,以及生产环境中的避坑技巧。

一、先搞懂:分布式锁的3个核心特性

不是随便一个“加锁逻辑”都能叫分布式锁,它必须满足三个核心特性,这也是面试中常考的基础考点:

1.1 互斥性(最核心)

这是锁的本质功能:同一时刻,只有一个客户端能持有锁。比如两个服务节点同时请求扣减同一商品的库存,只有一个节点能成功获取锁并执行操作,另一个必须等待。

这里要注意“客户端”是广义的,可能是一个服务实例、一个线程,具体范围可根据业务场景定义,但核心是“同一资源同一时间仅被一个操作者占用”。

1.2 容错性(避坑关键)

分布式系统中,节点崩溃、网络中断是常态,分布式锁必须能应对这些异常:即使持有锁的客户端崩溃或失去连接,锁也要能被正常释放,避免死锁

比如某节点获取锁后突然宕机,若锁无法释放,其他节点将永远无法获取该锁,导致业务阻塞。这也是分布式锁和单机锁最大的区别——单机锁依赖进程内资源,而分布式锁要应对跨节点的异常。

1.3 高可用性(生产必备)

锁服务本身不能成为“单点故障”:负责管理锁的服务(如Redis、ZooKeeper)必须高可用,即使部分节点故障,锁服务仍能正常工作

比如用单节点Redis实现分布式锁,若Redis节点宕机,所有依赖该锁的业务都会瘫痪。所以生产环境中,锁服务的部署必须考虑集群容错。

二、三大实现方式深度解析:优劣对比+实战代码

分布式锁的实现方式有很多,其中数据库、Redis、ZooKeeper是最主流的三种。它们各有优劣,适用不同的业务场景,下面逐个拆解。

2.1 方式一:基于数据库的分布式锁(最原始,少用)

数据库实现分布式锁的核心思路是“利用数据库的行级锁或唯一约束”,强制同一时刻只有一个客户端能操作特定资源。最常见的有两种方案:SELECT ... FOR UPDATE行锁和唯一索引约束。

方案1:SELECT ... FOR UPDATE行锁

先创建一张锁表,存储资源标识和持有锁的客户端信息,然后通过SELECT ... FOR UPDATE语句获取行锁,实现互斥。

  1. 创建锁表
 -- 锁表:resource字段存储资源唯一标识,holder存储持有锁的客户端ID
CREATE TABLE distributed_lock (id bigint NOT NULL AUTO_INCREMENT,resource varchar(64) NOT NULL COMMENT '资源标识',holder varchar(64) NOT NULL COMMENT '持有锁的客户端ID',create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,PRIMARY KEY (id),UNIQUE KEY uk_resource (resource) COMMENT '资源唯一约束'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
  1. 获取锁
    通过SELECT ... FOR UPDATE锁定指定资源的行,若该资源已被其他客户端锁定,当前请求会阻塞等待:
// 1. 先插入资源记录(若不存在),避免死锁
String insertSql = "INSERT IGNORE INTO distributed_lock (resource, holder) VALUES (?, ?)";
jdbcTemplate.update(insertSql, "product:stock:1001", "client_123");
// 2. 获取行锁:FOR UPDATE会锁定该资源对应的行
String lockSql = "SELECT * FROM distributed_lock WHERE resource = ? FOR UPDATE";
jdbcTemplate.query(lockSql, new Object[]{"product:stock:1001"}, rs -> {// 锁获取成功,执行业务逻辑return null;
});

  1. 释放锁
    业务执行完成后,删除对应的锁记录(或更新holder为空):
String unlockSql = "DELETE FROM distributed_lock WHERE resource = ? AND holder = ?";
jdbcTemplate.update(unlockSql, "product:stock:1001", "client_123");

方案2:唯一索引约束

利用唯一索引的“唯一性”特性:插入资源标识对应的记录,若插入成功,说明获取锁;若因唯一约束报错,说明锁已被其他客户端持有。


// 获取锁:插入成功即获锁,失败则锁已被持有
try {String insertSql = "INSERT INTO distributed_lock (resource, holder) VALUES (?, ?)";jdbcTemplate.update(insertSql, "product:stock:1001", "client_123");// 锁获取成功
} catch (DuplicateKeyException e) {// 锁已被其他客户端持有,获取失败throw new RuntimeException("获取锁失败");
}// 释放锁:删除记录
String unlockSql = "DELETE FROM distributed_lock WHERE resource = ? AND holder = ?";
jdbcTemplate.update(unlockSql, "product:stock:1001", "client_123");
数据库锁的优劣与适用场景

优势

劣势

适用场景

实现简单,无需额外中间件

性能低:高并发下数据库压力大,行锁会导致大量阻塞

并发量低、业务简单的场景,或没有Redis/ZooKeeper的小型系统

依赖数据库原有高可用方案(如主从复制)

容错性差:客户端崩溃未释放锁时,需额外定时任务清理

非核心业务,对性能要求不高的场景

2.2 方式二:基于Redis的分布式锁(最主流,推荐)

Redis凭借其高性能、原子操作特性,成为分布式锁的首选方案。核心思路是“利用SET命令的NX(不存在才设置)特性实现互斥,配合过期时间避免死锁”。

注意:不能用“SETNX + EXPIRE”两步操作,因为两步操作不是原子的,若SETNX成功后客户端崩溃,EXPIRE未执行,会导致锁永远无法释放。必须用Redis 2.6.12+支持的“SET NX EX”原子命令。

基础实现:SET NX EX原子命令
  1. 获取锁
    使用SET命令,同时指定NX(不存在才设置)、EX(过期时间)和锁值(用于释放锁时校验):
import redis.clients.jedis.Jedis;public class RedisLock {private static final String LOCK_KEY_PREFIX = "distributed:lock:";private static final int LOCK_EXPIRE = 30; // 锁默认过期时间30秒private Jedis jedis;
// 构造方法注入Jedis客户端
public RedisLock(Jedis jedis) {this.jedis = jedis;
}// 获取锁:返回锁值(用于释放锁校验),获取失败返回null
public String tryLock(String resource) {String lockKey = LOCK_KEY_PREFIX + resource;String lockValue = java.util.UUID.randomUUID().toString(); // 唯一锁值,避免误删其他客户端的锁// 原子操作:NX(不存在才设置)、EX(过期时间30秒)String result = jedis.set(lockKey, lockValue, "NX", "PX", LOCK_EXPIRE * 1000);// 返回OK说明获取锁成功,返回锁值用于后续释放return "OK".equals(result) ? lockValue : null;
}

}`

  1. 释放锁
    释放锁时必须校验锁值,避免误删其他客户端的锁。同时要保证“校验+删除”是原子操作,需用Lua脚本实现:
//释放锁:传入资源标识和获取锁时的锁值
public boolean unlock(String resource, String lockValue) {if (lockValue == null) {return false;}String lockKey = LOCK_KEY_PREFIX + resource;// Lua脚本:先校验锁值是否匹配,匹配则删除(原子操作)String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Long result = (Long) jedis.eval(luaScript, 1, lockKey, lockValue);
return result == 1;
}`
进阶优化:引入看门狗机制(解决锁过期问题)

基础实现有个隐患:若业务执行时间超过锁的过期时间,锁会被自动释放,导致并发安全问题。比如锁过期时间30秒,业务执行了40秒,锁释放后其他客户端会获取到锁。

解决方案就是看门狗机制(Redisson已封装完善):客户端获取锁后,启动一个后台线程,每隔锁过期时间的1/3(如10秒)检查一次,若锁仍被持有,则延长锁的过期时间。

用Redisson实现带看门狗的Redis锁(推荐生产使用):


import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;public class RedissonLockDemo {private RedissonClient redissonClient;public RedissonLockDemo(RedissonClient redissonClient) {this.redissonClient = redissonClient;}public void doBusinessWithLock(String resource) {// 1. 获取锁:默认启用看门狗,过期时间30秒,自动续期RLock lock = redissonClient.getLock("distributed:lock:" + resource);try {// 2. 加锁:阻塞等待,直到获取到锁lock.lock();// 3. 执行业务逻辑(即使执行超过30秒,看门狗会自动续期)System.out.println("获取锁成功,执行库存扣减等业务...");Thread.sleep(40000); // 模拟耗时业务} catch (InterruptedException e) {e.printStackTrace();} finally {// 4. 释放锁:必须在finally中执行if (lock.isHeldByCurrentThread()) {lock.unlock();}}}
}
Redis锁的优劣与适用场景

优势

劣势

适用场景

性能极高:Redis是内存数据库,加锁解锁耗时微秒级

主从复制有延迟:主节点锁未同步到从节点时主节点宕机,可能出现锁丢失

高并发场景:如秒杀、库存扣减、分布式限流等

实现简单,Redisson封装完善,开箱即用

需处理锁过期、看门狗续期等细节

对性能要求高,允许短暂“锁丢失”风险的场景(可通过Redis集群优化)

2.3 方式三:基于ZooKeeper的分布式锁(最可靠,略重)

ZooKeeper是分布式协调服务,其“临时顺序节点”特性天然适合实现分布式锁。核心思路是“通过创建临时顺序节点竞争锁,最小序号节点获取锁,其他节点监听前序节点”。

核心原理
  1. 创建临时顺序节点:客户端在ZooKeeper的/locks节点下,为目标资源创建临时顺序节点,如/locks/product:stock:1001/lock-00000001;
  2. 竞争锁:获取/locks/product:stock:1001下的所有子节点,排序后若自己是最小序号节点,则获取锁成功;
  3. 监听前序节点:若不是最小节点,则监听前一个节点(如自己是lock-00000003,监听lock-00000002),前序节点释放锁时会触发通知;
  4. 释放锁:业务执行完成后,删除自己创建的临时节点(客户端崩溃时,临时节点会自动删除,避免死锁)。
实战实现:用Curator客户端(推荐)

ZooKeeper原生API实现锁较复杂,推荐使用Apache Curator客户端,它已封装好分布式锁的实现(InterProcessMutex)。

  1. 引入依赖
<dependency><groupId>org.apache.curator</groupId><artifactId>curator-recipes</artifactId><version>5.5.0</version>
</dependency>

  1. 实现分布式锁
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import java.util.concurrent.TimeUnit;public class ZkLockDemo {private static final String LOCK_PATH = "/distributed/locks/";private CuratorFramework curatorFramework;
public ZkLockDemo(CuratorFramework curatorFramework) {this.curatorFramework = curatorFramework;
}public void doBusinessWithLock(String resource) {// 1. 创建分布式锁实例InterProcessMutex lock = new InterProcessMutex(curatorFramework, LOCK_PATH + resource);try {// 2. 获取锁:最多等待10秒,获取成功返回trueif (lock.acquire(10, TimeUnit.SECONDS)) {// 3. 执行业务逻辑System.out.println("获取锁成功,执行分布式任务...");Thread.sleep(10000);} else {throw new RuntimeException("获取锁失败,超时");}} catch (Exception e) {e.printStackTrace();} finally {// 4. 释放锁try {if (lock.isAcquiredInThisProcess()) {lock.release();}} catch (Exception e) {e.printStackTrace();}}
}

}`

ZooKeeper锁的优劣与适用场景

优势

劣势

适用场景

可靠性高:临时节点天然支持崩溃自动释放锁,监听机制确保锁高效竞争

性能中等:ZooKeeper是CP系统,节点同步有开销,加锁解锁耗时毫秒级

对可靠性要求极高的场景:如分布式事务、分布式一致性校验

无锁过期问题:无需手动设置过期时间,避免业务超时导致的锁释放问题

部署复杂:需维护ZooKeeper集群,运维成本高

并发量适中,不追求极致性能,但要求绝对可靠的场景

三、分布式锁的3大核心使用场景

了解了实现方式后,还要明确哪些场景需要用分布式锁。以下三个场景是分布式系统中的高频需求:

3.1 分布式事务:保证跨节点操作的一致性

在分布式事务中,若多个节点需要操作同一资源,需用分布式锁保证“要么都执行,要么都不执行”。比如跨库转账:A库扣减金额和B库增加金额,需用锁锁定转账订单,避免重复转账。

3.2 资源共享:控制并发访问频率

当多个节点共享有限资源时,需用分布式锁控制访问频率。比如:

  • 分布式限流:限制某接口的总并发请求数,用锁控制计数器的增减;
  • 库存扣减:秒杀场景中,多个节点同时扣减同一商品库存,用锁保证库存不超卖。

3.3 任务调度:避免重复执行

分布式任务调度系统中,同一任务可能部署在多个节点,需用分布式锁保证“同一时间只有一个节点执行任务”。比如定时清理日志的任务,若多个节点同时执行,会造成资源浪费。

四、分布式锁的4大常见问题与解决方案

分布式锁的实现和使用中,很容易踩坑。以下四个问题是开发和面试中的高频考点,必须掌握:

4.1 死锁:客户端崩溃导致锁无法释放

问题原因:客户端获取锁后崩溃,未执行释放锁操作,导致锁永远被持有。

解决方案
Redis锁:设置锁过期时间,配合看门狗机制;

ZooKeeper锁:使用临时节点,客户端崩溃后节点自动删除;

数据库锁:定时任务清理超时未释放的锁(根据create_time字段)。

4.2 锁竞争:高并发下锁等待时间过长

问题原因:高并发场景下,大量客户端等待同一把锁,导致响应延迟。

解决方案
锁粒度拆分:将粗粒度锁拆分为细粒度锁,比如将“商品库存锁”拆分为“单品库存锁”;

非阻塞锁:使用tryLock方法,获取不到锁时直接返回,避免阻塞等待;

排队机制:结合ZooKeeper的顺序节点,实现公平锁,避免饥饿问题。

4.3 锁丢失:主从复制延迟导致的问题

问题原因:Redis主从架构中,主节点获取锁后未同步到从节点,主节点宕机,从节点升级为主节点,其他客户端可重新获取锁,导致锁丢失。

解决方案
使用Redis集群的Redlock算法:向多个独立的Redis节点请求锁,只有超过半数节点获取成功,才认为锁获取成功;

优先使用ZooKeeper锁:ZooKeeper的CP特性确保主从节点数据一致,避免锁丢失。

4.4 误删锁:释放其他客户端的锁

问题原因:客户端A获取锁后执行超时,锁被自动释放,客户端B获取到锁;此时客户端A执行完业务,误删了客户端B的锁。

解决方案
锁值校验:释放锁时校验锁值是否为自己获取锁时的唯一值(如Redis锁的UUID值);

原子操作:用Lua脚本实现“校验+释放”的原子操作,避免两步操作的间隙出现异常。

五、总结

分布式锁的核心价值是“在分布式环境下保证资源的互斥访问”,其实现方式没有绝对的优劣,只有“是否适配业务场景”。选择时需权衡三个维度:性能要求、可靠性要求、运维成本

最后给出一个简单的选择指南:

  • 高并发、高性能、能接受轻微锁丢失风险:Redis锁(Redisson);
  • 高可靠、并发适中、能接受运维成本:ZooKeeper锁(Curator);
  • 小型系统、快速落地、低并发:数据库锁。
http://www.dtcms.com/a/570423.html

相关文章:

  • 网站制作价格多少钱wordpress带会员
  • 加速度计如何助力大型无人机飞得更稳、更准、更智能?
  • 网站动画效果用什么程序做的装修设计软件 知乎
  • 光刻胶分类与特性:正性胶和负性胶以及SU-8厚胶和AZ 1500 系列光刻胶(上)
  • 网站建设能挣钱网站正在建设中php
  • 苏州企业网站制作报价山西响应式网站哪家好
  • 33-蓝桥杯报名通知
  • 基于视觉分析的人脸联动使用手机检测系统 智能安全管理新突破 人脸与手机行为联动检测 多模态融合人脸与手机行为分析模型
  • 高青外贸公司网站建设怎么建设游网站主页
  • 财务部官方网站经济建设司网站建设做网站多少钱
  • 塑料机械怎么做网站模板之家如何免费下载
  • 济南正宗网站建设平台网站板块策划
  • Java设计模式精讲---01工厂方法模式
  • Nacos 综合漏洞利用工具 | 更新V3.0.5
  • 西宁网站建设排名花店如何做推广
  • 建博客网站重庆移动网站建设
  • 禅城网站建设联系电话电商平台系统开发
  • 知名做网站费用制作网页可以用
  • 给城市建设提议献策的网站wordpress logo更换
  • 商户查询更新缓存(opsForHash、opsForList、ObjectMapper、@Transactional、@PutMapping、装箱拆箱、线程池)
  • 做网站用dw的多吗武山县建设局网站
  • FPGA—ZYNQ学习GPIO-EMIO,MIO,AXIGPIO(五)
  • 移动端网站和app区别2021年给我一个网站
  • 记录CANOE启动报错“TimeService failed to reset all device clocks...”的问题解决过程
  • 我看别人做系统就直接网站下载深圳市专业制作网站公司吗
  • 可以做网站挂在百度上吗盐城滨海建设局网站
  • 网站建设年终总结怎么把自己的网站做自适应网站
  • 河东区建设局网站深圳专业网站设计公司哪家好
  • 网站建设哈尔滨app开发2php快速建网站
  • 网站开发三步自己写的网页怎么发布到网上