service层加需要加锁吗_Redis分布式锁,你真的用对了吗?

文章有点长并且绕,先来个图片缓冲下!

2d6772b32cfe53d6e6e4627d0d9bf8f7.png

前言

现在的业务场景越来越复杂,使用的架构也就越来越复杂,分布式、高并发已经是业务要求的常态。像腾讯系的不少服务,还有CDN优化、异地多备份等处理。
说到分布式,就必然涉及到分布式锁的概念,如何保证不同机器不同线程的分布式锁同步呢?

实现要点

  1. 互斥性,同一时刻,只能有一个客户端持有锁。

  2. 防止死锁发生,如果持有锁的客户端崩溃没有主动释放锁,也要保证锁可以正常释放及其他客户端可以正常加锁。

  3. 加锁和释放锁必须是同一个客户端。

  4. 容错性,只有redis还有节点存活,就可以进行正常的加锁解锁操作。

正确的redis分布式锁实现

错误加锁方式一

保证互斥和防止死锁,首先想到的使用redis的setnx命令保证互斥,为了防止死锁,锁需要设置一个超时时间。

public static void wrongLock(Jedis jedis, String key, String uniqueId, int expireTime) {

在多线程并发环境下,任何非原子性的操作,都可能导致问题。这段代码中,如果设置过期时间时,redis实例崩溃,就无法设置过期时间。如果客户端没有正确的释放锁,那么该锁(永远不会过期),就永远不会被释放。

错误加锁方式二

比较容易想到的就是设置值和超时时间为原子原子操作就可以解决问题。那使用setnx命令,将value设置为过期时间不就ok了吗?

public static boolean wrongLock(Jedis jedis, String key, int expireTime) {

乍看之下,没有什么问题。但仔细分析,有如下问题:

  1. value设置为过期时间,就要求各个客户端严格的时钟同步,这就需要使用到同步时钟。即使有同步时钟,分布式的服务器一般来说时间肯定是存在少许误差的。

  2. 锁过期时,使用 jedis.getSet虽然可以保证只有一个线程设置成功,但是不能保证加锁和解锁为同一个客户端,因为没有标志锁是哪个客户端设置的嘛。

错误解锁方式一

直接删除key

public static void wrongReleaseLock(Jedis jedis, String key) {

简单粗暴,直接解锁,但是不是自己加锁的,也会被删除,这好像有点太随意了吧!

错误解锁方式二

判断自己是不是锁的持有者,如果是,则只有持有者才可以释放锁。

public static void wrongReleaseLock(Jedis jedis, String key, String uniqueId) {

看起来很完美啊,但是如果你判断的时候锁是自己持有的,这时锁超时自动释放了。然后又被其他客户端重新上锁,然后当前线程执行到jedis.del(key),这样这个线程不就删除了其他线程上的锁嘛,好像有点乱套了哦!

正确的加解锁方式

基本上避免了以上几种错误方式之外,就是正确的方式了。要满足以下几个条件:

  1. 命令必须保证互斥

  2. 设置的key必须要有过期时间,防止崩溃时锁无法释放

  3. value使用唯一id标志每个客户端,保证只有锁的持有者才可以释放锁

加锁直接使用set命令同时设置唯一id和过期时间;其中解锁稍微复杂些,加锁之后可以返回唯一id,标志此锁是该客户端锁拥有;释放锁时要先判断拥有者是否是自己,然后删除,这个需要redis的lua脚本保证两个命令的原子性执行。
下面是具体的加锁和释放锁的代码:

@Slf4j
public class RedisDistributedLock {
    private static final String LOCK_SUCCESS = "OK";
    private static final Long RELEASE_SUCCESS = 1L;
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";
    // 锁的超时时间
    private static int EXPIRE_TIME = 5 * 1000;
    // 锁等待时间
    private static int WAIT_TIME = 1 * 1000;

    private Jedis jedis;
    private String key;

    public RedisDistributedLock(Jedis jedis, String key) {
        this.jedis = jedis;
        this.key = key;
    }

    // 不断尝试加锁
    public String lock() {
        try {
            // 超过等待时间,加锁失败
            long waitEnd = System.currentTimeMillis() + WAIT_TIME;
            String value = UUID.randomUUID().toString();
            while (System.currentTimeMillis()                 String result = jedis.set(key, value, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, EXPIRE_TIME);
                if (LOCK_SUCCESS.equals(result)) {
                    return value;
                }
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        } catch (Exception ex) {
            log.error("lock error", ex);
        }
        return null;
    }

    public boolean release(String value) {
        if (value == null) {
            return false;
        }
        // 判断key存在并且删除key必须是一个原子操作
        // 且谁拥有锁,谁释放
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = new Object();
        try {
            result = jedis.eval(script, Collections.singletonList(key),
                    Collections.singletonList(value));
            if (RELEASE_SUCCESS.equals(result)) {
                log.info("release lock success, value:{}", value);
                return true;
            }
        } catch (Exception e) {
            log.error("release lock error", e);
        } finally {
            if (jedis != null) {
                jedis.close();
            }
        }
        log.info("release lock failed, value:{}, result:{}", value, result);
        return false;
    }
}

单是一个redis的分布式锁就有这么多道道,不知道你是否看明白了?留言讨论下吧!

有道无术,术可成;有术无道,止于术

欢迎大家关注Java之道公众号

5af1899fd93bc3bba2c13ad841b43819.png

好文章,我在看❤️

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Redis分布式锁中,加锁失败会有不同的处理方式。根据引用和引用的描述,当加锁失败时,通常会返回当前已经加锁的线程还有多久到期的信息。然后,线程会休眠一段指定的时间后再次尝试加锁。 具体来说,对于RedissonLock,如果加锁失败,返回的过期时间(ttl)表示当前锁还有多久过期。例如,如果锁还有4秒过期,那么ttl就是4秒。而对于RedLock,在获取过期时间(ttl)时,会考虑剩余的等待时间(waitTime)。如果锁还有4秒过期,但是等待时间只剩下3秒,那么ttl就是3秒;反之,如果等待时间还有5秒,但是锁还有3秒就过期,那么ttl也是3秒。这是根据引用的描述。 总之,加锁失败后的处理方式可以根据具体的实现和需求进行调整,但通常会使用线程休眠和重新尝试加锁的策略来处理。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [Redis分布式锁加锁失败,休眠机制](https://blog.csdn.net/CPLASF_/article/details/123953842)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *3* [谈谈Redis分布式锁的正确实现方法](https://download.csdn.net/download/weixin_38745648/13703862)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值