Redis实现分布式锁的正确姿势 | Spring Cloud 36

news2024/10/5 19:09:02

一、分布式锁

1.1 什么是分布式锁

分布式锁,即分布式系统中的锁。在单体应用中我们通过锁解决的是控制共享资源访问的问题,而分布式锁,就是解决了分布式系统中控制共享资源访问的问题。与单体应用不同的是,分布式系统中竞争共享资源的最小粒度从线程升级成了进程。

1.2 分布式锁应该具备哪些条件

  • 在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行

  • 高可用的获取锁与释放锁

  • 高性能的获取锁与释放锁

  • 具备可重入特性(可理解为重新进入,由多于一个任务并发使用,而不必担心数据错误)

  • 具备锁失效机制,即自动解锁,防止死锁

  • 具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败

1.3 分布式锁的实现方式

  • 基于数据库实现分布式锁

  • 基于Zookeeper实现分布式锁

  • 基于Reids实现分布式锁

本章重点讲解的是基于Reids的分布式锁

二、利用RedisTemplate实现分布式锁

在真实的项目里,我们经常会使用Spring Boot/Spring Cloud框架,该框架已经集成了 RedisTemplate 类,开放了很多对RedisAPI

本章节列举基于RedisTemplate实现方式分布式锁的各种方式及存在的问题。

2.1 利用setIfAbsent先设置key value再设置expire

redisTemplate.opsForValue().setIfAbsent(key,value);
redisTemplate.expire(key, time, TimeUnit.SECONDS);

问题:「不是原子操作」。以上两条语句不是原子性的。假如执行完第一条语句后,服务挂掉,导致key永久存在,锁无法释放。

setIfAbsent(key, value)方法简介:如果key不存在则新增,返回 true;存在则不改变已经有的值返回 false

2.2 利用setIfAbsent同时设置 key value expire

redisTemplate.opsForValue().setIfAbsent(key, value, time, TimeUnit.SECONDS);

问题一:「锁过期释放了,业务还没执行完」。假设线程a获取锁成功,一直在执行临界区的代码。但是100s过去后,它还没执行完。但是,这时候锁已经过期了,此时线程b又请求过来。显然线程b就可以获得锁成功,也开始执行临界区的代码。那么问题就来了,临界区的业务代码都不是严格串行执行。

问题二:「锁被别的线程误删」。假设线程a执行完后,去释放锁。但是它不知道当前的锁可能是线程b持有的(线程a去释放锁时,有可能过期时间已经到了,此时线程b进来占有了锁)。那线程a就把线程b的锁释放掉了,但是线程b临界区业务代码可能都还没执行完。

2.3 利用setIfAbsent同时设置 key value expire,value为客户端标识

可以在获取锁,解锁时。首先获取客户端标识,如果和加锁时不一致,则获解锁操作失败,解决了2.2中提到了「锁被别的线程误删」问题。

Callable<String> callable = () -> {
    String result = "";
    String threadId = Thread.currentThread().getId() + "";
    if (Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(key, threadId, 10, TimeUnit.SECONDS))) {
        result = threadId;
        try {
        	// 模拟业务处理
            Thread.sleep(5000);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (redisTemplate.opsForValue().get(key).equals(threadId)) {
                redisTemplate.delete(key);
            }
        }
    }
    return result;
};
List<Future<String>> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
    Future<String> future = executorService.submit(callable);
    list.add(future);
}
for (Future<String> future : list) {
    log.info(future.get());
}

问题:「不是原子操作」。通过key获取加锁时的客户端标识和释放锁两条语句不是原子操作。

2.4 利用lua脚本进行加锁及释放锁原子操作

本章重点,通过lua脚本对2.3提出的原子性问题进行解决。

RedisTemplate执行lua脚本加锁释放锁工具类:

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.List;

@Component
public class ConcurrentLockUtil {

    private static final String LOCK_LUA = "if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then redis.call('expire', KEYS[1], ARGV[2]) return 'true' else return 'false' end";
    private static final String UNLOCK_LUA = "if redis.call('get', KEYS[1]) == ARGV[1] then redis.call('del', KEYS[1]) end return 'true' ";

    private final RedisScript lockRedisScript;
    private final RedisScript unLockRedisScript;
    private final RedisSerializer<String> argsSerializer;
    private final RedisSerializer<String> resultSerializer;
    private RedisTemplate redisTemplate;


