首页 > 数据库 >基于Redis的分布式锁

基于Redis的分布式锁

时间:2023-08-15 20:12:08浏览次数:44  
标签:基于 name Redis public String PREFIX 线程 stringRedisTemplate 分布式

在多线程的环境下,为了保证一个代码块在同一时间只能由一个线程访问,Java中我们一般可以使用synchronized语法和ReetrantLock去保证,这实际上是本地锁的方式。但是现在公司都是流行分布式架构,在分布式环境下,如何保证不同节点的线程同步执行呢?

实际上,对于分布式场景,我们可以使用分布式锁,它是控制分布式系统之间互斥访问共享资源的一种方式。

分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。
特点:

  1. 高可用
  2. 读线程可见
  3. 高性能
  4. 互斥
  5. 安全性

分布式锁的实现方式:

MySQL Redis Zookeeper
互斥 利用mysql本身的互斥锁机制 利用setnx命令 利用节点的唯一性和互斥性
高可用
高性能 一般 一般
安全性 断开连接自动释放锁 利用锁的超时时间自动释放 临时节点,断开连接自动释放

Redis的分布式锁实现

1. 利用setnx+expire命令 (错误的做法)

Redis的SETNX命令,setnx key value,将key设置为value,当键不存在时,才能成功,若键存在,什么也不做,成功返回1,失败返回0 。 SETNX实际上就是SET IF NOT Exists的缩写

因为分布式锁还需要超时机制,所以我们利用expire命令来设置,所以利用setnx+expire命令的核心代码如下:

public boolean tryLock(String key, String requset, int timeout) {
        Long result = jedis.setnx(key, requset);
        // result = 1时,设置成功,否则设置失败    
        if (result == 1L) {
            return jedis.expire(key, timeout) == 1L;
        } else {
            return false;
        }
    }

实际上上面的步骤是有问题的,setnx和expire是分开的两步操作,不具有原子性,如果执行完第一条指令应用异常或者重启了,锁将无法过期。
改善方式,可以通过 set命令去一次性的将参数添加上去,通过 help set 命令我们可以看到set的所有用法
大致如下:
SET key value[EX seconds][PX milliseconds][NX|XX]
EX seconds: 设定过期时间,单位为秒

PX milliseconds: 设定过期时间,单位为毫秒

NX: 仅当key不存在时设置值

XX: 仅当key存在时设置值

上诉代码通过图形总结
image

集合业务场景:
这是一个简单的分布式锁的实现

public class SimpleRedisLock implements ILock{

    private String name; // 锁的名称

    public static final String KEY_PREFIX = "lock";

    private StringRedisTemplate stringRedisTemplate;

    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * 尝试获取锁
     *
     * @param timeoutSec 锁持有的超时时间,过期后自动释放
     * @return true代表获取锁成功,false代表获取锁失败
     */
    @Override
    public boolean tryLock(long timeoutSec) {
        // 获取线程的标识
        long threadID = Thread.currentThread().getId();
        // 获取锁
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, String.valueOf(threadID), timeoutSec, TimeUnit.SECONDS);
        // 防止自动拆箱导致的空指针异常
        return Boolean.TRUE.equals(success);
    }

    /**
     * 释放锁
     */
    @Override
    public void unlock() {
        stringRedisTemplate.delete(KEY_PREFIX+name);
    }
}

场景代码如下:

  // 通过分布式锁实现一人一单线程安全问题
        SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
        // 获取锁
        boolean isLock = lock.tryLock(100);
        // 判断释放获取锁成功
        if (!isLock){
           // 获取锁失败,返回错误信息,或者重试
            return Result.fail("不允许重复下单");
        }
        try {
            //执行业务代码
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId, userId);
        }finally {
            // 释放锁
            lock.unlock();
        }

这个方案还是可能存在问题:

问题一:「锁过期释放了,业务还没执行完」。假设线程a获取锁成功,一直在执行临界区的代码。但是100s过去后,它还没执行完。但是,这时候锁已经过期了,此时线程b又请求过来。显然线程b就可以获得锁成功,也开始执行临界区的代码。那么问题就来了,临界区的业务代码都不是严格串行执行的啦。

