【Redis实战】分布式锁

news2024/11/17 4:27:19

分布式锁

synchronized只能保证单个JVM内部的线程互斥,不能保证集群模式下的多个JVM的线程互斥。

分布式锁原理

每个JVM内部都有自己的锁监视器,但是跨JVM,就会有多个锁监视器,就会有多个线程获取到锁,不能实现多JVM进程之间的互斥。
我们不能使用JVM内部的锁监视器,我们必须让多个JVM去使用同一个锁监视器,所以肯定是一个独立于JVM内部的,多个JVM都可以看到的监视器。
image.png
过程
image.png

特性

image.png

多进程可见

多个JVM都可以看到,比如Redis,MySQL等。JVM外部的基本都可以实现。

互斥

只能有一个人拿到锁

高可用

大多数情况下,获取锁都是成功的,而不是频繁失败

高并发/高性能

加锁本身就会影响性能,会变成串行执行,如果加锁本身也很慢,就不行了。

安全性

异常情况下,比如,获取锁完毕之后,锁无法释放,服务宕机了。
死锁问题等等。

功能性特性

比如是否可重入,阻塞还是非阻塞的,公平还是非公平锁

不同的分布式锁区别

image.png

MySQL

  • 互斥:通过事务的互斥锁来实现,事务提交锁释放,异常事务回滚
  • 高可用:依赖MySQL本身的高可用
  • 高性能:受限于MySQL的性能
  • 安全性:通过事务获取锁,断开链接的时候,锁会自动释放

Redis

  • 互斥:通过setnx互斥命令来实现互斥
  • 高可用:Redis本身可以实现主从和集群模式,可用性高
  • 高性能:较高
  • 安全性:服务出现故障,锁无法释放,死锁,可以利用key的过期机制来实现

Zookeeper

  • 互斥:利用内部节点的唯一性和有序性来实现,每个节点的id都是自增的,删除节点,另外一个节点就说最小的了
  • 高可用:支持集群
  • 高性能:保证强一致性,主从之间数据同步会消耗一定时间
  • 安全性:创建的是临时节点,服务宕机,锁会自动释放

Redis实现分布式锁

分布式锁需要实现两个最基本的方法

获取锁

互斥

确保只能有一个线程执行成功。通过redis的setnx命令来实现,同时执行时,只有1个能执行成功,实现互斥。

#获取锁
setnx key value

image.png

  • 添加锁的过期时间,避免服务宕机引起死锁。过期时间需要注意,业务还没处理完但是锁过期的问题
#设置过期时间
expire key 10

image.png
为了避免出现,setnx后,expire之前,服务宕机的问题,我们将两条命令合并为一条,保证原子性

#添加锁 nx是互斥,ex是过期时间
set key value ex 10 nx
#或者
set key value nx ex 10

image.png

非/阻塞式获取锁

获取锁成功返回ok,失败返回nil,如果失败了,有两种解决方案,jdk中,有两种方案:一直阻塞式等待,另一种,获取锁失败即刻返回。
非阻塞式获取锁,尝试一次,成功返回true,失败返回false!

释放锁

手动释放

手动删除即可

#释放锁
del key

image.png

超时释放

获取锁时,添加一个超时时间,避免出现服务宕机,锁无法被释放

流程

image.png

分布式锁初级版

执行流程

image.png

分布式锁代码

接口

/**
 * 分布式锁
 *
 * @author zhangzengxiu
 * @date 2023/10/9
 */
public interface ILock {

    /**
     * 尝试去获取锁
     *
     * @param timeoutSc 过期时间,过期锁自动释放
     * @return 获取成功返回true,失败返回false
     */
    boolean tryLock(long timeoutSc);

    /**
     * 释放锁
     */
    void unlock();
}

实现

import org.springframework.data.redis.core.StringRedisTemplate;

import java.util.concurrent.TimeUnit;

/**
 * @author zhangzengxiu
 * @date 2023/10/9
 */
public class SimpleRedisLock implements ILock {

    private StringRedisTemplate stringRedisTemplate;