    public ConcurrentLockUtil(RedisTemplate redisTemplate) {
        this.argsSerializer = new StringRedisSerializer();
        this.resultSerializer = new StringRedisSerializer();
        this.lockRedisScript = RedisScript.of(LOCK_LUA, String.class);
        this.unLockRedisScript = RedisScript.of(UNLOCK_LUA, String.class);
        this.redisTemplate = redisTemplate;
    }

    /**
     * 分布式锁
     *
     * @param lockKey
     * @param value
     * @param time
     * @return
     */
    public boolean lock(String lockKey, String value, long time) {
        List<String> keys = Collections.singletonList(lockKey);
        String flag = (String) redisTemplate.execute(lockRedisScript, argsSerializer, resultSerializer, keys, value, String.valueOf(time));
        return Boolean.valueOf(flag);
    }

    /**
     * 删除锁
     *
     * @param lock
     * @param val
     */
    public void unlock(String lock, String val) {
        List<String> keys = Collections.singletonList(lock);
        redisTemplate.execute(unLockRedisScript, argsSerializer, resultSerializer, keys, val);
    }
}

业务调用:

@Autowired
ConcurrentLockUtil concurrentLockUtil;

ExecutorService executorService = Executors.newFixedThreadPool(10);
String key = "test2-lock";

Callable<String> callable = () -> {
    String result = "";
    String threadId = Thread.currentThread().getId() + "";
    if (Boolean.TRUE.equals(concurrentLockUtil.lock(key, threadId, 10))) {
        result = threadId;
        try {
            Thread.sleep(5000);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            concurrentLockUtil.unlock(key, threadId);
        }
    }
    return result;
};
List<Future<String>> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
    Future<String> future = executorService.submit(callable);
    list.add(future);
}
for (Future<String> future : list) {
    log.info(future.get());
}

问题:针对2.2章节提到的「锁过期释放了,业务还没执行完」问题仍然存在。

有些小伙伴认为,稍微把锁过期时间设置长一些就可以。其实我们设想一下,是否可以给获得锁的线程,开启一个定时守护线程,每隔一段时间检查锁是否还存在,存在则对锁的过期时间延长,防止锁过期提前释放,请见下面章节。

三、利用Redisson框架RLock实现分布式锁

开源框架Redisson解决了2.1.4提出的问题。官网地址:https://github.com/redisson/redisson/wiki/1.-%E6%A6%82%E8%BF%B0

我们一起来看下Redisson底层原理图:

在这里插入图片描述

只要线程一加锁成功,就会启动一个watch dog看门狗,它是一个后台线程,会每隔10秒检查一下,如果线程1还持有锁,那么就会不断的延长锁key的生存时间(Redisson 中使用的 Lua 脚本做的检查及设置过期时间操作,本身是原子性的)。因此,解决了「锁过期释放,业务没执行完」问题。

3.1 加锁逻辑

RedissonLua 加锁脚本定义及流程如下:

在这里插入图片描述

在这里插入图片描述

3.2 解锁逻辑

RedissonLua 解锁脚本定义及流程如下:

在这里插入图片描述

在这里插入图片描述

3.3 续锁逻辑

在这里插入图片描述

可以看到续时方法将 threadId 当作标识符进行续时

在这里插入图片描述

知道核心理念就好了, 没必要研究每一行代码

在这里插入图片描述

四、利用RedissonRedLock多机实现分布式锁

4.1 单机版锁存在的问题

前面章节的几种方案都只是基于单机版的讨论,还不是很完美。其实Redis一般都是集群部署的:

在这里插入图片描述

如果线程一在Redismaster节点上拿到了锁,但是加锁的key还没同步到slave节点。恰好这时,master节点发生故障,一个slave节点就会升级为master节点。线程二就可能获取同个key的锁,但线程一也已经拿到锁了,锁的安全性就没了。

4.2 RedissonRedLock核心思想

为了解决上述问题,Redis作者 antirez提出一种高级的分布式锁算法:RedlockRedlock核心思想是这样的:

搞多个Redis master部署,以保证它们不会同时宕掉。并且这些master节点是完全相互独立的,相互之间不存在数据同步。同时,需要确保在这多个master实例上,是与在Redis单实例,使用相同方法来获取和释放锁。

在这里插入图片描述
RedissonRedLock算法:
在这里插入图片描述

RedissonRedLock业务逻辑实现:

Config config1 = new Config();
config1.useSingleServer().setAddress("redis://172.0.0.1:5378").setPassword("a123456").setDatabase(0);
RedissonClient redissonClient1 = Redisson.create(config1);

