写点什么

大数据 -50 Redis Java Lua 实现乐观锁、WATCH 机制与 SETNX 分布式锁

作者:武子康
  • 2025-07-24
    山东
  • 本文字数:5470 字

    阅读完需:约 18 分钟

大数据-50 Redis Java Lua实现乐观锁、WATCH机制与SETNX分布式锁

点一下关注吧!!!非常感谢!!持续更新!!!

🚀 AI 篇持续更新中!(长期更新)

AI 炼丹日志-30-新发布【1T 万亿】参数量大模型!Kimi‑K2 开源大模型解读与实践,持续打造实用 AI 工具指南!📐🤖

💻 Java 篇正式开启!(300 篇)

目前 2025 年 07 月 21 日更新到:


Java-77 深入浅出 RPC Dubbo 负载均衡全解析:策略、配置与自定义实现实战 MyBatis 已完结,Spring 已完结,Nginx 已完结,Tomcat 已完结,分布式服务正在更新!深入浅出助你打牢基础!

📊 大数据板块已完成多项干货更新(300 篇):

包括 Hadoop、Hive、Kafka、Flink、ClickHouse、Elasticsearch 等二十余项核心组件,覆盖离线+实时数仓全栈!大数据-278 Spark MLib - 基础介绍 机器学习算法 梯度提升树 GBDT 案例 详解


章节内容

上节我们完成了:


  • Redis 缓存相关的概念

  • 缓存穿透、缓存击穿、数据不一致性等

  • HotKey、BigKey 等问题

  • 针对上述问题提出一些解决方案


乐观锁深入解析

基本原理

乐观锁是一种并发控制机制,其核心思想是 CAS(Compare And Swap,比较并交换)。这种锁机制假设多个事务或线程在大多数情况下不会产生冲突,因此不需要加锁,而是通过版本控制来实现并发控制。

实现特点

  1. 非互斥性:乐观锁不会阻塞其他线程的访问,各线程可以同时读取数据

  2. 无锁等待:避免了传统锁机制中线程排队等待锁释放的资源消耗

  3. 重试机制:当检测到数据被修改时,操作会失败并需要重试,这可能导致一定性能开销

典型工作流程

  1. 读取数据时记录版本号或时间戳

  2. 修改数据前再次检查版本号是否变化

  3. 如果版本一致则提交修改并更新版本

  4. 如果版本不一致则丢弃当前修改并重试

应用场景

  • 高并发读操作:系统读取远多于写入时特别适用

  • 分布式系统:减少跨节点锁带来的性能问题

  • 电商库存管理:多个用户同时抢购同一商品时

  • 版本控制系统:如 Git 的合并冲突处理

优劣分析

优势


  • 响应速度快,适合低冲突场景

  • 不会导致死锁问题

  • 系统吞吐量通常较高


劣势


  • 冲突率高时重试开销大

  • 需要应用程序处理重试逻辑

  • 不保证操作一定成功

技术实现示例

在 Java 中,AtomicInteger 等原子类就是基于 CAS 实现的乐观锁机制:


AtomicInteger counter = new AtomicInteger(0);counter.compareAndSet(expectedValue, newValue);  // 典型的CAS操作
复制代码


在数据库层面,可以通过版本号字段实现乐观锁:


UPDATE products SET stock = stock - 1, version = version + 1 WHERE id = 123 AND version = 5  -- 确保版本未变
复制代码

Watch 实现

Redis 乐观锁实现详解