    /**
     * 锁统一前缀
     */
    public static final String KEY_PRE = "lock:";

    /**
     * 业务名称
     */
    private String name;

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

    @Override
    public boolean tryLock(long timeoutSc) {
        //获取线程标识
        long threadId = Thread.currentThread().getId();
        String key = KEY_PRE + name;
        String value = String.valueOf(threadId);
        Boolean res = stringRedisTemplate.opsForValue().setIfAbsent(key, value, timeoutSc, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(res);
    }

    @Override
    public void unlock() {
        String key = KEY_PRE + name;
        stringRedisTemplate.delete(key);
    }
}

业务代码

image.png

异常情况

线程1尝试去获取锁,获取到锁之后,

  • 正常情况:业务执行完毕后,正常释放锁

image.png

  • 异常情况:业务执行时间超过了锁的超时时间,锁被超时释放;

image.png

  • 误删锁

image.png

  • 线程1的锁由于业务阻塞被超时释放了,此时锁被线程2获取到了,此时线程1醒了,继续执行并释放了锁,此时被释放的锁是线程2的锁。
  • 这时线程3也获取到了被释放的锁,此时相当于多个线程在并行执行,线程并发安全问题依然存在。

解决方案
image.png
释放锁的时候判断是不是自己的锁,是自己的锁才能释放,否则无法释放锁。
image.png

改进分布式锁(解决锁误删问题)

image.png
线程id是JVM内部递增的,集群模式下,每个JVM内部都会有自增的线程id,会出现线程id冲突的情况。
如果只是使用线程id作为区分是不行的,还要区分JVM,我们可以使用UUID或者线程id拼接UUID的形式来实现。通过UUID来区分不同的JVM,再通过线程id来区分不同的线程。

业务流程

image.png

分布式锁代码实现

import cn.hutool.core.lang.UUID;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.StringRedisTemplate;

import java.util.concurrent.TimeUnit;

/**
 * @author zhangzengxiu
 * @date 2023/10/9
 */
public class SimpleRedisLock implements ILock {

    private StringRedisTemplate stringRedisTemplate;

    /**
     * 锁统一前缀
     */
    public static final String KEY_PRE = "lock:";

    /**
     * 锁的值的前缀
     */
    public static final String ID_PRE = UUID.randomUUID().toString(true) + "—";

    /**
     * 业务名称
     */
    private String name;

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

    @Override
    public boolean tryLock(long timeoutSc) {
        //获取线程标识
        String value = ID_PRE + Thread.currentThread().getId();
        String key = KEY_PRE + name;
        Boolean res = stringRedisTemplate.opsForValue().setIfAbsent(key, value, timeoutSc, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(res);
    }

    @Override
    public void unlock() {
        //获取线程标识
        String value = ID_PRE + Thread.currentThread().getId();
        String key = KEY_PRE + name;
        //获取锁中的标识
        String val = stringRedisTemplate.opsForValue().get(key);
        if (StringUtils.equals(value, val)) {
            //释放锁
            stringRedisTemplate.delete(key);
        }
    }
}

image.png

异常情况

当前代码依然存在异常情况,比如:

  • 线程1操作结束,释放锁的时候,先判断是否是自己的锁,然后准备释放的时候,被阻塞了,可能是因为JVM的垃圾回收机制FullGC导致了阻塞,导致了线程1 的锁由于超时自动释放
  • 此时线程2获取到了锁,在执行业务代码的过程中,线程1结束了阻塞,此时直接去释放了锁,但是此时释放的锁却是线程2的锁;
  • 现在属于无锁状态,此时线程3获取到了锁,线程2和3就属于并行执行,线程安全问题再次出现。

问题:判断锁和释放锁是两个操作,并不具有原子性!!!
image.png

Lua脚本解决原子性问题

判断锁+释放锁在特殊情况下依然存在原子性问题,也可以通过Redis的事务+乐观锁机制来实现。
image.png

Lua

Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。
借鉴网站:Lua 基本语法 | 菜鸟教程
我们可以使用Redis提供的函数进行调用,

redis.call('命令名称','key','其他参数',...);

示例代码:

redis.call('set','key','value');

执行脚本

EVAL "return redis.call('set','key','value')" 0

说明:
其中双引号中的内容是脚本内容
0:表示key类型参数的数量,我们可以将value设置为可传入的参数,不写死

示例:
不带参数的Lua脚本:
image.png
因为有些redis命令是可以一次性设置多个key value的,比如 mset
如果脚本中的key、value不想写死,可以作为参数传递。key类型参数会放到KEYS数组中,其他参数会放到ARGV数组中,在脚本中可以从KEYS和ARGV数组中获取参数:

lua语言中,数组的角标是从1开始而不是0

image.png
执行脚本:
image.png

分布式锁的释放锁的Lua脚本

释放锁业务流程

1、获取锁中的线程标识
2、判断是否与指定的标识(当前线程标识)一致
3、判断如果一致则释放锁(删除)
4、如果不一致啥也不做

Lua脚本
-- 获取锁中线程标识(key传参)
local key = KEYS[1]
-- 获取当前线程的标识(其他参数传参)
local threadId = ARGV[1]
-- 获取锁中的线程标识
local id = redis.call('get',key)
-- 比较线程中标识和锁中的标识是否一致
if (threadId == id) then
	-- 释放锁
	return redis.call('del',key)
end
return 0

简化写法:

-- 比较线程中标识和锁中的标识是否一致
if (ARGV[1] == redis.call('get',KEYS[1])) then
	-- 释放锁
	return redis.call('del',KEYS[1])
end
return 0

Java语言调用Lua脚本

image.png
修改代码:
修改前
image.png
修改后
image.png

    @Override
    public void unlock() {
        //传入Lua脚本的KEYS数组
        List<String> keys = new ArrayList<>(Arrays.asList(KEY_PRE + name));
        //传入Lua的其他参数
        String arg = ID_PRE + Thread.currentThread().getId();
        //调用Lua脚本
        stringRedisTemplate.execute(UNLOCK_SCRIPT, keys, arg);
    }

总结

image.png

基于Redis的分布式锁优化

当前的分布式锁仍然存在一些问题

存在的问题

image.png

不可重入

同一个线程无法多次获取同一把锁。
当线程1拿到锁,A方法调用B方法时,A方法需要锁,B方法也需要锁,但是,A在调B时,锁还没有释放,还在A手里,B就迟迟拿不到锁,A也无法释放锁,此时就会出现死锁

不可重试

获取锁,只重试一次,只要没获取到,立即返回false,没有进行重试

超时释放

如果锁超时时间过短,业务还没执行完,锁就被释放了,也会有问题。
如果锁超时时间过长,一但出了问题,需要很长一段时间才能自动释放锁。

主从一致性

主节点和从节点之间存在延迟,极端情况下,如果锁通过set写入到主节点,但是主节点还没来得及同步到从节点,这个时候主节点就宕机了,从节点里是没有这个锁的标识的。
此时,重新选举的主节点,是没有锁的,这个时候其他线程就会获取到锁。

如果你是用的单节点,其实也不用去理会这个问题。

以上这些问题,要实现起来其实很麻烦,我们可以通过现有的工具来进行实现。

Redisson

image.png

快速入门

引入依赖
        <!--redisson-->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.13.6</version>
        </dependency>
配置Redisson客户端

官方有提供来Redisson的SpringBoot的stater,但是会替代Spring官方提供的配置和实现,不建议使用,建议自己去配置。

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.redisson.config.SingleServerConfig;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author zhangzengxiu
 * @date 2023/10/10
 */
@Configuration
public class RedisConfig {

    /**
     * redis的主机
     */
    @Value("${spring.redis.host}")
    private String redisHost;

    /**
     * redis的端口
     */
    @Value("${spring.redis.port}")
    private String redisPort;

    /**
     * redis的密码
     */
    @Value("${spring.redis.password}")
    private String redisPassword;

