Redis 篇-深入了解基于 Redis 实现分布式锁(解决多线程安全问题、锁误删问题和确保锁的原子性问题)

本文主要是介绍Redis 篇-深入了解基于 Redis 实现分布式锁(解决多线程安全问题、锁误删问题和确保锁的原子性问题),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

🔥博客主页: 【小扳_-CSDN博客】
❤感谢大家点赞👍收藏⭐评论✍

文章目录

        1.0 分布式锁概述

        1.1 Redis 分布式锁实现思路

        1.2 实现基本的分布式锁

        2.0 Redis 分布式锁误删问题

        2.1 解决 Redis 分布式锁误删问题

        3.0 Redis 分布式锁原子性问题

        3.1 Lua 脚本解决多条命令原子性问题

        4.0 基本 Redis 实现的分布式锁代码


        1.0 分布式锁概述

        分布式锁是一种用于在分布式系统中控制对共享资源的访问的机制。它确保在同一时间只有一个进程或线程能够访问特定的资源,从而避免数据冲突和不一致性。

        当项目部署到集群中,如果只用 sychronized 锁是不足以在集群环境中确保线程安全,简单的说一下原因:在集群中,有多个 JVM ,就会有多个字符串常量池,锁的作用域仅限于当前 JVM 的对象或类。当多个 JVM 访问同一个资源时,每个 JVM 都会有自己的锁,导致无法实现对共享资源的有效控制。所以出现锁不住资源的情况。

        因此需要用分布式锁来完成。

常见的实现方式:

        1)数据库锁:利用数据库的事务机制来实现锁定。

        2)Redis 锁:使用 Redis 的 setnx 命令来实现分布式锁。

        3)Zookeeper 锁:利用 Zookeeper 的临时节点和顺序节点来实现分布式锁。

        1.1 Redis 分布式锁实现思路

        使用 Redis 的 SETNX 命令来实现分布式锁。

        首先,先介绍 setnx 的特性,一旦使用 setnx 设置某一个字段时,当设置成功之后再使用 setnx 设置重复字段,则会出现失败情况。Redis 分布式锁就是利用该特性来实现锁。当然,这只是大概的情况,还有很多细节需要注意。

        1)尝试获取锁的思路:

        先使用 setnx 设置某一个字段,如果返回值为成功,则获取锁成功;如果返回值为失败,则获取锁失败,那么获取锁失败可以根据具体业务情况来安排,比如可以先等待一段时间,接着再去尝试获取锁、还可以直接抛出异常等。

        还要考虑一种情况,当出现锁忘记释放了,则该字段就会一直存在缓存中,随着时间积累,缓存空间就会慢慢的减少,因此,给该字段设置 TTL ,超时时间。

        2)释放锁的思路:

        一般来说,直接用 del 命令,删除某一个字段即可。

        以上获取锁和释放锁都是最基础的形态,还有很多情况需要考虑,因此还不能在实战中使用。

        1.2 实现基本的分布式锁

尝试获取锁:

import cn.hutool.core.util.BooleanUtil;
import org.springframework.data.redis.core.StringRedisTemplate;import java.util.concurrent.TimeUnit;public class RedisLock{private final StringRedisTemplate stringRedisTemplate;private final String name;private static final String KEY_PREFIX = "lock:";public RedisLock(StringRedisTemplate stringRedisTemplate, String name) {this.stringRedisTemplate = stringRedisTemplate;this.name = name;}/*** 尝试获取锁* @param time* @param unit* @return*/public boolean tryLock(long time,TimeUnit unit){long threadId = Thread.currentThread().getId();Boolean b = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId + "", time, unit);return BooleanUtil.isTrue(b);}}

        在创建 RedisLock 对象的时候,需要转递 StringRedisTemplate 类型对象,还有业务名称 name 作为锁绑定的具体对象,且在设置 setnx 的时候,value 设置为当前线程 id ,有助于查看当前锁被那一个线程获取了。最后需要注意,不可直接将类型 Boolean 类型的对象直接返回,因为由 Boolea 会自动拆箱 boolean 基本类型对象,在拆箱过程中容易出现空指针异常。

