关于SpringBoot 使用 Redis 分布式锁解决并发问题
作者:qhh0205 发布时间:2024-01-19 19:59:40
目录
问题背景
解决方案
主要实现原理:
可靠性:
SpringBoot 集成使用 Redis 分布式锁
使用示例
参考文档
问题背景
现在的应用程序架构中,很多服务都是多副本运行,从而保证服务的稳定性。一个服务实例挂了,其他服务依旧可以接收请求。但是服务的多副本运行随之也会引来一些分布式问题,比如某个接口的处理逻辑是这样的:接收到请求后,先查询 DB 看是否有相关的数据,如果没有则插入数据,如果有则更新数据。在这种场景下如果相同的 N 个请求并发发到后端服务实例,就会出现重复插入数据的情况:
解决方案
针对上面问题,一般的解决方案是使用分布式锁来解决。同一个进程内的话用本进程内的锁即可解决,但是服务多实例部署的话是分布式的,各自进程独立,这种情况下可以设置一个全局获取锁的地方,各个进程都可以通过某种方式获取这个全局锁,获得到锁后就可以执行相关业务逻辑代码,没有拿到锁则跳过不执行,这个全局锁就是我们所说的分布式锁。分布式锁一般有三种实现方式:1. 数据库乐观锁;2. 基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁。
我们这里介绍如何基于 Redis 的分布式锁来解决分布式并发问题:Redis 充当获取全局锁的地方,每个实例在接收到请求的时候首先从 Redis 获取锁,获取到锁后执行业务逻辑代码,没争抢到锁则放弃执行。
主要实现原理:
Redis 锁主要利用 Redis 的 setnx 命令:
加锁命令:SETNX key value,当键不存在时,对键进行设置操作并返回成功,否则返回失败。KEY 是锁的唯一标识,一般按业务来决定命名。Value 一般用 UUID 标识,确保锁不被误解。
解锁命令:DEL key,通过删除键值对释放锁,以便其他线程可以通过 SETNX 命令来获取锁。
锁超时:EXPIRE key timeout, 设置 key 的超时时间,以保证即使锁没有被显式释放,锁也可以在一定时间后自动释放,避免资源被永远锁住。
可靠性:
为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:
互斥性。在任意时刻,保证只有一台机器的一个线程可以持有锁;
不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁;
具备非阻塞性。一旦获取不到锁就立刻返回加锁失败;
加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了;
SpringBoot 集成使用 Redis 分布式锁
写了一个 RedisLock 工具类,用于业务逻辑执行前加锁和业务逻辑执行完解锁操作。这里的加锁操作可能实现的不是很完善,有加锁和锁过期两个操作原子性问题,如果 SpringBoot 版本是2.x的话是可以用注释中的代码在加锁的时候同时设置锁过期时间,如果 SpringBoot 版本是2.x以下的话建议使用 Lua 脚本来确保操作的原子性,这里为了简单就先这样写:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* @description: Redis分布式锁实现工具类
* @author: qianghaohao
* @time: 2021/7/19
*/
@Component
public class RedisLock {
@Autowired
StringRedisTemplate redisTemplate;
/**
* 获取锁
*
* @param lockKey 锁
* @param identity 身份标识(保证锁不会被其他人释放)
* @param expireTime 锁的过期时间(单位:秒)
* @return
*/
public boolean lock(String lockKey, String identity, long expireTime) {
// 由于我们目前 springboot 版本比较低,1.5.9,因此还不支持下面这种写法
// return redisTemplate.opsForValue().setIfAbsent(lockKey, identity, expireTime, TimeUnit.SECONDS);
if (redisTemplate.opsForValue().setIfAbsent(lockKey, identity)) {
redisTemplate.expire(lockKey, expireTime, TimeUnit.SECONDS);
return true;
}
return false;
}
/**
* 释放锁
*
* @param lockKey 锁
* @param identity 身份标识(保证锁不会被其他人释放)
* @return
*/
public boolean releaseLock(String lockKey, String identity) {
String luaScript = "if " +
" redis.call('get', KEYS[1]) == ARGV[1] " +
"then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>();
redisScript.setResultType(Boolean.class);
redisScript.setScriptText(luaScript);
List<String> keys = new ArrayList<>();
keys.add(lockKey);
Object result = redisTemplate.execute(redisScript, keys, identity);
return (boolean) result;
}
}
使用示例
这里只贴出关键的使用代码,注意:锁的 key 根据自己的业务逻辑命名,能唯一标示同一个请求即可。value 这里设置为 UUID,为了确保释放锁的时候能正确释放(只释放自己加的锁)。
@Autowired
private RedisLock redisLock; // redis 分布式锁
String redisLockKey = String.format("%s:docker-image:%s", REDIS_LOCK_PREFIX, imageVo.getImageRepository());
String redisLockValue = UUID.randomUUID().toString();
try {
if (!redisLock.lock(redisLockKey, redisLockValue, REDIS_LOCK_TIMEOUT)) {
logger.info("redisLockKey [" + redisLockKey + "] 已存在,不执行镜像插入和更新");
result.setMessage("新建镜像频繁,稍后重试,锁占用");
return result;
}
... // 执行业务逻辑
catch (Execpion e) {
... // 异常处理
} finally { // 释放锁
if (!redisLock.releaseLock(redisLockKey, redisLockValue)) {
logger.error("释放redis锁 [" + redisLockKey + "] 失败);
} else {
logger.error("释放redis锁 [" + redisLockKey + "] 成功");
}
}
参考文档
https://www.jianshu.com/p/6c2f85e2c586
https://xiaomi-info.github.io/2019/12/17/redis-distributed-lock/
来源:https://blog.csdn.net/qianghaohao/article/details/119061642


猜你喜欢
- 在Apache, PHP, MySQL的体系架构中,MySQL对于性能的影响最大,也是关键的核心部分。对于Discuz!论坛程序也是如此,M
- 在Oracle数据库中,DBA可以通过观测一定的表或视图来了解当前空间的使用状况,进而作出可能的调整决定。 一.表空间的自由空间 通过对表空
- 这篇文章主要介绍了python中如何使用insert函数,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的
- 很多人说设计是力求细节的,在网页设计里表达出的细节就是图标。图标在一个设计里带来了额外的注解并且使设计里的对象和元素引起用户的注意。以下介绍
- 前言何为爬虫,其实就是利用计算机模拟人对网页的操作例如 模拟人类浏览购物网站使用爬虫前一定要看目标网站可刑不可刑 :-)可以在目标网站添加/
- 1.find函数find() 方法检测字符串中是否包含子字符串 str ,如果指定 beg(开始) 和 end(结束) 范围,则检查是否包含
- 随着SaaS服务的流行,越来越多的人选择在各个平台上编写文档,制作表格并进行分享。同时,随着Markdown语法的破圈,很多平台开始集成支持
- 用过软件的朋友都知道,进度条是一个优秀软件的重要组成部分。它的存在能够使用户及时掌握程序的运行进度,确认应用程序正常工作。可是ASP中似乎没
- 使用runserver可以使我们的django项目很便捷的在本地运行起来,但这只能在局域网内访问,如果在生产环境部署django,就要多考虑
- 一、定位 oracle分两大块,一块是开发,一块是管理。开发主要是写写存储过程、触发器什么的,还有就是用Oracle的Develop工具做f
- 下面,小编将通过一组实例演示,让大家更直观,更清楚明白的了解要设置中文这一内容的操作步骤。首先展示实例代码:import pygamefro
- substr --- 取得部份字符串 语法 : string substr (string string, int start [, int
- 今天帮同事处理一个棘手的事情,问题是这样的:无论在客户机用哪个版本的mysql客户端连接服务器,发现只要服务器端设置了character-s
- 前言前面已经讲述了如何获取股票的k线数据,今天我们来分析一下股票的资金流入情况,股票的上涨和下跌都是由资金推动的,这其中的北上资金就是一个风
- 1、新建独立运行环境,命名为env[root@vultr ~]# mkdir projects # 测试的项目总目录[root@vultr
- 排查原因,发现是80端口被其它程序占用(很常见的事情╮(╯_╰)╭)。解决方法用记事本打开目录x:\xampp\apache\conf下的h
- 打印类的所有属性和方法利用dir(obj)方法获得obj对象的所有属性和方法名,返回一个list。for item in dir(top_k
- 映射Go编程提供的一个重要的数据类型就是映射,唯一映射一个键到一个值。一个键要使用在以后检索值的对象。给定的键和值,可以在一个Map对象存储
- 一、前言在多进程中,每个进程之间是什么关系呢?其实每个进程都有自己的地址空间、内存、数据栈以及其他记录其运行状态的辅助数据。下面通过一个例子
- python代码实现冒泡排序代码其实很简单,具体代码如下所示:代码Code highlighting produced by Actipro