【redis】redis实现分布式锁

在分布式系统中,协调多个节点对共享资源的访问是一个经典难题。分布式锁作为解决这类问题的关键组件,需要满足互斥性容错性超时释放等核心特性。

本文基于Redis的原子操作特性,详细讲解如何用Java实现企业级分布式锁。

关键原理解析

原子加锁

怎么样才算加锁成功呢?有下面两种方案:

  1. 使用setnx命令,key不存在时设置成功,否则失败,谁设置key成功,谁就获得锁。

  2. 使用set命令并带上nx选项,效果与上面一样。

避免死锁

如果持有锁的客户端挂了,那么这个锁就会一直被占有而得不到释放,造成死锁,怎么办?

可以为key设置一个超时时间,如果客户端加锁后就挂了,那么这个key到时间就会被删除,不会造成死锁。

  1. 使用setnx命令
shell 复制代码
setnx key value
expire key 10

这种方案会由两条命令来执行,有可能setnx命令执行成功而expire命令执行失败,无法保证原子性操作,还是可能会导致死锁。

  1. 使用set命令并带上nx、ex选项
shell 复制代码
set key value nx ex 10

这种方案只使用了一条命令,能够保证原子性,不会造成死锁。

安全解锁

为什么释放锁的时候不是直接发送del key命令?

可能存在以下场景:

  • 线程A获取锁,因GC暂停或其他原因导致锁过期

  • 线程B获得锁,线程A恢复后误删线程B的锁

也可能由于程序的bug,导致线程A加的锁被进程B释放,所以释放锁的时候需要校验value值,避免进程A加的锁被其他进程释放,所以value值的设置也是有讲究的,这个值只有线程A知道,这样释放的时候需要检验这个value,只有线程A知道这个正确的value才能删除这个key。

所以释放锁的时候需要分为两步:第一步校验锁的值,第二步删除锁。这就需要通过Lua脚本来保证这两步的原子性,具体lua脚本如下:

lua 复制代码
if redis.call('get', KEYS[1]) == ARGV[1] 
then
  return redis.call('del', KEYS[1]) "
else 
  return 0 
end

锁续期机制

假如锁在超时时间内,业务还没处理完,key快要过期了怎么办?

可以通过启动一个后台守护线程(也叫看门狗)定时延长锁过期时间(续命),解决业务操作超时问题,让业务逻辑执行完成,避免key过期让其他线程抢到锁。

为什么要启动一个守护线程来为key延时,而不是非守护线程?因为守护线程会随创建它的线程的关闭而自动销毁,无需手动关闭。

Jedis实现分布式锁

使用Jedis实现分布式锁:

java 复制代码
package com.morris.redis.demo.lock;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.params.SetParams;

import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * 使用jedis实现分布式锁
 */
public class JedisLock {

    public static final int EXPIRE_TIME = 30;

    private final JedisPool jedisPool;

    private final String lockKey;

    private final String lockValue;

    private Thread watchDogThread;

    public JedisLock(JedisPool jedisPool, String lockKey) {
        this.jedisPool = jedisPool;
        this.lockKey = lockKey;
        this.lockValue = UUID.randomUUID().toString();
    }

    public void lock() {
        while (!tryLock()) {
            try {
                TimeUnit.MILLISECONDS.sleep(100); // 失败后短暂等待
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

    public boolean tryLock() {
        try(Jedis jedis = jedisPool.getResource();) {
            // 原子化加锁:SET lockKey UUID NX EX expireTime
            String result = jedis.set(lockKey, lockValue,
                    SetParams.setParams().nx().ex(EXPIRE_TIME));
            if ("OK".equals(result)) {
                startWatchdog(); // 启动续期线程
                return true;
            }
            return false;
        }
    }

    private void startWatchdog() {
        watchDogThread = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) { // 循环条件检查中断状态
                try {
                    TimeUnit.SECONDS.sleep(EXPIRE_TIME / 2); // 每1/3过期时间执行一次
                } catch (InterruptedException e) {
                    // 捕获中断异常,退出循环
                    Thread.currentThread().interrupt(); // 重置中断状态
                    break;
                }
                // 续期逻辑:延长锁过期时间
                // 当超时时间小于1/2时,增加超时时间到原来的4s
                try(Jedis jedis = jedisPool.getResource()) {
                    jedis.expire(lockKey, EXPIRE_TIME);
                    System.out.println("为" + lockKey + "续期" + EXPIRE_TIME + "秒");
                }
            }
        }, "expire-thread");
        watchDogThread.setDaemon(true); // 设置为守护线程
        watchDogThread.start();
    }