释放锁:

import cn.hutool.core.util.BooleanUtil;
import org.springframework.data.redis.core.StringRedisTemplate;import java.util.concurrent.TimeUnit;public class RedisLock{private final StringRedisTemplate stringRedisTemplate;private final String name;private static final String KEY_PREFIX = "lock:";public RedisLock(StringRedisTemplate stringRedisTemplate, String name) {this.stringRedisTemplate = stringRedisTemplate;this.name = name;}/*** 尝试获取锁* @param time* @param unit* @return*/public boolean tryLock(long time,TimeUnit unit){long threadId = Thread.currentThread().getId();Boolean b = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId + "", time, unit);return BooleanUtil.isTrue(b);}/*** 释放锁*/public void unLock(){stringRedisTemplate.delete(KEY_PREFIX+name);}}

        2.0 Redis 分布式锁误删问题

        在获取锁之后,正常执行完逻辑任务,再释放锁。这一过程按理来说,不会出现分布式锁被误删的情况,但是再考虑到一下情况:

        假设线程一正常获取锁之后,执行任务,但是该任务出现了阻塞情况,等待的时间较久,此时当锁到过期时间之后,就会自动被释放了,当时此时线程一还不知道当前锁被释放了,就在这时候,线程二来正常的获取锁,因为锁已经被释放了,所以线程二是可以获取锁成功的,接着,线程二获取锁之后,就开始执行任务了,此刻线程一任务执行完之后,会直接释放锁,这就出现线程一误删了线程二的锁问题。

如图:

        出现误删问题,就有可能出现多个线程获取锁的情况发生,从而出现线程安全问题,所以需要解决该问题。

        2.1 解决 Redis 分布式锁误删问题

        为了解决 Redis 分布式锁被误删的问题,可以想到的办法是:在释放锁之前,判断当前的锁 “是否” 是自己之前获取的锁,如果是,则可以直接释放锁;如果不是,则什么都不用做。

        具体如何判断当前锁 “是否” 是自己之前获取的锁呢?

        之前我们在设置 setnx 的时候,将 value 设置为线程 id ,那么就可以在释放锁的时候通过判断当前线程 id 与获取锁的时候设置 value 的线程 id 值两者是否一致。

        但是如果在集群环境中只判断线程 id 是否相同还不足以确保不会出现误删的情况发生,因为在集群环境中,有多个 JVM ,则非常有可能出现线程 id 相同的情况,所以还需要加上 UUID 来设置前缀,确保每一个 JVM 的前缀都是不一样的,结合起来就可以解决该情况了。

代码如下:

import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.BooleanUtil;
import org.springframework.data.redis.core.StringRedisTemplate;import java.util.concurrent.TimeUnit;public class RedisLock{private final StringRedisTemplate stringRedisTemplate;private final String name;private static final String KEY_PREFIX = "lock:";private static final String VAL_PREFIX = UUID.randomUUID().toString(true) + "-";public RedisLock(StringRedisTemplate stringRedisTemplate, String name) {this.stringRedisTemplate = stringRedisTemplate;this.name = name;}/*** 尝试获取锁* @param time* @param unit* @return*/public boolean tryLock(long time,TimeUnit unit){String value = VAL_PREFIX + Thread.currentThread().getId();Boolean b = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, value, time, unit);return BooleanUtil.isTrue(b);}/*** 释放锁*/public void unLock(){//先获取value值String newValue = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);String oldValue = VAL_PREFIX+Thread.currentThread().getId();//再判断两者是否相同if (oldValue.equals(newValue)){//相同情况,直接删除即可stringRedisTemplate.delete(KEY_PREFIX+name);}//不相同情况,什么都不做}}

        3.0 Redis 分布式锁原子性问题

        由于在释放锁之前加上了,判断当前锁 "是否" 是自己的代码,从而有可能出现了原子性问题,当判断完之后,出现线程阻塞,导致释放锁时机延长,直到超过了过期时间,则锁就会被自动释放,当线程阻塞完毕之后,再来释放锁,此时有可能出现误删锁。

如图:

        因此需要保证判断锁和释放锁具有原子性,要么一起执行,要么都不执行。

        3.1 Lua 脚本解决多条命令原子性问题

        Redis 提供了 Lua 脚本功能,在一个脚本中编写多条 Redis 命令,确保多条命令执行时的原子性。 基本语法可以参数网站:Lua 教程 | 菜鸟教程 (runoob.com)

        使用 Lua 脚本语言编写 Redis 多条命令,先根据 key 来查询 value ,再判断 value 与当前线程标识是否相同,如果相同,则进行删除缓存;如果不相同,则什么都不需要做。

Lua 脚本如下:

-- 这里的 KEYS[1] 就是锁的Key,这里的ARGV[1]就是当前线程标识
-- 获取锁中的标识,判断是否与当前线程标识一致
if(redis.call('GET',KEYS[1]) == ARGV[1]) then-- 一致,则删除锁return redis.call('DEL',KEYS[1])
end
-- 不一致,则直接返回
return 0

        在 Java 中使用 StringRedisTemplate 对象来调用 execute 方法从而调用 Lua 脚本。

        需要传的参数:

        1)DefaultRedisScript 类型对象,该对象主要用来将读取 Lua 脚本。

        2)List<K> keys 数组对象,主要是传入 key 的实参,因为在 Lua 脚本中设置是形参,因此根据实际情况来传入实参。

        3)Object... args 任意对象,根据实际情况来传入除了 KEY 以外的实参。

        Lua 中的形参 KEYS[1] 对应的实参为 List<K> keys 数组对象,而形参 ARGV[1] 对应的实参为 Object... args 任意对象。

        在 Lua 中使用 redis.call() 方法,可以理解成调用该方法来实现对 Redis 操作。 

具体代码实现:

    private static final DefaultRedisScript<Long> defaultRedisScript;static {defaultRedisScript = new DefaultRedisScript<>();defaultRedisScript.setLocation(new ClassPathResource("unlock.lua"));defaultRedisScript.setResultType(Long.class);}/*** 释放锁*/public void unLock(){stringRedisTemplate.execute(defaultRedisScript,Collections.singletonList(KEY_PREFIX + name),VAL_PREFIX + Thread.currentThread().getId());}

        最后,使用 Lua 脚本实现对 Redis 多条命令的操作,再由 Java 读取操作 Lua 脚本语言,从而实现解决原子性问题。

        4.0 基本 Redis 实现的分布式锁代码

        解决了在集群环境下,确保线程安全问题,且解决了误删锁问题和解决原子性问题。

代码如下:

import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.BooleanUtil;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;import java.util.Collections;
import java.util.concurrent.TimeUnit;public class RedisLock{private final StringRedisTemplate stringRedisTemplate;private final String name;private static final String KEY_PREFIX = "lock:";private static final String VAL_PREFIX = UUID.randomUUID().toString(true) + "-";private static final DefaultRedisScript<Long> defaultRedisScript;static {defaultRedisScript = new DefaultRedisScript<>();defaultRedisScript.setLocation(new ClassPathResource("unlock.lua"));defaultRedisScript.setResultType(Long.class);}public RedisLock(StringRedisTemplate stringRedisTemplate, String name) {this.stringRedisTemplate = stringRedisTemplate;this.name = name;}/*** 尝试获取锁* @param time* @param unit* @return*/public boolean tryLock(long time,TimeUnit unit){String value = VAL_PREFIX + Thread.currentThread().getId();Boolean b = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, value, time, unit);return BooleanUtil.isTrue(b);}/*** 释放锁*/public void unLock(){stringRedisTemplate.execute(defaultRedisScript,Collections.singletonList(KEY_PREFIX + name),VAL_PREFIX + Thread.currentThread().getId());}/*    public void unLock(){//先获取value值String newValue = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);String oldValue = VAL_PREFIX+Thread.currentThread().getId();//再判断两者是否相同if (oldValue.equals(newValue)){//相同情况,直接删除即可stringRedisTemplate.delete(KEY_PREFIX+name);}//不相同情况,什么都不做}*/}