    /**
     * redis协议
     */
    public static final String REDIS_PRE = "redis://";

    @Bean
    public RedissonClient getRedissonClient() {
        //配置类
        Config config = new Config();
        //配置单节点的Redis
        SingleServerConfig ssc = config.useSingleServer();
        //配置集群 需要配置多个Redis地址
        //SingleServerConfig ssc = config.useClusterServers();
        ssc.setAddress(REDIS_PRE + redisHost + ":" + redisPort);
        ssc.setPassword(redisPassword);
        //创建客户端
        return Redisson.create(config);
    }
}
使用分布式锁
	@Autowired
    private RedissonClient redissonClient;

	@Test
    public void testRedissonLock() throws InterruptedException {
        //获取锁(可重入)
        RLock lock = redissonClient.getLock("orderLock");
        /**
         * 尝试获取锁
         * 无参:失败直接返回
         * 有参:
         * 1:获取锁的最大等待时间,在此期间,获取锁失败了就会等待一段时间再去重试,超过这个最大等待时间才会返回false
         * 10:自动释放的时间,服务出现宕机的情况下,自动释放的时间
         * TimeUnit.SECONDS:时间单位
         */
        boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS);
        if (isLock) {
            try {
                System.out.println("");
            } finally {
                lock.unlock();
            }
        }
    }

修改之前的业务代码
image.png

Redisson可重入锁原理

可重入
流程

image.png
不可重入原因
image.png
当method1调用method2执行时,需要再次执行setnx,但是setnx是互斥的,所以无法再次获取这把锁。
我们可以参考JDK提供的ReentrantLock�来实现锁的可重入,在获取锁的同时去判断是否是当前线程,每次获取锁就进行+1操作,释放锁就-1。所以使用redis的string类型就不满足要求了。
我们可以通过hash结构来实现:
string类型可以通过set nx ex这样的命令来实现,但是hash并没有这样的组合命令,只能将命令拆开来实现。
image.png

获取锁Lua脚本

image.png

释放锁Lua脚本

image.png
查看Redisson获取锁的源码:
Lua脚本是通过字符串的形式来直接写死的。
image.png
释放锁
image.png

可重试

源码
image.png

image.png

image.pngtime就是:设置的超时时间-前面第一次获取锁消耗的时间所得到的剩余时间
重试等待:利用了信号量+消息订阅机制
不是while(true)无休止的等待,是等每次订阅到之后才进行重试。
image.png
至此,重试问题已经解决了。

超时释放

获取锁成功了,但是业务还没执行完,锁到期了,锁被释放了???

timeout超时任务进行自动续约,每过一段时间就重置时间,一直执行

image.png
新的任务没有更新有效期的任务,所以需要调用renewExpiration方法,旧的任务已经有了这个刷新有效期的任务,就不需要再调用一次了。
image.png
image.png
image.png
锁释放的时间?是在unlock的时候才释放锁
image.png

总结

image.png
image.png

主从一致性问题

获取到锁之后,主节点宕机
image.png
重新选举出来的新的主节点,出现数据丢失,锁失效
image.png

解决方案

联合节点(最少3个节点
简单粗暴,那就不要主从节点,每个节点都获取锁成功,才算成功!
image.png
如果后期其中一个节点宕机了,他自己的从节点数据丢失,那么此时并不是所有的节点都持有这把锁。
因为只有每一个节点都拿到锁,才算获取锁成功。
只要有1个节点是存活的状态,那么就不会有其他线程拿到锁,就不会有锁失效的问题。
image.png
我们可以单独使用几个节点,但是不建立主从关系就可以。
3个独立节点配置方式:
image.png
image.png
源码:
image.png

image.png

最终总结

image.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1082710.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

分享一份关于 Rust 编程的学习指南

Rust是一种现代的系统级编程语言&#xff0c;以其注重内存安全、性能和并发性而闻名。学习Rust可以是一段有回报的旅程&#xff0c;为您打开构建强大高效应用的机会。无论您是经验丰富的开发者还是完全的初学者&#xff0c;本指南将通过精选的资源和技巧帮助您踏上Rust编程之旅…

【angular】实现简单的angular国际化(i18n)

文章目录 目标过程运行参考 目标 实现简单的angular国际化。本博客实现中文版和法语版。 将Hello i18n!变为中文版&#xff1a;你好 i18n!或法语版:Bonjour l’i18n !。 过程 创建一个项目&#xff1a; ng new i18nDemo在集成终端中打开。 添加本地化包&#xff1a; ng a…

景联文科技:3D点云标注应用场景和专业平台

3D点云技术之所以得到广泛发展和应用&#xff0c;主要是因为它能够以一种直观、真实和全面的方式来表示和获取现实世界中的三维信息。 3D点云的优势&#xff1a; 真实感和立体感&#xff1a;3D点云数据能够呈现物体的真实感和立体感&#xff0c;使观察者能够更直观地理解物体的…

Springboot整合阿里云OSS进行上传单个图片,多个图片,删除图片功能

1. 导入OSS依赖 <!-- 阿里云oss依赖 --><dependency><groupId>com.aliyun.oss</groupId><artifactId>aliyun-sdk-oss</artifactId><version>3.10.2</version></dependency> 2. 进行OSS配置 package com.example.sushe…

[GAMES101]透视投影变换矩阵中为什么需要改变z值

透视投影需要保证&#xff0c;1.变换矩阵内的元素是常数&#xff0c;2.相对深度值不变&#xff08;绝对值不重要&#xff09;&#xff1b;若再加上变换后zNear和zFar平面上的点依旧在zNear和zFar平面上这两个条件&#xff08;实际上并不一定需要满足这两个条件&#xff09;&…

机器人制作开源方案 | 扫地机器人

1. 功能描述 扫地机器人是现代家庭清洁的得力助手&#xff0c;能够自主规划清扫路径&#xff0c;避开障碍物&#xff0c;有效覆盖整个清洁区域。扫地机器人的出现极大地减轻了家庭清洁的负担&#xff0c;节省了时间和精力&#xff0c;它可以定期清理地面&#xff0c;确保家居环…

043:mapboxGL鼠标点击提示source属性信息

第043个 点击查看专栏目录 本示例的目的是介绍演示如何在vue+mapbox中通过鼠标点击提示source属性信息。这里用到了popup弹窗,用到了click事件,用到了鼠标样式的变化等功能。 直接复制下面的 vue+mapbox源代码,操作2分钟即可运行实现效果 文章目录 示例效果配置方式示例源…

正点原子嵌入式linux驱动开发——Busybox根文件系统构建

前面已经移植了TF-A、Uboot和Linux kernel&#xff0c;就剩最后一个 rootfs(根文件系统)了&#xff0c;本章就来学习一下根文件系统的组成以及如何构建根文件系统。这是Linux系统移植的最后一步&#xff0c;根文件系统构建好以后就意味着拥有了一个完整的、可以运行的最小系统 …

【C语言】求解数独 求数独的解的个数 多解数独算法

目录 什么是数独&#xff1f; 数独的解法&#xff1f; 数独DFS算法详解 1. 初始化条件 2. 填入已初始化的数独表 3. 填数独 4. 拓展问题 请问删掉数独中的哪两个数可以使得数独的解最大&#xff1f; 删除的是哪两个数&#xff1f; 最终代码 main函数&#xff08;如何执行…

前端-uniapp-开发指南

美团外卖微信小程序开发 uniapp-美团外卖微信小程序开发P1 成果展示P2外卖小程序后端&#xff0c;学习给小程序写http接口P3 主界面配置P4 首页组件拆分P13 外卖列表布局筛选组件商家 布局测试数据创建样式 请求商家外卖数据封装请求并发请求 uni-app框架调用https接口 开发小程…

UE4和C++ 开发-C++绑定widget的方式和初始化UI

C绑定widget的方式有两种&#xff0c;一种是使用meta (BindWidget)&#xff0c;一种是使用GetWidgetFromName(TEXT("")),两种方式都可以。一、meta BindWidget方式 注意这种绑定的方式UMG里面的空间名称需要与C里面声明的变量名称相同 Btn_StartU 二、GetWidge…

成都瀚网科技有限公司:怎么优化抖店体验分?

近年来&#xff0c;抖音电商平台凭借强大的用户基础和广阔的销售渠道吸引了越来越多的商家入驻。然而&#xff0c;对于新手卖家来说&#xff0c;提高抖店经验值却成了一件头疼的事情。那么&#xff0c;如何优化抖店体验分呢&#xff1f;本文将从产品质量、服务态度、运营策略等…

Springboot集成MyBatis实现查询表操作(二)

目录 第一章、准备1.1&#xff09;准备数据库表1.2&#xff09;创建springboot项目&#xff0c;添加依赖1.3&#xff09;使用mybatis逆向工程 第二章、代码开发2.1&#xff09;建包并编写代码2.2&#xff09;application配置文件2.3&#xff09;设置编译位置 第三章、测试访问3…

【NUMA平衡】浅入介绍NUMA平衡技术及调度方式

在云计算方案设计或项目问题处理的时候&#xff0c;经常会遇到NUMA平衡的问题&#xff0c;进行让人不清楚NUMA到底有何用&#xff0c;如何发挥作用&#xff0c;本文就NUMA技术原理和调度进行简要整理&#xff0c;方便后续需要时候查阅学习。 一.背景 一般的对称多处理器中&am…

基于SpringBoot的新闻稿件管理系统

目录 前言 一、技术栈 二、系统功能介绍 管理员模块的实现 用户信息管理 记者信息管理 审批员信息管理 记者模块的实现 新闻信息管理 审批员模块的实现 新闻信息管理 用户模块的实现 新闻信息 三、核心代码 1、登录模块 2、文件上传模块 3、代码封装 前言 随着信…

安装JoySSL的SSL证书有什么优势?

近年来&#xff0c;网络安全事件层出不穷&#xff0c;屡禁不止。 据统计仍有57%的网站未进行https加密&#xff0c;成为数据泄漏的“导火索”之一。 而SSL证书不仅仅可以保护网站数据安全&#xff0c;而且可以降低网站被第三方窃取或篡改的风险。 安装JoySSL证书的好处&#…

kaggle新赛:写作质量预测大赛【数据挖掘】

赛题名称&#xff1a;Linking Writing Processes to Writing Quality 赛题链接&#xff1a;https://www.kaggle.com/competitions/linking-writing-processes-to-writing-quality 赛题背景 写作过程中存在复杂的行为动作和认知活动&#xff0c;不同作者可能采用不同的计划修…

振弦采集仪应用于隧道安全监测

振弦采集仪应用于隧道安全监测 振弦采集仪是当今必不可少的现代隧道安全监测工具。该设备广泛应用于隧道内部各种安全参数的实时监测&#xff0c;包括但不限于隧道变形、裂缝、压力、温度等。本文详细介绍了振弦采集仪在隧道安全监测中的应用。 首先&#xff0c;我们来了解一下…

nodejs+vue宠物店管理系统

例如&#xff1a;如何在工作琐碎,记录繁多的情况下将宠物店管理的当前情况反应给管理员决策,等等。在此情况下开发一款宠物店管理系统小程序&#xff0c; 困扰管理层的许多问题当中,宠物店管理也是不敢忽视的一块。但是管理好宠物店又面临很多麻烦需要解决,于是乎变得非常合乎时…

科技资讯|微软AR眼镜新专利曝光,可拆卸电池解决续航焦虑

微软正在深入研究增强现实&#xff08;AR&#xff09;领域&#xff0c;最近申请了一项“热插拔电池”相关专利。该专利于 2023 年 10 月 5 日发布&#xff0c;描述了采用模块化设计的 AR 眼镜&#xff0c;热插拔电池放置在了镜腿部分&#xff0c;可以直接拿下替换&#xff0c;对…