    public void unlock() {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        try(Jedis jedis = jedisPool.getResource()) {
            jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(lockValue));
        }
        stopWatchdog();
    }

    private void stopWatchdog() {
        if (watchDogThread != null) {
            watchDogThread.interrupt(); // 发送中断信号
            watchDogThread = null;      // 清理线程引用,避免内存泄漏
        }
    }
}

目前这个分布锁的局限性与改进措施:

  • 单点故障:使用Redlock算法,在多个独立Redis节点上获取锁
  • 不可重入:记录线程标识和重入次数
  • 不公平:使用Redis列表维护等待队列

jedis分布式锁的使用:

java 复制代码
package com.morris.redis.demo.lock;

import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * jedis分布式锁的使用
 */
public class JedisLockDemo {

    private volatile static int count;

    public static void main(String[] args) throws InterruptedException {
        JedisPool jedisPool = new JedisPool(new JedisPoolConfig());

        int threadCount = 3;
        CountDownLatch countDownLatch = new CountDownLatch(threadCount);
        ExecutorService executorService = Executors.newFixedThreadPool(threadCount);

        for (int i = 0; i < threadCount; i++) {
            executorService.submit(() -> {
                JedisLock jedisLock = new JedisLock(jedisPool, "lock-key");
                jedisLock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + "获得锁,开始执行业务逻辑。。。");
                    try {
                        TimeUnit.SECONDS.sleep(60);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println(Thread.currentThread().getName() + "获得锁,结束执行业务逻辑。。。");
                    count++;
                } finally {
                    jedisLock.unlock();
                }
                countDownLatch.countDown();
            });
        }

        countDownLatch.await();
        executorService.shutdown();
        System.out.println(count);
    }

}

Redisson中分布式锁的使用

pom.xml中引入redission的依赖:

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

Redisson中分布式锁的使用:

java 复制代码
package com.morris.redis.demo.lock;

import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * redisson中分布式锁的使用
 */
public class RedissonLockDemo {

    private volatile static int count;

    public static void main(String[] args) throws InterruptedException {
        // 配置Redisson客户端
        Config config = new Config();
        config.useSingleServer()
                .setAddress("redis://127.0.0.1:6379");

        // 创建Redisson客户端实例
        RedissonClient redisson = Redisson.create(config);

        int threadCount = 3;
        CountDownLatch countDownLatch = new CountDownLatch(threadCount);
        ExecutorService executorService = Executors.newFixedThreadPool(threadCount);

        for (int i = 0; i < threadCount; i++) {
            executorService.submit(() -> {
                RLock lock = redisson.getLock("lock-key");
                lock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + "获得锁,开始执行业务逻辑。。。");
                    try {
                        TimeUnit.SECONDS.sleep(60);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println(Thread.currentThread().getName() + "获得锁,结束执行业务逻辑。。。");
                    count++;
                } finally {
                    lock.unlock();
                }
                countDownLatch.countDown();
            });
        }

        countDownLatch.await();
        executorService.shutdown();
        System.out.println(count);
        redisson.shutdown();
    }

}

总结

优点:基于redis实现的分布式锁就会拥有redis的特点,那就是速度快。

缺点:实现逻辑复杂,redis本身是一个AP模型,只能保证网络分区和可用性,并不能保证强一致性,而分布式锁这个逻辑是一个CP模型,必须保证一致性,所以redis这种实现方式在一定概率上会出现多个客户端获取到锁,例如redis中的master节点设置key成功并返回给客户端,此时还没来得及同步给slave就挂了,然后slave被选举为新的master节点,其他客户端来获取锁就会成功,这样多个客户端就同时获取到锁了。

相关推荐
数据智能老司机4 小时前
CockroachDB权威指南——CockroachDB SQL
数据库·分布式·架构
数据智能老司机4 小时前
CockroachDB权威指南——开始使用
数据库·分布式·架构
松果猿4 小时前
空间数据库学习(二)—— PostgreSQL数据库的备份转储和导入恢复
数据库
Kagol5 小时前
macOS 和 Windows 操作系统下如何安装和启动 MySQL / Redis 数据库
redis·后端·mysql
无名之逆5 小时前
Rust 开发提效神器:lombok-macros 宏库
服务器·开发语言·前端·数据库·后端·python·rust
s9123601015 小时前
rust 同时处理多个异步任务
java·数据库·rust
数据智能老司机5 小时前
CockroachDB权威指南——CockroachDB 架构
数据库·分布式·架构
hzulwy5 小时前
Redis常用的数据结构及其使用场景
数据库·redis
程序猿熊跃晖5 小时前
解决 MyBatis-Plus 中 `update.setProcInsId(null)` 不生效的问题
数据库·tomcat·mybatis
ashane13147 小时前
Redis 哨兵集群(Sentinel)与 Cluster 集群对比
redis