【JAVA高级】 redis分布式双重加锁(业务校验:防止接口并发调用时数据重复)

embedded/2024/10/19 7:01:58/

文章目录

    • 此问题的考虑思路
    • 使用Redis的key-value锁的基本思路
    • 结合Redis数据结构实现避免重复
    • 注意事项
    • 实现代码
      • 只避免 name和age的重复
      • 避免 name和age的和age和sex重复:使用双重的分布式锁实现:

在这里插入图片描述

背景:在日常开发过程中,遇到了一个需求,比如有一个对象User(name,age、sex)有三个属性,现在需要用户新增接口中,防止此接口被多人同时请求访问,产生了姓名&年龄相同的,还有年龄&性别相同的数据;

此问题的考虑思路

如果一个线程调用用户新增接口的时候,在业务中通过查询数据库中是否已有相关数据,从而不抛出异常提示,不让做保存到数据库的操作;这种考虑是我们最常见的考虑内容。还有个问题,如果是外部系统,涉及的操作并发量特别的大,那调用这个接口的并发量也很大的话,单纯在通过校验库中是否有重复的数据防止重复数据插入只能阻止一部分问题数据的入库。如果同时有两个用户甲乙,填写的姓名和年龄
(年龄和性别是同样的考虑方法);此时从库中查了,没有已有的数据,此时为了防止这两个用户甲乙操作的重复数据同时入库的情况,我们就得加上一个分布式锁了(如果在单体应用中可以使用synchronized),分布式架构中需要使用Redis分布式锁或者Redission分布式锁来实现相应的控制了;

在设计分布式Redis锁以避免在新增User时出现同name和age组合,或者同age和sex组合的情况,你需要构建一个能够唯一标识这些条件的key。由于Redis锁通常用于确保操作的原子性,而你的需求是检查并避免重复数据,这里实际上可能更偏向于使用Redis的其它数据结构(如集合、有序集合或哈希表)来辅助实现,而不是仅仅使用单独的key-value锁

使用Redis的key-value锁的基本思路

**1.定义锁的key:**锁的key应该能够唯一标识你想要保护的资源或操作。在你的场景中,由于涉及到多个字段的组合检查,你可以考虑将这些
字段组合成一个字符串作为key。例如:

  • 对于name和age的组合,可以使用user🔒name:{name}:age:{age}。
  • 对于age和sex的组合,可以使用user🔒age:{age}:sex:{sex}。
    **2.设置锁:**在尝试新增User之前,先尝试设置这个锁。如果锁设置成功(即没有其他进程或线程持有这个锁),则继续执行检查逻辑。
    **3.检查并插入:**在锁的保护下,检查数据库中是否已经存在具有相同name和age或age和sex组合的User。如果不存在,则执行插入操作。
    **4.释放锁:**无论操作成功还是失败,最后都要释放锁,以便其他进程或线程可以获取锁并执行操作。

结合Redis数据结构实现避免重复

然而,更有效的方法可能是使用Redis的集合(Set)或有序集合(Sorted Set)来存储已经存在的组合,并检查新组合是否已存在。

1.使用集合:

  • 对于name和age的组合,可以创建一个集合user:name_age,其中每个元素都是{name}:{age}的字符串。
  • 对于age和sex的组合,可以创建另一个集合user:age_sex,其中每个元素都是{age}:{sex}的字符串。
  • 在新增User时,先检查相应的集合中是否已经存在该组合。如果不存在,则添加到集合中,并执行数据库插入操作。
    2.使用有序集合(如果需要按某种顺序排序):
  • 类似于集合,但你可以为元素指定一个分数(score),以便按特定顺序存储和检索元素。

注意事项

**性能考虑:**随着集合中元素的增加,检查操作可能会变慢。因此,你可能需要考虑使用哈希表或其他数据结构来优化查找性能。
**事务性:**确保检查集合和插入数据库的操作是原子性的,以防止在检查之后但在插入之前发生数据变化。
**锁的超时:**设置锁的超时时间以防止死锁。
**锁的粒度:**根据你的应用场景,你可能需要调整锁的粒度。例如,如果操作非常频繁,并且可以接受一定程度的重复检查,则可以考虑放宽锁的粒度或使用更轻量级的同步机制。