Config config2 = new Config();
config2.useSingleServer().setAddress("redis://172.0.0.1:5379").setPassword("a123456").setDatabase(0);
RedissonClient redissonClient2 = Redisson.create(config2);

Config config3 = new Config();
config3.useSingleServer().setAddress("redis://172.0.0.1:5380").setPassword("a123456").setDatabase(0);
RedissonClient redissonClient3 = Redisson.create(config3);

String lockKey = "myLock";
int waitTimeout = 5;
int leaseTime = 30;

/**
 * 获取多个 RLock 对象
 */
RLock lock1 = redissonClient1.getLock(lockKey);
RLock lock2 = redissonClient2.getLock(lockKey);
RLock lock3 = redissonClient3.getLock(lockKey);

/**
 * 根据多个 RLock 对象构建 RedissonRedLock (最核心的差别就在这里)
 */
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);

try {
    /**
     * 4.尝试获取锁
     * waitTimeout 尝试获取锁的最大等待时间,超过这个值,则认为获取锁失败
     * leaseTime   锁的持有时间,超过这个时间锁会自动失效(值应设置为大于业务处理的时间,确保在锁有效期内业务能处理完)
     */
    boolean res = redLock.tryLock((long) waitTimeout, (long) leaseTime, TimeUnit.SECONDS);
    if (res) {
        //成功获得锁,在这里处理业务
    }
} catch (Exception e) {
    throw new RuntimeException("aquire lock fail");
} finally {
    //无论如何, 最后都要解锁
    redLock.unlock();
}

RedissonRedLock核心源码:

public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
    long newLeaseTime = -1;
    if (leaseTime != -1) {
        newLeaseTime = unit.toMillis(waitTime)*2;
    }
    
    long time = System.currentTimeMillis();
    long remainTime = -1;
    if (waitTime != -1) {
        remainTime = unit.toMillis(waitTime);
    }
    long lockWaitTime = calcLockWaitTime(remainTime);
    /**
     * 1. 允许加锁失败节点个数限制(N-(N/2+1))
     */
    int failedLocksLimit = failedLocksLimit();
    /**
     * 2. 遍历所有节点通过EVAL命令执行lua加锁
     */
    List<RLock> acquiredLocks = new ArrayList<>(locks.size());
    for (ListIterator<RLock> iterator = locks.listIterator(); iterator.hasNext();) {
        RLock lock = iterator.next();
        boolean lockAcquired;
        /**
         *  3.对节点尝试加锁
         */
        try {
            if (waitTime == -1 && leaseTime == -1) {
                lockAcquired = lock.tryLock();
            } else {
                long awaitTime = Math.min(lockWaitTime, remainTime);
                lockAcquired = lock.tryLock(awaitTime, newLeaseTime, TimeUnit.MILLISECONDS);
            }
        } catch (RedisResponseTimeoutException e) {
            // 如果抛出这类异常,为了防止加锁成功,但是响应失败,需要解锁所有节点
            unlockInner(Arrays.asList(lock));
            lockAcquired = false;
        } catch (Exception e) {
            // 抛出异常表示获取锁失败
            lockAcquired = false;
        }
        
        if (lockAcquired) {
            /**
             *4. 如果获取到锁则添加到已获取锁集合中
             */
            acquiredLocks.add(lock);
        } else {
            /**
             * 5. 计算已经申请锁失败的节点是否已经到达 允许加锁失败节点个数限制 (N-(N/2+1))
             * 如果已经到达, 就认定最终申请锁失败,则没有必要继续从后面的节点申请了
             * 因为 Redlock 算法要求至少N/2+1 个节点都加锁成功,才算最终的锁申请成功
             */
            if (locks.size() - acquiredLocks.size() == failedLocksLimit()) {
                break;
            }

            if (failedLocksLimit == 0) {
                unlockInner(acquiredLocks);
                if (waitTime == -1 && leaseTime == -1) {
                    return false;
                }
                failedLocksLimit = failedLocksLimit();
                acquiredLocks.clear();
                // reset iterator
                while (iterator.hasPrevious()) {
                    iterator.previous();
                }
            } else {
                failedLocksLimit--;
            }
        }

        /**
         * 6.计算 目前从各个节点获取锁已经消耗的总时间,如果已经等于最大等待时间,则认定最终申请锁失败,返回false
         */
        if (remainTime != -1) {
            remainTime -= System.currentTimeMillis() - time;
            time = System.currentTimeMillis();
            if (remainTime <= 0) {
                unlockInner(acquiredLocks);
                return false;
            }
        }
    }

    if (leaseTime != -1) {
        List<RFuture<Boolean>> futures = new ArrayList<>(acquiredLocks.size());
        for (RLock rLock : acquiredLocks) {
            RFuture<Boolean> future = ((RedissonLock) rLock).expireAsync(unit.toMillis(leaseTime), TimeUnit.MILLISECONDS);
            futures.add(future);
        }
        
        for (RFuture<Boolean> rFuture : futures) {
            rFuture.syncUninterruptibly();
        }
    }

    /**
     * 7.如果逻辑正常执行完则认为最终申请锁成功,返回true
     */
    return true;
}

