基于 RedisTemplate + 线程池 实现 Redis分布式锁

news/2024/11/20 6:25:20/文章来源:https://www.cnblogs.com/1399z3blog/p/18290473

分布式锁需求

往往部署的 后台服务 不会只是 单机部署 了,而是通过集群的方式运行在两个甚至多个部署的服务器上(即便是同一个服务器的两个端口上,也会出现同样的问题)等架构来进行部署。
在用户所发送的请求中,每个请求将会通过负载均衡发送到不同的服务器中。如果我们还想对集群中的某个代码片段进行加锁,那么就需要我们的分布式锁出场了。
如果使用传统的加锁方式,我们会对该代码片段加上 synchronized 关键字。如下代码所示

synchronized (this){// todo 业务逻辑
}

synchronized 关键字,只能控制该 JVM 进程中的锁资源控制,这一方法有着很大的局限性。主要也是完成 单体架构 或者 进程内 需要加锁的需求。
synchronized 底层也是通过获取 某个对象的 对象头,来获取一个 监听器锁,而我们知道对象是存储在 JVM 的堆区 的。
所以,synchronized 只是 JVM 层面的 锁,不能使用在 集群中。
分布式锁的实现方式也有多种如:Redis分布式锁、zookeeper分布式锁等,本篇主要介绍 Redis 分布式锁。

redis为什么能实现分布式锁?

单线程模型介绍

我们知道 Redis 是一个以 键值对 存储的 nosql,所以使用 Redis 实现的分布式锁将以数据的形式作为 锁资源 存入redis。作为 “锁” 就要求在某一时刻,只会有一个线程在执行该片段。即串行执行加锁片段

Redis主线程(读写线程)模型 就是 单线程 的。也就是说在用户的请求到来时的同一时刻只会有一个线程在执行 Redis数据 相关的操作。

如图:

redis 中存入锁数据之后,第二个操作 redis 的线程(即便是从另外一个服务器来请求的线程)能够立刻得到 锁的状态(已存在该锁)。从而实现对集群的指定代码片段进行加锁

如何实现redis分布式锁?

前置知识:

  • redis 的命令,平时使用的最多的就是 set | get
  • 为实现分布式锁的特性,我们需要保证原子性,一般redis会使用 setnx 来实现
  • setnx 在redis中,如果本来有该缓存数据,则不会更新数据,否则反之
  • 在使用 java 的 api中如:RedisTemplate,该命令会根据更新状态返回一个 布尔值,如果插入成功则返回 true
  • setnx key value

Redis分布式锁的实现主要模型步骤:

  1. 在第一个线程访问时在 Redis 中添加一项缓存数据作为 锁资源
  2. 每个线程在执行该片段开始时,就会执行 setnx 命令进行缓存锁资源更新
  3. 如果更新失败,也会时返回值为 false,则说明有线程正在执行该片段。这时可以选择阻塞线程或给用户反馈一些提示。(如:系统繁忙之类的提示)
  4. 在线程结束时,需要主动删除该锁资源,让接下来的还未执行的线程进行争夺。

代码演示:

try{// 获取分布式锁Boolean lock = redisTemplate.opsForValue().setIfPresent("lock", "resource");// 如果锁资源未正常更新,则返回提示if(!lock){return "系统繁忙";}// 如果正常更新,则进行业务逻辑代码// todo 业务逻辑}finally {// 执行完成后,删除锁redisTemplate.delete("lock");
}

setIfPrefent() 方法是 RedisTemplate 中的api,相当于 setnx命令。

在执行业务逻辑代码时该服务挂掉了怎么办?

finally 只能处理 异常 出现的错误,如果执行业务逻辑时挂掉,说明锁已经加上,但是却没有删除。
这个时候说明 锁永远的留在了 Redis 中。那么所有的用户线程就都进行了阻塞。这种情况在我们的 生产环境 肯定是不允许出现的。