Redis 的 WATCH 命令是实现乐观锁的关键机制,它允许我们在执行事务前监视一个或多个键,如果在事务执行期间这些键的值被修改,整个事务就会被取消。以下是更为详细的实现步骤和说明:


  1. 监控阶段

  2. 使用 WATCH 命令监控指定的键(如:WATCH my_counter

  3. 这个键可以是一个计数器、库存量或任何需要原子性更新的值

  4. 监控会持续到事务执行完成或取消

  5. 获取当前值

  6. 通过 GET 命令获取被监控键的当前值(如:GET my_counter

  7. 将这个值存储在客户端本地以备后续计算使用

  8. 事务准备阶段

  9. 使用 MULTI 命令开始一个事务块

  10. 在事务中执行修改操作(如:INCR my_counterSET my_counter new_value

  11. 可以包含多个操作命令,它们将作为一个原子单元执行

  12. 事务执行阶段

  13. 使用 EXEC 命令执行事务

  14. 如果在 WATCHEXEC 之间键的值未被修改,事务会成功执行

  15. 如果键的值被其他客户端修改,事务会返回 (nil) 表示执行失败

  16. 失败处理

  17. 当事务失败时,应该重新尝试整个流程(从 WATCH 开始)

  18. 通常需要设置最大重试次数以避免无限循环


应用场景举例


  • 电商系统中的库存扣减

  • 分布式环境下的计数器

  • 抢购系统中的商品限量购买


代码示例(伪代码):


WATCH inventorycurrent = GET inventoryif current > 0:    MULTI    DECR inventory    EXECelse:    UNWATCH
复制代码

wacth 实现

暂时就先忽略编码规范的内容,就先实现即可。具体编写逻辑如下:


public class Test02 {
public static void main(String[] args) { String redisKey = "lock"; ExecutorService executor = Executors.newFixedThreadPool(20); try { Jedis jedis = new Jedis("h121.wzk.icu", 6379); jedis.del(redisKey); jedis.set(redisKey, "0"); jedis.close(); } catch (Exception e) { e.printStackTrace(); }
for (int i = 0; i < 300; i ++) { executor.execute(() -> { Jedis jedis = null; try { jedis = new Jedis("h121.wzk.icu", 6379); jedis.watch(redisKey); String redisValue = jedis.get(redisKey); int value = Integer.valueOf(redisValue); String userInfo = UUID.randomUUID().toString(); if (value < 20) { Transaction tx = jedis.multi(); tx.incr(redisKey); List<Object> list = tx.exec(); if (list != null && !list.isEmpty()) { System.out.println("获取锁成功, 用户信息: " + userInfo + " 成功人数: " + (value + 1)); } } else { System.out.println("秒杀结束!"); } } catch (Exception e) { e.printStackTrace(); } finally { if (null != jedis) { jedis.close(); }
} }); } executor.shutdown(); }
}
复制代码


运行之后,会看到已经在进行争抢了:


获取锁成功, 用户信息: e6e06770-f274-4d89-8369-65babc2e3073 成功人数: 1获取锁成功, 用户信息: 2cc2803b-085e-47ee-9fe6-4bbe1f694fd5 成功人数: 2获取锁成功, 用户信息: 525ad22c-abb2-4f94-868a-cca981f9d768 成功人数: 3获取锁成功, 用户信息: 9af67396-798e-4e09-b524-6ddc5e1673ec 成功人数: 4···省略秒杀结束!获取锁成功, 用户信息: dba287f8-65f0-4da8-a131-05304164b3aa 成功人数: 18秒杀结束!获取锁成功, 用户信息: 05c5c5f9-f9cd-48b3-a266-c4ff3f256814 成功人数: 20秒杀结束!
复制代码

SETNX

setnx 详细介绍

基本概念

setnx(SET if Not eXists)是 Redis 提供的一个原子性操作命令,用于实现分布式锁。当且仅当 key 不存在时,将 key 的值设为 value;若 key 已经存在,则不做任何操作。

应用场景

1. 共享资源互斥

在分布式系统中,当多个进程/服务需要互斥地访问某个共享资源时(如数据库中的某条记录、文件系统中的某个文件等),可以使用 setnx 实现互斥访问。


示例场景:


  • 电商系统中的库存扣减

  • 秒杀系统中的商品抢购

  • 支付系统的订单处理

2. 共享资源串行化

当需要对共享资源的访问进行有序控制时,setnx 可以确保同一时刻只有一个客户端能够操作该资源,其他客户端必须等待。

3. 单应用中的锁

在单进程多线程环境下,可以使用:


  • synchronized(Java 内置锁)

  • ReentrantLock(可重入锁)


但在分布式环境下,这些单机锁机制无法满足需求。

4. 分布式应用中的锁

在分布式系统中,由于涉及多个进程(可能部署在不同机器上),需要使用分布式锁来解决:


  • 多进程之间的同步问题

  • 多线程之间的同步问题

实现原理

Redis 的 setnx 命令特别适合实现分布式锁,这是因为:


  1. Redis 的单线程特性保证了命令执行的原子性

  2. setnx 操作本身是原子的,不存在竞态条件

  3. 通过设置 key 的过期时间可以避免死锁

典型实现方式

  1. 获取锁:SETNX lock_key unique_value

  2. 设置过期时间:EXPIRE lock_key timeout

  3. 释放锁:通过 Lua 脚本保证原子性删除


注意事项:


  • 需要确保设置值和设置过期时间是原子操作

  • 每个客户端应该使用唯一的 value 来标识自己

  • 需要合理设置锁的超时时间

与其他方案的对比

相比 Zookeeper 等分布式协调服务实现的分布式锁,基于 Redis 的 setnx 实现:


  • 优点:性能更高,实现简单

  • 缺点:可靠性略低,需要处理锁续期等问题

SETNX 实现

获取锁方式 1 SET

public boolean getLock(String lockKey,String requestId,int expireTime) {    // NX:保证互斥性    // hset 原子性操作 只要lockKey有效 则说明有进程在使用分布式锁    String result = jedis.set(lockKey, requestId, "NX", "EX", expireTime);    if("OK".equals(result)) {        return true;    }    return false;}
复制代码

获取锁方式 2 SETNX

public boolean getLock(String lockKey,String requestId,int expireTime) {    Long result = jedis.setnx(lockKey, requestId);    if(result == 1) {        // 成功设置 进程down 永久有效 别的进程就无法获得锁        jedis.expire(lockKey, expireTime);        return true;    }    return false;}
复制代码

释放锁方式 1 del

注意,当调用 del 方法时候,如果这把锁已经不属于当前客户端了,比如已经过期了,而别的人拿到了这把锁,此时删除就会导致释放掉了别人的锁。


public static void releaseLock(String lockKey,String requestId) {    if (requestId.equals(jedis.get(lockKey))) {        jedis.del(lockKey);    }}
复制代码

释放锁方式 2 lua

public static boolean releaseLock(String lockKey, String requestId) {    String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return    redis.call('del', KEYS[1]) else return 0 end";    Object result = jedis.eval(script, Collections.singletonList(lockKey),    Collections.singletonList(requestId));    if (result.equals(1L)) {        return true;    }    return false;}
复制代码

Redisson 分布式锁

Redisson 介绍

Redisson 是一个基于 Redis 实现的 Java 驻内存数据网格(In-Memory Data Grid)框架。它提供了丰富的分布式 Java 对象和服务,包括分布式集合、分布式锁、分布式服务等高级功能,使开发者能够轻松构建分布式系统。

核心特性

  • 基于 Redis 实现:Redisson 完全兼容 Redis 协议,可以直接连接 Redis 服务器

  • 分布式服务:提供了分布式锁、分布式集合、分布式原子变量等常用分布式服务

  • 高性能:基于 NIO 的 Netty 框架实现,具有优秀的网络通信性能

  • 支持多种 Redis 部署模式:包括单节点、哨兵、集群等多种部署方式

技术架构

Redisson 采用 Netty 作为底层通信框架,通过异步非阻塞 I/O 实现高性能的网络通信。其核心架构包括:


  1. 连接管理器:管理 Redis 连接池

  2. 命令执行器:处理 Redis 命令的发送和响应

  3. 编解码器:负责数据的序列化和反序列化

  4. 监听器:处理 Redis 的订阅/发布消息

典型应用场景

  1. 分布式锁:解决分布式环境下的资源竞争问题

  2. 分布式集合:如分布式 Map、Set、List 等

  3. 分布式限流:控制系统的访问频率

  4. 分布式发布订阅:实现跨服务的消息通信


// 使用示例Config config = new Config();config.useSingleServer().setAddress("redis://127.0.0.1:6379");RedissonClient redisson = Redisson.create(config);
// 获取分布式锁RLock lock = redisson.getLock("myLock");try { lock.lock(); // 业务逻辑处理} finally { lock.unlock();}
复制代码


Redisson 通过丰富的 API 和稳定的性能,成为 Java 开发者在分布式系统中常用的工具库之一。

添加依赖

<dependency>  <groupId>org.redisson</groupId>  <artifactId>redisson</artifactId>  <version>2.7.0</version></dependency>
复制代码

配置 Redisson

public class RedissonManager {
private static final Config CONFIG = new Config();
private static Redisson redisson = null;
static { CONFIG .useClusterServers() .setScanInterval(2000) .addNodeAddress("redis://h121.wzk.icu:6379") .addNodeAddress("redis://h122.wzk.icu:6379") .addNodeAddress("redis://h123.wzk.icu:6379"); redisson = (Redisson) Redisson.create(CONFIG); }
public static Redisson getRedisson() { return redisson; }
}
复制代码

获取与释放锁

public class DistributedRedisLock {
private static Redisson redisson = RedissonManager.getRedisson();
private static final String LOCK_TITLE = "redisLock_";
public static boolean acquire(String lockName) { String key = LOCK_TITLE + lockName; RLock rLock = redisson.getLock(key); rLock.lock(3, TimeUnit.SECONDS); return true; }
public static void release(String lockName) { String key = LOCK_TITLE + lockName; RLock rLock = redisson.getLock(key); rLock.unlock(); }
}
复制代码

业务使用

public String discount() throws IOException{    String key = "lock001";    // 加锁    DistributedRedisLock.acquire(key);    // 执行具体业务逻辑    dosoming    // 释放锁    DistributedRedisLock.release(key);    // 返回结果    return soming;}
复制代码

实现原理

分布式锁特性

  • 互斥性:任意时刻,只能有一个客户端获取锁,不能同时有两个客户端获取到锁。

  • 同一性:锁只能被持有该锁客户端删除,不能由其他客户端删除

  • 可重入性:持有某个客户端可持续对该锁加锁 实现锁的续租

  • 容错性:超过生命周期会自动进行释放,其他客户端可以获取到锁

常见分布式锁对比


发布于: 刚刚阅读数: 4
用户头像

武子康

关注

永远好奇 无限进步 2019-04-14 加入

Hi, I'm Zikang,好奇心驱动的探索者 | INTJ / INFJ 我热爱探索一切值得深究的事物。对技术、成长、效率、认知、人生有着持续的好奇心和行动力。 坚信「飞轮效应」,相信每一次微小的积累,终将带来深远的改变。

评论

发布
暂无评论
大数据-50 Redis Java Lua实现乐观锁、WATCH机制与SETNX分布式锁_Java_武子康_InfoQ写作社区