关于SpringBoot 使用 Redis 分布式锁解决并发问题


Posted in Redis onNovember 17, 2021

问题背景

现在的应用程序架构中,很多服务都是多副本运行,从而保证服务的稳定性。一个服务实例挂了,其他服务依旧可以接收请求。但是服务的多副本运行随之也会引来一些分布式问题,比如某个接口的处理逻辑是这样的:接收到请求后,先查询 DB 看是否有相关的数据,如果没有则插入数据,如果有则更新数据。在这种场景下如果相同的 N 个请求并发发到后端服务实例,就会出现重复插入数据的情况:

关于SpringBoot 使用 Redis 分布式锁解决并发问题

解决方案

针对上面问题,一般的解决方案是使用分布式锁来解决。同一个进程内的话用本进程内的锁即可解决,但是服务多实例部署的话是分布式的,各自进程独立,这种情况下可以设置一个全局获取锁的地方,各个进程都可以通过某种方式获取这个全局锁,获得到锁后就可以执行相关业务逻辑代码,没有拿到锁则跳过不执行,这个全局锁就是我们所说的分布式锁。分布式锁一般有三种实现方式:1. 数据库乐观锁;2. 基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁。

我们这里介绍如何基于 Redis 的分布式锁来解决分布式并发问题:Redis 充当获取全局锁的地方,每个实例在接收到请求的时候首先从 Redis 获取锁,获取到锁后执行业务逻辑代码,没争抢到锁则放弃执行。

关于SpringBoot 使用 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/

到此这篇关于SpringBoot 使用 Redis 分布式锁解决并发问题的文章就介绍到这了,更多相关SpringBoot Redis 分布式锁内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Redis 相关文章推荐
Redis6.0搭建集群Redis-cluster的方法
May 08 Redis
redis使用不当导致应用卡死bug的过程解析
Jul 01 Redis
浅谈Redis的keys命令到底有多慢
Oct 05 Redis
详解Redis在SpringBoot工程中的综合应用
Oct 16 Redis
Window server中安装Redis的超详细教程
Nov 17 Redis
分布式Redis Cluster集群搭建与Redis基本用法
Feb 24 Redis
Redis 中使用 list,streams,pub/sub 几种方式实现消息队列的问题
Mar 16 Redis
Redis调用Lua脚本及使用场景快速掌握
Mar 16 Redis
redis调用二维码时的不断刷新排查分析
Apr 01 Redis
Redis特殊数据类型Geospatial地理空间
Jun 01 Redis
浅谈Redis缓冲区机制
Jun 05 Redis
Redis Stream类型的使用详解
Redis 持久化 RDB 与 AOF的执行过程
Redis模仿手机验证码发送的实现示例
redis中lua脚本使用教程
Redis高并发防止秒杀超卖实战源码解决方案
Redis的字符串是如何实现的
SpringBoot集成Redis的思路详解
You might like
实现分十页分向前十页向后十页的处理
2006/10/09 PHP
PHP 5.0对象模型深度探索之类的静态成员
2008/03/27 PHP
PHP XML和数组互相转换详解
2016/10/26 PHP
laravel 5异常错误:FatalErrorException in Handler.php line 38的解决
2017/10/12 PHP
img的onload的另类用法
2008/01/10 Javascript
获取div编辑框,textarea,input text的光标位置 兼容IE,FF和Chrome的方法介绍
2012/11/08 Javascript
重构Javascript代码示例(重构前后对比)
2013/01/23 Javascript
Javascript 垃圾收集机制介绍理解
2013/05/14 Javascript
js与jquery获取父级元素,子级元素,兄弟元素的实现方法
2014/01/09 Javascript
Jquery方式获取iframe页面中的 Dom元素
2014/05/07 Javascript
js动态修改整个页面样式达到换肤效果
2014/05/23 Javascript
JS中三目运算符和if else的区别分析与示例
2014/11/21 Javascript
js 判断附件后缀的简单实现方法
2016/10/11 Javascript
12个非常有用的JavaScript技巧
2017/05/17 Javascript
详解AngularJs路由之Ui-router-resolve(预加载)
2017/06/13 Javascript
JavaScript判断浏览器和hack滚动条的写法
2017/07/23 Javascript
JavaScript解决浮点数计算不准确问题的方法分析
2018/07/09 Javascript
vue+AI智能机器人回复功能实现
2020/07/16 Javascript
[02:05]2014DOTA2国际邀请赛 BBC外卡赛赛后总结
2014/07/09 DOTA
Python中实现从目录中过滤出指定文件类型的文件
2015/02/02 Python
Python实现SSH远程登陆,并执行命令的方法(分享)
2017/05/08 Python
Python实现的HMacMD5加密算法示例
2018/04/03 Python
Python数据分析:手把手教你用Pandas生成可视化图表的教程
2018/12/15 Python
实例讲解Python3中abs()函数
2019/02/19 Python
PyQt Qt Designer工具的布局管理详解
2019/08/07 Python
Python实现AI自动抠图实例解析
2020/03/05 Python
Python模块相关知识点小结
2020/03/09 Python
python3 循环读取excel文件并写入json操作
2020/07/14 Python
Skyscanner新西兰:全球领先的旅游搜索网站
2019/08/26 全球购物
数控专业自荐书范文
2014/03/16 职场文书
学历公证书范本
2014/04/09 职场文书
事业单位个人查摆问题及整改措施
2014/10/28 职场文书
2014保险公司内勤工作总结
2014/12/16 职场文书
高三教师工作总结2015
2015/07/21 职场文书
教师节感想
2015/08/11 职场文书
MongoDB修改oplog大小的四种方法
2022/04/11 MongoDB