这篇关于Redis 篇-深入了解基于 Redis 实现分布式锁(解决多线程安全问题、锁误删问题和确保锁的原子性问题)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!


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

相关文章

python操作redis基础

《python操作redis基础》Redis(RemoteDictionaryServer)是一个开源的、基于内存的键值对(Key-Value)存储系统,它通常用作数据库、缓存和消息代理,这篇文章... 目录1. Redis 简介2. 前提条件3. 安装 python Redis 客户端库4. 连接到 Re

Python+PyQt5实现MySQL数据库备份神器

《Python+PyQt5实现MySQL数据库备份神器》在数据库管理工作中,定期备份是确保数据安全的重要措施,本文将介绍如何使用Python+PyQt5开发一个高颜值,多功能的MySQL数据库备份工具... 目录概述功能特性核心功能矩阵特色功能界面展示主界面设计动态效果演示使用教程环境准备操作流程代码深度解

如何清理MySQL中的binlog问题

《如何清理MySQL中的binlog问题》:本文主要介绍清理MySQL中的binlog问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目http://www.chinasem.cn录清理mysql中的binlog1.查看binlog过期时间2. 修改binlog过期

golang float和科学计数法转字符串的实现方式

《golangfloat和科学计数法转字符串的实现方式》:本文主要介绍golangfloat和科学计数法转字符串的实现方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望... 目录golang float和科学计数法转字符串需要对float转字符串做处理总结golang float

linux lvm快照的正确mount挂载实现方式

《linuxlvm快照的正确mount挂载实现方式》:本文主要介绍linuxlvm快照的正确mount挂载实现方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录linux lvm快照的正确mount挂载1. 检查快照是否正确创建www.chinasem.cn2.

如何解决yum无法安装epel-release的问题

《如何解决yum无法安装epel-release的问题》:本文主要介绍如何解决yum无法安装epel-release的问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不... 目录yum无法安装epel-release尝试了第一种方法第二种方法(我就是用这种方法解决的)总结yum

利用Python实现时间序列动量策略

《利用Python实现时间序列动量策略》时间序列动量策略作为量化交易领域中最为持久且被深入研究的策略类型之一,其核心理念相对简明:对于显示上升趋势的资产建立多头头寸,对于呈现下降趋势的资产建立空头头寸... 目录引言传统策略面临的风险管理挑战波动率调整机制:实现风险标准化策略实施的技术细节波动率调整的战略价

使用Python和Tkinter实现html标签去除工具

《使用Python和Tkinter实现html标签去除工具》本文介绍用Python和Tkinter开发的HTML标签去除工具,支持去除HTML标签、转义实体并输出纯文本,提供图形界面操作及复制功能,需... 目录html 标签去除工具功能介绍创作过程1. 技术选型2. 核心实现逻辑3. 用户体验增强如何运行

SpringBoot实现Kafka动态反序列化的完整代码

《SpringBoot实现Kafka动态反序列化的完整代码》在分布式系统中,Kafka作为高吞吐量的消息队列,常常需要处理来自不同主题(Topic)的异构数据,不同的业务场景可能要求对同一消费者组内的... 目录引言一、问题背景1.1 动态反序列化的需求1.2 常见问题二、动态反序列化的核心方案2.1 ht

Python实现文件批量重命名器

《Python实现文件批量重命名器》在日常工作和学习中,我们经常需要对大量文件进行重命名操作,本文将介绍一个使用Python开发的文件批量重命名工具,提供了多种重命名模式,有需要的小伙伴可以了解下... 目录前言功能特点模块化设计1.目录路径获取模块2.文件列表获取模块3.重命名模式选择模块4.序列号参数配