解决方案:利用 Redis 的 过期策略,为该锁资源添加 过期时间。

代码参考:

try{// 获取分布式锁Boolean lock = redisTemplate.opsForValue().setIfPresent("lock", "resource", 10, TimeUnit.SECONDS);// 如果锁资源未正常更新,则返回提示if(!lock){return "系统繁忙";}// 如果正常更新,则进行业务逻辑代码// todo 业务逻辑}finally {// 执行完成后,删除锁redisTemplate.delete("lock");
}

这样即便服务挂掉了,在到了过期时间之后,该锁资源也会自动释放

如果运行时间超过了过期时间怎么办?

运行时间超过了过期时间,在第一个线程没有全部执行完时,第二个线程就开始执行了。如下图模拟的场景所示:假设线程一共需要执行 15s,但是 Redis锁 过期时间只有 10s

这样就违背了分布式锁的作用。而因为 线程1 的锁已经被过期了,线程2马上就能得到锁。

出现的新问题有:

  1. 原本应该串行的两个线程,有了并发的情况。这可能违背我们所设想的情况,而出现不可预料的错误。
  2. 由于线程1 还没结束,线程2重新加了锁。而不久之后 线程1 结束了,又执行了删除锁的操作,导致线程2 刚加的锁 就被释放了

解决方案:

问题1:创建出分线程对过期时间进行 “续命”, 即延长过期时间

问题2:对每个线程存入值时创建一个线程标识,在执行删除操作时,核对自己的标识,如果是自己当时创建的锁,才执行删除操作。

代码参考:

String clientID = UUID.randomUUID().toString();// 问题2:创建线程标识,并存入redis;
try{// 获取分布式锁Boolean lock = redisTemplate.opsForValue().setIfPresent("lock", clientID, 10, TimeUnit.SECONDS);// 如果锁资源未正常更新,则返回提示if(!lock){return "系统繁忙";}// 问题1 创建线程续命new Thread(new Runnable() {@Overridepublic void run() {// 对 redis的锁过期时间进行续命}}).start();// 如果正常更新,则进行业务逻辑代码// todo 业务逻辑}finally {// 执行完成后,判断为自己创建的锁,则删除锁if(clientID.equals(redisTemplate.opsForValue().get("lock"))){redisTemplate.delete("lock");}}

创建出分线程的时机应该在判断是否已存在后 立刻 创建,避免因前面代码执行时间过长而导致来不及续命。
现在看来好像分布式锁已经是一个比较完善了,但仍然有待优化也需要根据自己的业务逻辑代码进行修改和设计

代码模板

在实际开发中还是不建议直接通过 Thread 类来进行创建线程,这里模板使用 JUC 提供的,ScheduledThreadPoolExecutor 类来实现线程管理

	// 该线程池能够轻松帮助我们实现有关时间控制的任务@ResourceScheduledThreadPoolExecutor scheduledThreadPoolExecutor;// ----------- 业务方法分布式锁片段 --------------ScheduledFuture<?> addLockLifeThread = null;try{// 创建线程id, 用作判断String clientId = UUID.randomUUID().toString();// 设置分布式锁Boolean lock = redisTemplate.opsForValue().setIfPresent(LOCK_KEY, clientId, LOCK_TTL, TimeUnit.SECONDS);if (lock == null || !lock) {// todo 如果没有拿到锁优化为阻塞,不要直接返回return false;}// 使用线程池创建定时任务线程addLockLifeThread = scheduledThreadPoolExecutor.scheduleAtFixedRate(() -> {// lock锁续命lengthenLockLife(clientId);}, ADD_LOCK_TTL, ADD_LOCK_TTL, TimeUnit.SECONDS); // 后面的参数表示,ADD_LOCK_TTL秒后,开始第1次执行,每隔ADD_LOCK_TTL秒在执行一次// ===== todo 完成需要进行加锁的业务逻辑 ==========} catch (Exception e){log.info("执行出错:{}", e.getMessage());}finally{// 关闭续命线程,释放锁资源if(addLockLifeThread != null){addLockLifeThread.cancel(true);}redisTemplate.delete(LOCK_KEY);}// -----------------------------------------------/*** 分布式锁进行续命** @param clientId 创建的线程id*/
public void lengthenLockLife(String clientId) {String redisLock = redisTemplate.opsForValue().get(LOCK_KEY);if (clientId.equals(redisLock)) {// 如果是此线程加的锁,进行续命操作redisTemplate.expire(LOCK_KEY, LOCK_TTL, TimeUnit.SECONDS);log.info("线程id {},进行续命", clientId);}
}

创建线程池时,需要 合理配置线程池参数。如:最多允许并发线程为 5 时,可将线程池 核心线程数 配置为 5等。
尽量避免线程添加到 阻塞队列 中,甚至是使用 非核心线程。当然具体情况需要根据业务情况而定。毕竟线程池相关的资源在使用过程中不容易被垃圾回收

redis分布式锁工具类

package com.common.util;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;import java.util.Collections;
import java.util.Objects;/*** @title  用于实现分布式锁* @author Created by wql on 2020/5/29.*/
@Component
public class RedisUtil {public static final String LOCK_PREFIX = "redis_lock_";private static final Long SUCCESS = 1L;// ms  默认10分钟public static final int LOCK_EXPIRE = 60 * 10;@AutowiredRedisTemplate redisTemplate;/*** 最终加强分布式锁** @param key key值* @return 是否获取到*/public boolean lock(String key) {String lock = LOCK_PREFIX + key;// 利用lambda表达式return (Boolean) redisTemplate.execute((RedisCallback) connection -> {long expireAt = System.currentTimeMillis() + LOCK_EXPIRE + 1;Boolean acquire = connection.setNX(lock.getBytes(), String.valueOf(expireAt).getBytes());if (acquire) {return true;} else {byte[] value = connection.get(lock.getBytes());if (Objects.nonNull(value) && value.length > 0) {long expireTime = Long.parseLong(new String(value));if (expireTime < System.currentTimeMillis()) {// 如果锁已经过期byte[] oldValue = connection.getSet(lock.getBytes(), String.valueOf(System.currentTimeMillis() + LOCK_EXPIRE + 1).getBytes());// 防止死锁return Long.parseLong(new String(oldValue)) < System.currentTimeMillis();}}}return false;});}/*** 获取锁* @param lockKey* @param value* @param expireTime:单位-秒* @return*/public  boolean getLock(String lockKey, String value, Integer expireTime){if(StringUtils.isTrimBlank(expireTime)){expireTime = LOCK_EXPIRE;}boolean ret = false;try{String script = "if redis.call('setNx',KEYS[1],ARGV[1]) then if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('expire',KEYS[1],ARGV[2]) else return 0 end end";RedisScript<String> redisScript = new DefaultRedisScript<>(script, String.class);Object result = redisTemplate.execute(redisScript, Collections.singletonList(LOCK_PREFIX+lockKey),value,expireTime);if(SUCCESS.equals(result)){return true;}}catch(Exception e){return false;}return ret;}/*** 释放锁* @param lockKey* @param value* @return*/public  boolean releaseLock(String lockKey, String value) {String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";RedisScript<String> redisScript = new DefaultRedisScript<>(script, String.class);Object result = redisTemplate.execute(redisScript, Collections.singletonList(LOCK_PREFIX+lockKey), value);if (SUCCESS.equals(result)) {return true;}return false;}/*** 删除锁** @param key*/public void delete(String key) {redisTemplate.delete(LOCK_PREFIX+key);}}

原文章地址:
https://blog.csdn.net/programming132/article/details/129196336

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2024全球数字经济大会:大模型时代下DataOps驱动企业数智化升级

在会上,白鲸开源CEO 郭炜于「开放原子开源数据库生态论坛」上发分享了题为 《大模型时代下DataOps驱动企业数智化升级》 的演讲,并在「开源与闭源,在行业应用中的发展潜能」的圆桌对话中分享了自己的开源心得体会与经验。7月5日,以“开源生态筑基础,数字经济铸未来”为主题…

多进程优化顶点小说爬虫:加速爬取速度的深度优化策略

本文介绍了如何通过引入多进程技术对顶点小说爬虫进行进阶优化,显著提升了数据爬取效率。首先分析了单进程爬虫面临的瓶颈与挑战,随后详细讨论了多进程并行爬取的实施方法及其在提升效率方面的优势。顶点小说进阶(多进程+协程) 建议: 看之前可以先看我之前发布的文章(异步…

从数据到洞察:DataOps加速AI模型开发的秘密实践大公开!

在AI驱动的商业世界中,DataOps作为连接数据与洞察的桥梁,正迅速成为企业数据战略的核心。在WOT全球技术创新大会2024北京站,白鲸开源联合创始人&CTO 代立冬 在「大数据技术与基础设施」专场深入分析DataOps的核心理念、AI大模型开发流程,并通过白鲸开源科技的实践案例,…

powerquery刷新后固定列宽度

依次打开“表格工具”->“设计”->“属性”->“调整列宽”, 取消“调整列宽”选项

camunda快速入门(三):设计表单和审批流程

在本节中,您将学习如何使用camunda表单设计器设计一个在线表单,用流程建模工具设计一个审批流, 即BPMN 2.0 用户任务让人类参与到您的流程中。三、设计一个人工任务流程并配置表单 在本节中,您将学习如何使用 BPMN 2.0 用户任务让人类参与到您的流程中。 1、添加用户任务活…

微信电脑版v3.9.11.17防撤回插件

下载链接:https://download.csdn.net/download/hello_hlqk/89525196?spm=1001.2101.3001.9500 通过该插件,可以看到对方撤回的消息。 修改原理,如下图:使用说明: 解压后,双击start.bat来运行软件

大厂面试高频题——二分查找

35. 搜索插入位置 给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。 请必须使用时间复杂度为 O(log n) 的算法。 思考 二分模板题 class Solution:def searchInsert(self, nums: List[int], target…

如何解决网页打开加载缓慢问题

以我为例 今天打开B站发现很多视频打开之后一直是加载状态,加载半天也打不开,还有知乎跳转到csdn也无法正常打自己捣鼓的许久发现可以通过修改DNS来解决 首先打开此电脑→找到网络→右键点击属性→左上角更改适配器设置→选择你的网络双击→点击属性→找到Internet 协议版本4…

服务器开发——setsockopt函数

setsockopt() setsockopt() 是一个用于设置套接字选项的 Unix 系统调用。它允许程序员修改套接字的行为,以适应不同的网络环境和应用程序需求。 1. 函数介绍 函数原型: #include <sys/socket.h> /*** level:选项所在的协议层。例如,SOL_SOCKET 表示套接字层,IPPRO…

Python OpenCv对规则物体进行实时检测

很多情况需要对物体进行检测,常规的方法也有很多种。但是检测出来的边缘一般都是非常多,结果也是非常杂乱的,显然这种结果不是我们想要的。如果颜色相较于背景非常鲜艳的可以调节hsv阈值再进行检测,如果是一直在运动的物体可以通过帧差法进行物体检测,还有很多高深的算法也…

学习canvas(一些常用api)

当然,以下是这些常用Canvas API的总结,按照Markdown格式编写: 常用Canvas API总结 1. 获取绘图上下文 const canvas = document.getElementById(myCanvas); const ctx = canvas.getContext(2d); // 或 webgl2. 绘制矩形 ctx.fillStyle = blue; ctx.fillRect(10, 10, 150, 10…

uniapp 解决本地跨域问题

让每一滴智慧绘制成一条不归路!