实现代码

只避免 name和age的重复

下面的实现的一些代码:希望能帮到大家理解思路。

java">import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.data.redis.core.RedisTemplate;  
import org.springframework.data.redis.core.ValueOperations;  
import org.springframework.stereotype.Service;  import java.util.concurrent.TimeUnit;  @Service  
public class UserService {  @Autowired  private RedisTemplate<String, String> redisTemplate;  // 假设的锁过期时间  private static final long LOCK_EXPIRATION_TIME = 10L; // 10秒  // 尝试获取锁  private boolean tryLock(String key) {  ValueOperations<String, String> opsForValue = redisTemplate.opsForValue();  // 尝试设置锁,如果键不存在则设置成功,并设置过期时间  return opsForValue.setIfAbsent(key, "locked", LOCK_EXPIRATION_TIME, TimeUnit.SECONDS);  }  // 释放锁  private void releaseLock(String key) {  redisTemplate.delete(key);  }  // 新增User的逻辑  public void addUserIfNotExists(User user) {  String nameAgeLockKey = "user:lock:name:" + user.getName() + ":age:" + user.getAge();  // 尝试获取锁  ,获取锁成功才会继续执行下面业务上的校验if (tryLock(nameAgeLockKey)) {  try {  // 在这里执行数据库检查(是否已存在同name和age)  // 如果不存在,则执行插入操作  // 假设检查通过,执行插入操作(这里省略了具体的数据库操作)  System.out.println("User added successfully");  } finally {  // 释放锁  releaseLock(nameAgeLockKey);  }  } else {  // 未能获取锁,可能是其他进程正在处理相同的组合  System.out.println("Failed to acquire lock(s), user addition may be in progress");  }  }  // ... 其他代码 ...  
}

避免 name和age的和age和sex重复:使用双重的分布式锁实现:

java">import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.data.redis.core.RedisTemplate;  
import org.springframework.data.redis.core.ValueOperations;  
import org.springframework.stereotype.Service;  import java.util.concurrent.TimeUnit;  @Service  
public class UserService {  @Autowired  private RedisTemplate<String, String> redisTemplate;  // 假设的锁过期时间  private static final long LOCK_EXPIRATION_TIME = 10L; // 10秒  // 尝试获取锁  private boolean tryLock(String key) {  ValueOperations<String, String> opsForValue = redisTemplate.opsForValue();  // 尝试设置锁,如果键不存在则设置成功,并设置过期时间  return opsForValue.setIfAbsent(key, "locked", LOCK_EXPIRATION_TIME, TimeUnit.SECONDS);  }  // 释放锁  private void releaseLock(String key) {  redisTemplate.delete(key);  }  // 新增User的逻辑  public void addUserIfNotExists(User user) {  //加两次锁String nameAgeLockKey = "user:lock:name:" + user.getName() + ":age:" + user.getAge();  String ageSexLockKey = "user:lock:age:" + user.getAge() + ":sex:" + user.getSex();  // 尝试获取两个锁  if (tryLock(nameAgeLockKey) && tryLock(ageSexLockKey)) {  try {  // 在这里执行数据库检查(是否已存在同name和age或同age和sex的User)  // 如果不存在,则执行插入操作  // 假设检查通过,执行插入操作(这里省略了具体的数据库操作)  System.out.println("User added successfully");  } finally {  // 释放锁  释放两次releaseLock(nameAgeLockKey);  releaseLock(ageSexLockKey);  }  } else {  // 未能获取锁,可能是其他进程正在处理相同的组合  System.out.println("Failed to acquire lock(s), user addition may be in progress");  }  }  // ... 其他代码 ...  
}

双重加锁的 注意点
上面的代码示例简化了错误处理和重试逻辑。在实际应用中,你可能需要处理各种异常情况,例如Redis服务器不可用、锁被意外删除或过期等。此外,如果业务逻辑复杂或执行时间较长,你可能需要考虑使用更高级的锁机制,如Redis的发布/订阅模式、Lua脚本或Redis的RedLock算法来确保锁的安全性和可靠性。

