关于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 相关文章推荐
redis 查看所有的key方式
May 07 Redis
浅谈redis缓存在项目中的使用
May 20 Redis
SpringBoot 集成Redis 过程
Jun 02 Redis
Redis集群的关闭与重启操作
Jul 07 Redis
Redis性能监控的实现
Jul 09 Redis
Redis 常见使用场景
Aug 30 Redis
关于SpringBoot 使用 Redis 分布式锁解决并发问题
Nov 17 Redis
Redis高可用集群redis-cluster详解
Mar 20 Redis
Redis数据同步之redis shake的实现方法
Apr 21 Redis
Redis sentinel哨兵集群的实现步骤
Jul 15 Redis
如何使用注解方式实现 Redis 分布式锁
Jul 23 Redis
Redis Stream类型的使用详解
Redis 持久化 RDB 与 AOF的执行过程
Redis模仿手机验证码发送的实现示例
redis中lua脚本使用教程
Redis高并发防止秒杀超卖实战源码解决方案
Redis的字符串是如何实现的
SpringBoot集成Redis的思路详解
You might like
php数组删除元素示例
2014/03/21 PHP
深入理解PHP中的count函数
2016/05/31 PHP
javascript实现的动态文字变换
2007/07/28 Javascript
CSS+Jquery实现页面圆角框方法大全
2009/12/24 Javascript
JavaScript和CSS交互的方法汇总
2014/12/02 Javascript
javascript实现的网站访问量统计代码
2015/12/20 Javascript
Jquery和JS获取ul中li标签的实现方法
2016/06/02 Javascript
javascript数字验证的实例代码(推荐)
2016/08/20 Javascript
纯JS实现可拖拽表单的简单实例
2016/09/02 Javascript
jQuery实现简洁的轮播图效果实例
2016/09/07 Javascript
javascript滚轮控制模拟滚动条
2016/10/19 Javascript
JS 实现Base64编码与解码实例详解
2016/11/07 Javascript
微信页面倒计时代码(解决safari不兼容date的问题)
2016/12/13 Javascript
JavaScript利用Date实现简单的倒计时实例
2017/01/12 Javascript
Vue 短信验证码组件开发详解
2017/02/14 Javascript
js实现京东轮播图效果
2017/06/30 Javascript
react-native使用react-navigation进行页面跳转导航的示例
2017/09/07 Javascript
JavaScript实现微信红包算法及问题解决方法
2018/04/26 Javascript
vue中如何实现后台管理系统的权限控制的方法示例
2018/09/19 Javascript
vue(2.x,3.0)配置跨域代理
2019/11/27 Javascript
js页面加载后执行的几种方式小结
2020/01/30 Javascript
python处理csv数据的方法
2015/03/11 Python
使用python检测主机存活端口及检查存活主机
2015/10/12 Python
Python2.7下安装Scrapy框架步骤教程
2017/12/22 Python
Python实用技巧之列表、字典、集合中根据条件筛选数据详解
2018/07/11 Python
一篇文章搞定Python操作文件与目录
2019/08/13 Python
python将字母转化为数字实例方法
2019/10/04 Python
Pytorch 图像变换函数集合小结
2021/02/01 Python
Ado与Ado.net的相同与不同
2014/12/08 面试题
什么是索引指示器
2012/08/20 面试题
初中生个人学习的自我评价
2013/12/04 职场文书
医药个人求职信范文
2014/01/29 职场文书
优秀教师感人事迹材料
2014/05/04 职场文书
Win11绿屏怎么办?Win11绿屏死机的解决方法
2021/11/21 数码科技
Shell脚本一键安装Nginx服务自定义Nginx版本
2022/03/20 Servers
Go语言怎么使用变长参数函数
2022/07/15 Golang