当然,对于 RedissonRedLock 算法不是没有质疑声, 大家可以去 Redis 官网查看Martin KleppmannRedis 作者Antirez 的辩论。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/452024.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【网络安全】SSRF漏洞

ssfr ssrf产生的原因原理展示使用不当可能出现ssrf漏洞函数漏洞检测(靶场一)代码curl是什么检测服务器是否可以从其他服务器获取数据使用file协议获取远端服务器的内容利用dict协议探测端口 漏洞检测&#xff08;靶场二&#xff09;代码file_get_contents()利用file协议读取服务…

过来人(江苏)专转本考试后的感悟和经验,真的很受用

过来人转本考试后的感悟和经验&#xff0c;真的很受用&#xff01; 转本不仅是分数的较量&#xff0c;也是信息收集、时间管理、学习能力、毅力等等的较量。 同学们在转本中难免会遇见一些困难&#xff0c;为了避免走弯路&#xff0c;一起来看看过来人的感悟和经验吧&#xf…

Android音频使用webrtc降噪处理、回声消除

Android音频使用webrtc降噪处理、回声消除 介绍音频处理在Android应用中的重要性和应用场景 音频处理在Android应用中扮演着重要的角色&#xff0c;它能够改善用户体验&#xff0c;提升应用的功能性和吸引力。下面将介绍音频处理在Android应用中的广泛应用以及音频处理对用户体…

深度学习--基础(一)pytorch安装--cpu

在线安装 无GPU的时候&#xff0c;只能安装CPU版本&#xff0c;打开官网 https://pytorch.org/ 直接Pip安装即可 国内访问这些下载安装会出现超时的情况&#xff0c;可以-i指定国内安装源&#xff1a; pip3.11 install torch torchvision torchaudio -i https://pypi.tuna.ts…

【架构】微前端

文章目录 概述优劣优点缺点 微前端的整体架构微前端部署平台微前端运行时基于 SPA 的微前端架构 应用生命周期 方案qiankun 主应用qiankun微应用Vue 2 微应用 来源 概述 微前端不是单纯的前端框架或者工具&#xff0c;而是一套架构体系&#xff0c;这个概念最早在 2016 年底被…

[C++]内存管理

目录 内存管理&#xff1a;&#xff1a; 1.C/C内存分布 2.C语言中动态内存管理方式 3.C中动态内存管理 4.operator new与operator delete函数 5.new和delete的实现原理 6.定位new表达式 7.内存泄漏 内存管理&#xff1a;&#xff1a; 1.C/C内存分布 int globalVar 1; stati…

第11届蓝桥杯省赛真题剖析-2020年6月21日Scratch编程初中级组

[导读]&#xff1a;超平老师的《Scratch蓝桥杯真题解析100讲》已经全部完成&#xff0c;后续会不定期解读蓝桥杯真题&#xff0c;这是Scratch蓝桥杯真题解析第125讲。 第11届蓝桥杯省赛&#xff0c;这是2020年6月21日举办的省赛Scratch考试真题&#xff0c;原定于2020年3月7日…

速度和可靠性是可以两全其美的

通过采用整体方法并利用工程原理和实践&#xff0c;我们可以两全其美——速度和可靠性。 当涉及到在线服务时&#xff0c;正常运行时间是至关重要的&#xff0c;但这并不是唯一需要考虑的事情。想象一下&#xff0c;经营一家网上商店——让你的网站99.9%的时间都可用听起来不错…

10个很少人知道的 JavaScript 控制台方法

您肯定听说过 console.log() 并且可能一直在使用它。它非常流行&#xff0c;像 Visual Studio Intellicode 这样的工具通常会在 IDE 中输入时在任何其他控制台方法之前推荐它&#xff1a; 在今天这篇文章中&#xff0c;我们将探讨一些最有用的控制台方法及其在数据可视化、调试…

服装产业数字化升级,低代码赋能企业柔性生产