问题二:「锁被别的线程误删」。假设线程a执行完后,去释放锁。但是它不知道当前的锁可能是线程b持有的(线程a去释放锁时,有可能过期时间已经到了,此时线程b进来占有了锁)。那线程a就把线程b的锁释放掉了,但是线程b临界区业务代码可能都还没执行完呢。
image

解决方案:
在释放锁之前,判断这个锁是否是自己的

image

image

代码如下:

public class SimpleRedisLock implements ILock{

    private String name; // 锁的名称

    // 锁的前缀
    public static final String KEY_PREFIX = "lock";
    // 锁的ID前缀,唯一标识,用于解决误删
    public static final String ID_PREFIX = cn.hutool.core.lang.UUID.randomUUID().toString(true)+"-";

    private StringRedisTemplate stringRedisTemplate;

    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * 尝试获取锁
     *
     * @param timeoutSec 锁持有的超时时间,过期后自动释放
     * @return true代表获取锁成功,false代表获取锁失败
     */
    @Override
    public boolean tryLock(long timeoutSec) {
        // 获取线程的标识
        String threadID = ID_PREFIX+ Thread.currentThread().getId();
        // 获取锁
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadID, timeoutSec, TimeUnit.SECONDS);
        // 防止自动拆箱导致的空指针异常
        return Boolean.TRUE.equals(success);
    }

    /**
     * 释放锁
     */
    @Override
    public void unlock() {
        // 获取线程标识
        String threadID = ID_PREFIX+ Thread.currentThread().getId();
        String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
        // 判断标识是否一致
        if (threadID.equals(id)){
            // 释放锁
            stringRedisTemplate.delete(KEY_PREFIX+name);
        }

    }
}

为了更严谨,一般也是用lua脚本代替。lua脚本如下:
image
Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性,Lua是一种编程语言,他的基本语法大家可以参考官网:https://www.runoob.com/lua/lua-tutorial.html

按照上诉lua脚本是这样的:

if redis.call('get',KEYS[1]) == ARGV[1] then 
   return redis.call('del',KEYS[1]) 
else
   return 0
end;

JAVA代码调用lua脚本:

private static final DefaultRedisScript<Long> unlock_script;
    static {
        unlock_script = new DefaultRedisScript<>();
        unlock_script.setLocation(new ClassPathResource("unlock.lua"));
        unlock_script.setResultType(Long.class);
    }
    @Override
    public void unlock() {
        // 调用lua 脚本
        stringRedisTemplate.execute(unlock_script,
                Collections.singletonList(KEY_PREFIX + name),
                ID_PREFIX+Thread.currentThread().getId());
    }

image
目前基于 setnx实现的分布式锁 存在下面的问题:

  1. 不可重入,同一个线程无法多次获取同一把锁
  2. 不可重试,获取锁只尝试一次就返回false,没有重试机制
  3. 超时释放,锁超时释放虽然可以避免死锁,但如果是业务执行耗时较长,也会导致锁释放,存在安全隐患问题
  4. 主从一致性,如果Redis提供了主从集群,主从同步存在延迟,当主宕机时,如果从并同步主中的锁数据,则会出现锁实现。
    以上都是基于stringRedisTemplate 实现,接下来使用redission去实现。

标签:基于,name,Redis,public,String,PREFIX,线程,stringRedisTemplate,分布式
From: https://www.cnblogs.com/zgf123/p/17632322.html

