Redis实现分布式锁

使用Jedis实现Redis客户端,且只考虑Redis服务端单机部署的场景

一、可靠性

为了确保分布式锁可用,锁的实现至少要同时满足以下三个条件:

  • 互斥性。在任意时刻,只有一个客户端能持有锁
  • 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁
  • 加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了

二、依赖和配置信息

1)、引入依赖

        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>2.9.0</version>
        </dependency>

2)、相关配置

application.properties

#redis连接信息
jedisPool.host=127.0.0.1
jedisPool.port=6379

配置类

@Configuration
public class JedisConfig {
    @Value("${jedisPool.host}")
    private String host;

    @Value("${jedisPool.port}")
    private Integer port;

    @Bean
    public Jedis jedis() {
        JedisPool jedisPool = new JedisPool(host, port);
        Jedis jedis = jedisPool.getResource();
        return jedis;
    }
}

三、加锁的实现

public class RedisLock {
    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";

    /**
     * 尝试获取分布式锁
     *
     * @param jedis      Redis客户端
     * @param lockKey    锁标识
     * @param requestId  请求标识
     * @param expireTime 超期时间(秒)
     * @return 是否获取成功
     */
    public static boolean getLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime * 1000);
        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }

加锁过程中主要使用的redis命令set key value nx px expireTime,当key不存在或者已经过期时,进行set操作,返回OK;当key存在时,不做任何操作

对应的Java代码为:jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime * 1000)

  • key:传入锁标识
  • value:传入的是requestId,目的是为了实现加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了,requestId可以使用UUID.randomUUID().toString()方法生成
  • nxxx:NX
  • expx:PX
  • time:key的过期时间

setnx保证了如果已有key存在,则函数不会调用成功,只有一个客户端能持有锁,满足互斥性

设置了过期时间,即使锁的持有者后续发生崩溃而没有解锁,锁也会因为到了过期时间而自动解锁(即key被删除),不会发生死锁

将value赋值为requestId,代表加锁的客户端请求标识,那么在客户端在解锁的时候就可以进行校验是否是同一个客户端

1)、错误示例1

    public static void getLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
        Long result = jedis.setnx(lockKey, requestId);
        if (result == 1) {
            // 若在这里程序突然崩溃,则无法设置过期时间,将发生死锁         jedis.expire(lockKey, expireTime);
        }
    }

实现思路:使用jedis.setnx()jedis.expire()组合实现加锁

存在的问题:通过两条Redis命令,不具有原子性,如果程序在执行完jedis.setnx()之后突然崩溃,导致锁没有设置过期时间,那么将会发生死锁(低版本的jedis并不支持多参数的set()方法)

2)、错误示例2

    public static boolean getLock(Jedis jedis, String lockKey, int expireTime) {
        long expires = System.currentTimeMillis() + expireTime;
        String expiresStr = String.valueOf(expires);
        // 如果当前锁不存在,返回加锁成功
        if (jedis.setnx(lockKey, expiresStr) == 1) {
            return true;
        }
        // 如果锁存在,获取锁的过期时间
        String currentValueStr = jedis.get(lockKey);
        if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
            // 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间
            String oldValueStr = jedis.getSet(lockKey, expiresStr);
            if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
                // 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才有权利加锁
                return true;
            }
        }
        // 其他情况,一律返回加锁失败
        return false;
    }

实现思路:使用jedis.setnx()命令实现加锁,其中key是锁,value是锁的过期时间。通过setnx()方法尝试加锁,如果当前锁不存在,返回加锁成功。 如果锁已经存在则获取锁的过期时间,和当前时间比较,如果锁已经过期,则设置新的过期时间,返回加锁成功

存在的问题:

  • 由于是客户端自己生成过期时间,所以需要强制要求分布式下每个客户端的时间必须同步
  • 当锁过期的时候,如果多个客户端同时执行jedis.getSet()方法,那么虽然最终只有一个客户端可以加锁,但是这个客户端的锁的过期时间可能被其他客户端覆盖
  • 锁不具备拥有者标识,即任何客户端都可以解锁

四、解锁的实现

    private static final Long RELEASE_SUCCESS = 1L;

    /**
     * 释放分布式锁
     *
     * @param jedis     Redis客户端
     * @param lockKey   锁标识
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    public static boolean releaseLock(Jedis jedis, 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 (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }

这段Lua代码的功能是首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)。使用eval()方法执行Lua语言来实现可以确保上述操作是原子性的

1)、错误示例

    public static void releaseLock(Jedis jedis, String lockKey, String requestId) {
        // 判断加锁与解锁是不是同一个客户端
        if (requestId.equals(jedis.get(lockKey))) {
            // 若在此时,这把锁突然不是这个客户端的,则会误解锁
            jedis.del(lockKey);
        }
    }

存在的问题:如果调用jedis.del()方法的时候,这把锁已经不属于当前客户端的时候会解除他人加的锁比如客户端A加锁,一段时间之后客户端A解锁,在执行jedis.del()之前,锁突然过期了,此时客户端B尝试加锁成功,然后客户端A再执行del()方法,则将客户端B的锁给解除了

GitHub地址:https://github.com/hxt970311/RedisLock


参考:https://wudashan.cn/2017/10/23/Redis-Distributed-Lock-Implement/#releaseLock-wrongDemo2

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

邋遢的流浪剑客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值