另外,请注意,tryLock 方法中的 setIfAbsent 操作是原子的,这意味着它会在单个Redis命令中完成检查和设置操作,从而避免了竞态条件。但是,由于网络延迟、Redis服务器性能等因素,多个客户端仍可能几乎同时尝试获取相同的锁。因此,即使使用了锁,也需要谨慎地设计你的业务逻辑和错误处理策略。


http://www.ppmy.cn/embedded/121068.html

相关文章

无人机视角垃圾检测数据集,26700余张无人机图像,超过4万标注信息,共3.6GB数据量,可用于环卫快速检查,垃圾快速定位等应用。

无人机视角垃圾检测&#xff0c;26700余张无人机图像&#xff0c;超过4万标注信息&#xff0c;共3.6GB数据量&#xff0c;可用于环卫快速检查&#xff0c;垃圾快速定位等应用。 名称 无人机视角垃圾检测数据集 规模 图像数量&#xff1a;26700余张标注信息&#xff1a;超过4…

微服务nginx解析部署使用全流程

目录 1、nginx介绍 1、简介 2、反向代理 3、负载均衡 2、安装nginx 1、下载nginx 2、解压nginx安装包 3、安装nginx​编辑 1、执行configure命令 2、执行make命令 4、启动nginx 1、查找nginx位置并启动 2、常用命令 3、反向代理 1、介绍反向代理配置 1、基础配置…

【Linux进程间通信】Linux匿名管道详解:构建进程间通信的隐形桥梁

&#x1f4dd;个人主页&#x1f339;&#xff1a;Eternity._ ⏩收录专栏⏪&#xff1a;Linux “ 登神长阶 ” &#x1f339;&#x1f339;期待您的关注 &#x1f339;&#x1f339; ❀Linux进程间通信 &#x1f4d2;1. 进程间通信介绍&#x1f4da;2. 什么是管道&#x1f4dc;3…

电脑usb接口封禁如何实现?5种禁用USB接口的方法分享!(第一种你GET了吗?)

“防患于未然&#xff0c;安全始于细节。”在信息技术飞速发展的今天&#xff0c;企业的信息安全问题日益凸显。 USB接口作为数据传输的重要通道&#xff0c;在带来便利的同时&#xff0c;也成为了数据泄露和安全风险的高发地。 因此&#xff0c;对电脑USB接口进行封闭管理&a…

Web3.0 应用项目

Web3.0 是下一代互联网的概念&#xff0c;旨在去中心化、用户拥有数据控制权和通过区块链技术实现信任的网络。Web3.0的应用项目主要集中在区块链、加密货币、去中心化应用 (DApps)、去中心化金融 (DeFi)、NFT&#xff08;非同质化代币&#xff09;等领域。以下是一些典型的 We…

(c++)在堆区创建一个数组并且访问与释放

在堆区创建一个数组&#xff0c;然后利用一个指针指向这个数组的首地址&#xff0c;通过这个指针来访问这个数组。 代码展示了三种赋值的方式&#xff1a; 1.直接利用数组访问赋值 2.利用循环结构&#xff08;和1原理一样&#xff09; 3.循环结构键盘输入赋值 然后输出这个…

STM32重启源深度解析

文章目录 STM32重启源深度解析一、STM32重启概述二、硬件层面的重启源1、电源异常电压不稳定&#xff1a;电源供电不足&#xff1a; 2、复位电路故障复位引脚异常&#xff1a;复位电路设计不合理&#xff1a; 3、外部干扰电磁干扰&#xff1a;静电干扰&#xff1a; 三、软件层面…

墙绘艺术市场的数字化转型:SpringBoot案例

1 绪论 1.1 研究背景 当前社会各行业领域竞争压力非常大&#xff0c;随着当前时代的信息化&#xff0c;科学化发展&#xff0c;让社会各行业领域都争相使用新的信息技术&#xff0c;对行业内的各种相关数据进行科学化&#xff0c;规范化管理。这样的大环境让那些止步不前&#…