相关文章

  • Redis Bigkey排查
    在处理bigkey问题可以先从一下几点入手什么是bigkey?bigkey危害?bigkey是如何产生的?如何发现bigkey?如何处理bigkey?什么是BigkeyRedisbigkey是指在Redis数据库中占用空间较大的键值对。这些键通常包含了大量的数据,可能会影响Redis的性能和内存使用。例如......
  • 大数据分布式存储
    为什么需要分布式存储?数据量太大,单机存储能力有上限,需要靠数量来解决问题数量的提升带来的是网络传输、磁盘读写、CPU、内存等各方面的综合提升。分布式组合在一起可以达到1+1>2的效果1.分布式系统常见的组织形式?去中心化模式:没有明确中心,大家协调工作中心化模式:有明确的中......
  • 基于花生壳和EMQX搭建本地mqtt服务
    本篇关于搭建本地mqtt调试服务器步骤1:下载相关工具,花生壳以及EMQX开源mqtt服务EMqx下载地址:https://www.emqx.com/zh/try?product=broker花生壳下载地址:https://hsk.oray.com/步骤2:花生壳实现内网穿透在这里不做讲解,主要讲述EMQX相关步骤;emqx开源版本下载,解压后,成功后的界面:浏览......
  • 基于Hexo和Butterfly创建个人技术博客,(15) 开发个人hexo主题-stylus动态样式语法
    stylus可以简单理解为一个动态的css样式表,在原有W3C规定的基础上增加了编程的能力,在使用前通过插件会再编译成普通的css文件。本章目标:掌握stylus样式语法,本章开始我们会从头开始编写自己的博客主题,同样采用pug和styl语法,官方帮助文档:stylus一、概述Stylus语法是python式基于缩进......
  • 分布式坑
    这三年被本篇主要内容如下:  前言 我们都在讨论分布式,特别是面试的时候,不管是招初级软件工程师还是高级,都会要求懂分布式,甚至要求用过。传得沸沸扬扬的分布式到底是什么东东,有什么优势? 借用火影忍术  看过火影的同学肯定知道漩涡鸣人的招牌忍术:多重影分身之术。......
  • 计算机视觉智能中医(二):基于mediapipe的手掌图片穴位识别
    文章目录1简要介绍2技术详情3代码实现3.1导入相应的库3.2定义在图片中画点写字的函数3.3定义手部关键点监测模型并导入3.4导入绘图函数3.5将原本三维的坐标系转为2维方便平面坐标计算3.6计算手部穴位的函数4完整代码5实验结果展示1简要介绍说明:此方法可能有一定的缺......
  • 计算机视觉智能中医(七):基于Unet模型的舌面裂纹自动分析
    此方法的具体细节与舌体分割类似,只是所用到的数据集不同!代码参照:计算机视觉智能中医(三):基于Unet模型的舌头舌体图片分割舌裂,即舌面裂纹。在中医诊断中健康人群的舌面看起来比较光滑,而舌体上出现各式各样的裂纹往往预示着患有一些疾病。舌裂的提取非常困难,舌面上的裂纹色值与普通舌面......
  • 优化 Redis 集群缓存分配:解决节点间分配不均导致内存溢出问题
    一、Redis集群部署简介在现代应用程序中,缓存被广泛应用以提高性能和减轻后端数据库的压力。本文将探讨面对Redis集群缓存分配不均问题时的解决方法。我们的Redis集群部署包括3主3从,每个节点分配内存4G(服务器总内存32G),内存淘汰策略相同:volatile-lru。二、问题描述在......
  • Redis(2):四种模式(单机、哨兵、集群、主从复制)部署
    学习自:Redis的四种模式-何童鞋-博客园深入理解Redis的部署模式四种模式的探索(redis部署四种模式)-数据库运维技术服务redis部署和运行原理redis的三种部署方式_小咪咪的技术博客_51CTO博客上一节:Redis(1):四种模式(单机、哨兵、集群、主从复制)原理0、所写m:master,主服务器s:s......
  • 【Python&RS】基于GDAL遥感影像分幅裁剪
    ​    随着科技的进步,遥感影像包含的信息越来越多,存储空间也变得很大,这就导致我们在处理影像时软件会非常的卡。同时目前很火的深度学习也需要对影像分割后制作样本集,所以今天给大家分享下如何使用Python的GDAL库对遥感影像进行分幅裁剪!一、导入需要的三方库   ......