一、前言 随着消费者个性化需求不断增加&#xff0c;我国服装行业正面临着前所未有的挑战。此外&#xff0c;电商渠道占比不断提高&#xff0c;订单碎片化程度进一步放大&#xff0c;传统计划性生产的供应链流程已无法适应不断变化的着衣需求&#xff0c;使得服装品牌商在供应…

6.MapReduce的框架原理

本章节将分为InputFormat,split,OutputFormat三个小章节来介绍框架原理 1.InputFormat 1.1 切片: 将输入数据分成几份,每份交给一个MapTask去处理(getSplit方法) 对于MapRedcue,切片发生在客户端,任务提交的时候 机制:MapTask并行度决定机制 切了多少片,就开启多少个M…

记录一次heap.bin文件分析

背景&#xff1a;生产服务运行OA系统服务&#xff0c;用户使用过程中&#xff0c;突然发现服务不能访问&#xff0c;接到用户反馈后&#xff0c;第一时间登陆服务器&#xff0c;发现东方通进程在、端口在&#xff0c;服务器CPU使用率并不高&#xff0c;为不影响用户正常开展业务…

记录bingAI解答pyjwt参数和头部的问题

python jwt.encode()函数的参数是哪些 正在搜索: python jwt.encode()函数的参数 正在为你生成答案… 已收到消息. 在Python中&#xff0c;jwt.encode()函数的参数有三个&#xff1a;第一个是payload&#xff0c;主要用来存放有效的信息&#xff0c;例如用户名&#xff0c;过期…

去后厂村开游戏厅吧!基于PP-TinyPose的简易体感游戏开发框架

‍ 项目简介 近年来&#xff0c;随着虚拟现实技术和计算机图形学技术的迅猛发展&#xff0c;越来越多的体感游戏在市场上出现并受到欢迎。要让体感游戏具备良好的表现&#xff0c;就需要使用大量的传感器&#xff0c;甚至需要使用高性能的计算机和图形处理器。这不仅会增加游戏…

单链表leetcode——C语言

203. 移除链表元素 难度简单1230收藏分享切换为英文接收动态反馈 给你一个链表的头节点 head 和一个整数 val &#xff0c;请你删除链表中所有满足 Node.val val 的节点&#xff0c;并返回 新的头节点 。 示例 1&#xff1a; 输入&#xff1a;head [1,2,6,3,4,5,6], val 6…

java_java基础语法

注释 什么是注释 简单来说注释就是在程序中对代码进行解释说明的文字,方便自己和其他人理解,查看,不会影响程序的正常执行注释有哪些 单行注释// 注释内容只能写一行多行注释/* 注释内容1 注释内容2 */文档注释/** 注释内容 注释内容 */字面量 告诉程序员,数据在程序中的书写…

C++中常用的四种类型转换方式

C中常用的四种类型转换方式 一、相关概念二、static_cast 转换2.1、说明2.2、返回值2.3、示例 三、const_cast 转换3.1、说明3.2、返回值3.3、示例 四、dynamic_cast 转换4.1、说明4.2、返回值4.3、示例 五、reinterpret_cast 转换5.1、说明5.2、返回值5.3、示例 总结 一、相关…

【大数据之Hadoop】十八、MapReduce之压缩

1 概述 优点&#xff1a;减少磁盘IO、减少磁盘存储空间。 缺点&#xff1a;因为压缩解压缩都需要cpu处理&#xff0c;所以增加CPU开销。 原则&#xff1a;运算密集型的Job&#xff0c;少用压缩&#xff1b;IO密集型的Job&#xff0c;多用压缩。 2 压缩算法对比 压缩方式选择时…

深入浅出DPDK-1.1主流包处理硬件平台

DPDK用软件的方式在通用多核处理器上演绎着数据包处理的新篇章&#xff0c;而对于数据包处理&#xff0c;多核处理器显然不是唯一的平台。支撑包处理的主流硬件平台大致可分为三个方向&#xff1a;硬件加速器、网络处理器、多核处理器。 根据处理内容、复杂度、成本、量产规模…

【数据结构】- 链表之单链表(上)

文章目录 前言一、链表1.1链表的概念及结构1.2链表的分类 二、单链表(上)2.1单链表的实现2.2单链表实现的两种结构解析2.3单链表的接口实现2.3.1头插2.3.2温馨提醒 宝子~2.3.3头插完整版代码2.3.4尾插2.3.5温馨提醒 宝子~2.3.6总而言之 总结 前言 “偶尔失意 是为了压住翘起的…