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

168 阅读10分钟

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

🚀 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. 监控阶段

    • 使用 WATCH 命令监控指定的键(如:WATCH my_counter
    • 这个键可以是一个计数器、库存量或任何需要原子性更新的值
    • 监控会持续到事务执行完成或取消
  2. 获取当前值

    • 通过 GET 命令获取被监控键的当前值(如:GET my_counter
    • 将这个值存储在客户端本地以备后续计算使用
  3. 事务准备阶段

    • 使用 MULTI 命令开始一个事务块
    • 在事务中执行修改操作(如:INCR my_counterSET my_counter new_value
    • 可以包含多个操作命令,它们将作为一个原子单元执行
  4. 事务执行阶段

    • 使用 EXEC 命令执行事务
    • 如果在 WATCHEXEC 之间键的值未被修改,事务会成功执行
    • 如果键的值被其他客户端修改,事务会返回 (nil) 表示执行失败
  5. 失败处理

    • 当事务失败时,应该重新尝试整个流程(从 WATCH 开始)
    • 通常需要设置最大重试次数以避免无限循环

应用场景举例

  • 电商系统中的库存扣减
  • 分布式环境下的计数器
  • 抢购系统中的商品限量购买

代码示例(伪代码):

WATCH inventory
current = GET inventory
if current > 0:
    MULTI
    DECR inventory
    EXEC
else:
    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;
}

实现原理

在这里插入图片描述

分布式锁特性

  • 互斥性:任意时刻,只能有一个客户端获取锁,不能同时有两个客户端获取到锁。
  • 同一性:锁只能被持有该锁客户端删除,不能由其他客户端删除
  • 可重入性:持有某个客户端可持续对该锁加锁 实现锁的续租
  • 容错性:超过生命周期会自动进行释放,其他客户端可以获取到锁

常见分布式锁对比

在这里插入图片描述