Redis企业开发实战(四)——点评项目之分布式锁

目录

一、分布式锁介绍 

(一)分布式锁基本介绍

(二)分布式锁满足的条件 

(三)常见的分布式锁

1.Mysql

2.Redis

3.Zookeeper

二、Redis分布式锁详解

(一)Redis分布式锁的实现核心思路

获取锁:

释放锁:

(二)基于Redis实现分布式锁初级版本

1.定义一个锁的基本接口

2.创建实现类实现锁接口的业务逻辑 

3.使用锁

(三)Redis分布式锁误删情况说明

(四)解决Redis分布式锁误删问题

(五)分布式锁的原子性问题

(六)Lua脚本解决多条命令原子性问题

1.Lua脚本基本介绍

2.释放锁的Lua脚本 

(七)利用Java代码调用Lua脚本改造分布式锁 

(八)总结——基于Redis的分布式锁实现思路

(九)基于setnx实现的分布式锁存在的问题

1.重入问题:

2.不可重试:

3.超时释放:

4.主从一致性:


        《Redis企业开发实战(三)——点评项目之优惠券秒杀》上回书说到,集群模式下,syn锁会失效,因为每个tomcat中会有自己的JVM空间,每个JVM空间中的锁监视器会监事各自的线程,导致超领问题。syn锁只能保证单个JVM中的多个线程之间互斥。因此,我们必须使用分布式锁。

一、分布式锁介绍 

(一)分布式锁基本介绍

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

        分布式锁的核心思想:就是让大家都使用同一把锁,只要大家使用的是同一把锁,那么我们就能锁住线程,不让线程进行,让程序串行执行,这就是分布式锁的核心思路。 

(二)分布式锁满足的条件 

  • 可见性:多个线程都能看到相同的结果,注意:这个地方说的可见性并不是并发编程中指的内存可见性,只是说多个进程之间都能感知到变化的意思
  • 互斥:互斥是分布式锁的最基本的条件,使得程序串行执行
  • 高可用:程序不易崩溃,时时刻刻都保证较高的可用性
  • 高性能:由于加锁本身就让性能降低,所有对于分布式锁本身需要他就较高的加锁性能和释放锁性能
  • 安全性

(三)常见的分布式锁

1.Mysql

        mysql本身就带有锁机制,但是由于mysql性能本身一般,所以采用分布式锁的情况下,使用mysql作为分布式锁比较少见。

2.Redis

        redis作为分布式锁是非常常见的一种使用方式,现在企业级开发中基本都使用redis或者zookeeper作为分布式锁,利用setnx这个方法,如果插入key成功,则表示获得到了锁,如果有人插入成功,其他人插入失败则表示无法获得到锁,利用这套逻辑来实现分布式锁。

3.Zookeeper

        ZooKeeper 实现分布式锁主要是利用其提供的临时顺序节点和监听机制来确保在分布式环境下的互斥访问。

二、Redis分布式锁详解

(一)Redis分布式锁的实现核心思路

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

获取锁:

  • 互斥:确保只能有一个线程获取锁
  • 非阻塞:尝试一次,成功返回true,失败返回false
# 添加锁,NX是互斥,EX是设置超时时间
SET lock thread1 NX EX 10

释放锁:

  • 手动释放
  • 超时释放:获取锁时添加一个超时时间
# 释放锁,删除即可
DEL key

(二)基于Redis实现分布式锁初级版本

1.定义一个锁的基本接口

public interface ILock {
    /**
     * 尝试获取锁
     *
     * @param timeoutSec 锁持有的超时时间,过期后自动释放
     * @return true代表获取锁成功,false代表获取锁失败
     */
    boolean tryLock(long timeoutSec);

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

2.创建实现类实现锁接口的业务逻辑 

public class SimpleRedisLock implements ILock {
    private String name;
    private static final String KEY_PREFIX = "lock:";

    private StringRedisTemplate stringRedisTemplate;

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

    @Override
    public boolean tryLock(long timeoutSec) {
        // 获取线程标识作为锁的名称
        String value = Thread.currentThread().getId() + "";
        // 获取锁
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, value, timeoutSec, TimeUnit.SECONDS);
        // 防止自动拆箱产生null
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unLock() {
        // 通过del删除锁
        stringRedisTemplate.delete(KEY_PREFIX + name);
    }
}

3.使用锁

原来的sync不再使用

@Override
public Result seckillVoucher(Long voucherId) {
    // 查询优惠券是否存在
    SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
    if (seckillVoucher == null) {
        return Result.fail("优惠券不存在");
    }
    // 查询秒杀是否开始
    LocalDateTime beginTime = seckillVoucher.getBeginTime();
    if (beginTime.isAfter(LocalDateTime.now())) {
        return Result.fail("秒杀尚未开始");
    }
    // 查询秒杀是否结束
    LocalDateTime endTime = seckillVoucher.getEndTime();
    if (endTime.isBefore(LocalDateTime.now())) {
        return Result.fail("秒杀已经结束");
    }
    // 判断库存是否充足
    Integer stock = seckillVoucher.getStock();
    if (stock < 1) {
        return Result.fail("库存不足");
    }
    Long userId = UserHolder.getUser().getId();
    // 获取锁
    SimpleRedisLock simpleRedisLock = new SimpleRedisLock("order:" + userId, stringRedisTemplate
    boolean isLock = simpleRedisLock.tryLock(1200);
    // 判断获取锁是否成功
    if (!isLock) {
        // 获取锁失败返回错误信息
        return Result.fail("不允许重复下单!");
    }
    // 获取锁成功进行事务操作
    try {
        // 获取和事务有关的代理对象
        IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
        // 返回订单id
        return proxy.createVoucherOrder(voucherId);
    } finally {
        // 最后必须要释放锁
        simpleRedisLock.unLock();
    }    
}

当统一用户重复下单优惠券时,只会成功执行一次,优惠券也只会减少一张。 

(三)Redis分布式锁误删情况说明

逻辑说明:

        持有锁的线程在锁的内部出现了阻塞,导致他的锁自动释放,这时其他线程,线程2来尝试获得锁,就拿到了这把锁,然后线程2在持有锁执行过程中,线程1反应过来,继续执行,而线程1执行过程中,走到了删除锁逻辑,此时就会把本应该属于线程2的锁进行删除,这就是误删别人锁的情况说明。

解决方案:

        在每个线程释放锁的时候,去判断一下当前这把锁是否属于自己,如果属于自己,则不进行锁的删除。假设还是上面的情况,线程1卡顿,锁自动释放,线程2进入到锁的内部执行逻辑,此时线程1反应过来,然后删除锁,但是线程1,判断当前这把锁不是属于自己,于是不进行删除锁逻辑;当线程2走到删除锁逻辑时,如果没有卡过自动释放锁的时间点,则判断当前这把锁是属于自己的,于是删除这把锁。

(四)解决Redis分布式锁误删问题

        需求:修改之前的分布式锁实现

        满足:在获取锁时存入线程标识(可以用UUID表示);在释放锁时先获取锁中的线程标识,判断是否与当前线程标识一致。

        如果一致则释放锁

        如果不一致则不释放锁

        核心逻辑:在存入锁时,放入自己线程的标识,在删除锁时,判断当前这把锁的标识是不是自己存入的,如果是,则进行删除,如果不是,则不进行删除。

public class SimpleRedisLock implements ILock {
    private String name;
    private static final String KEY_PREFIX = "lock:";
    private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";

    private StringRedisTemplate stringRedisTemplate;

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

    @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);
        // 防止自动拆箱产生null
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unLock() {
        // 获取当前线程的标识
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        // 从Redis中获取锁的标识
        String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
        // 如果两者相等,则删除锁
        if (threadId.equals(id)) {
            // 通过del删除锁
            stringRedisTemplate.delete(KEY_PREFIX + name);
        }
    }
}

(五)分布式锁的原子性问题

更为极端的误删逻辑说明:

        线程1现在持有锁之后,在执行业务逻辑过程中,他正准备删除锁,而且已经走到了条件判断的过程中,比如他已经拿到了当前这把锁确实是属于他自己的,正准备删除锁但是此时他的锁到期了,那么此时线程2进来,但是线程1他会接着往后执行,当他卡顿结束后,他直接就会执行删除锁那行代码,相当于条件判断并没有起到作用,这就是删锁时的原子性问题。

        之所以有删除锁的原子性问题,是因为线程1的拿锁,判断锁,删锁,实际上并不是原子性的。

        也就是说,当线程1获取到锁,执行业务逻辑时,可能会因为锁超时,导致线程1的锁被删掉,此时线程1的业务还未执行完毕,线程2又获取到锁,线程2的业务执行完毕,直接就释放锁了,此时线程1要释放锁,就会把线程2的锁删掉。

(六)Lua脚本解决多条命令原子性问题

1.Lua脚本基本介绍

        Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。Lua是一种编程语言,它的基本语法参考网址:Lua 教程 | 菜鸟教程。Lua的效率很高,我们可以使用lua去操作redis,又能保证他的原子性,这样就可以实现拿锁比锁删锁是一个原子性动作了。

这里重点介绍Redis提供的调用函数,语法如下:

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

例如,我们要执行set name jack,则脚本是这样:

-- 执行 set name jack
redis.call('set', 'name', 'jack')

例如,我们要先执行set name Rose,再执行get name,则脚本如下:

-- 先执行 set name jack
redis.call('set', 'name', 'Rose')
-- 再执行 get name
local name = redis.call('get', 'name')
-- 返回
return name

写好脚本以后,需要用Redis命令来调用脚本,调用脚本的常见命令如下:

如果脚本中的key、value不想写死,可以作为参数传递。key类型参数会放入KEYS数组,其它参数会放入ARGV数组,在脚本中可以从KEYS和ARGV数组获取这些参数: 

注意:KEYS和ARGV必须要大写。

> eval "return redis.call('set','name','jack')" 0
OK
> eval "return redis.call('set',KEYS[1],ARGV[1])" 1 name zhangsan
OK
> eval "return redis.call('set',KEYS[1],ARGV[1])" 1 name ROSE
OK

2.释放锁的Lua脚本 

释放锁的业务流程是这样的

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

最终操作redis的拿锁比锁删锁的lua脚本:

-- 这里的 KEYS[1] 就是锁的key,这里的ARGV[1] 就是当前线程标示
-- 获取锁中的标示,判断是否与当前线程标示一致
if (redis.call('GET', KEYS[1]) == ARGV[1]) then
  -- 一致,则删除锁
  return redis.call('DEL', KEYS[1])
end
-- 不一致,则直接返回
return 0

(七)利用Java代码调用Lua脚本改造分布式锁 

将上面的lua脚本放在resources目录下

修改SimpleRedisLock类

public class SimpleRedisLock implements ILock {
    private String name;
    private static final String KEY_PREFIX = "lock:";
    private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";

    private StringRedisTemplate stringRedisTemplate;

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

    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 boolean tryLock(long timeoutSec) {
        // 获取线程标识作为锁的名称
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        // 获取锁
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
        // 防止自动拆箱产生null
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unLock() {
        // 调用lua脚本
        stringRedisTemplate.execute(
                UNLOCK_SCRIPT,
                // 锁的key
                Collections.singletonList(KEY_PREFIX + name),
                // 获取当前线程的标识
                ID_PREFIX + Thread.currentThread().getId()
        );
    }
}

(八)总结——基于Redis的分布式锁实现思路

利用set nx ex获取锁,并设置过期时间,保存线程标示

释放锁时先判断线程标示是否与自己一致,一致则删除锁

特性:

  • 利用set nx满足互斥性
  • 利用set ex保证故障时锁依然能释放,避免死锁,提高安全性
  • 利用Redis集群保证高可用和高并发特性

(九)基于setnx实现的分布式锁存在的问题

1.重入问题:

        重入问题是指获得锁的线程可以再次进入到相同的锁的代码块中,可重入锁的意义在于防止死锁,比如HashTable这样的代码中,他的方法都是使用synchronized修饰的,假如他在一个方法内,调用另一个方法,那么此时如果是不可重入的,不就死锁了吗?所以可重入锁他的主要意义是防止死锁,我们的synchronized和Lock锁都是可重入的。

2.不可重试:

        是指目前的分布式只能尝试一次,我们认为合理的情况是:当线程在获得锁失败后,他应该能再次尝试获得锁。

3.超时释放:

        我们在加锁时增加了过期时间,这样的我们可以防止死锁,但是如果卡顿的时间超长,虽然我们采用了lua表达式防止删锁的时候,误删别人的锁,但是毕竟没有锁住,有安全隐患。

4.主从一致性:

        如果Redis提供了主从集群,当我们向集群写数据时,主机需要异步的将数据同步给从机,而万一在同步过去之前,主机宕机了,就会出现死锁问题。但是这种情况出现的概率相对较低,因为主从同步是时间往往是在毫秒级别。

因此,使用Redisson可以解决上述的问题。 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【HarmonyOS Next 自定义可拖拽image】

效果图&#xff1a; 代码&#xff1a; import display from "ohos.display" import { AppUtil } from "pura/harmony-utils"/*** 自定义可拖拽图标组件*/ Component export default struct DraggableImage {imageResource?: ResourceimageHeight: numbe…

Jmeter快速实操入门

以下操作需要提前安装了JDK&#xff08;JDK版本要Java8&#xff09;&#xff0c;并且配置了环境变量。 1、下载Jmeter,下载连接。选择zip版本&#xff0c;解压即可。 2、解压后的文件目录如下。 3、进入bin文件夹&#xff0c;双击ApacheJMeter&#xff0c;运行Jmeter。 4、在测…

学习 PostgreSQL 流复制

PostgreSQL 流复制 PostgreSQL数据库异常中止后&#xff0c;数据库刚重启时&#xff0c;会重放停机前最后一个checkpoint点之后的 WAL日志&#xff0c;在把数据库恢复到停机的状态后&#xff0c;自动进入正常的状态&#xff0c;可以接收其他用户的查询和修改。 想象另一个场景…

macbook2015升级最新MacOS 白苹果变黑苹果

原帖&#xff1a;https://www.bilibili.com/video/BV13V411c7xz/MAC OS系统发布了最新的Sonoma&#xff0c;超酷的动效锁屏壁纸&#xff0c;多样性的桌面小组件&#xff0c;但是也阉割了很多老款机型的升级权利&#xff0c;所以我们可以逆向操作&#xff0c;依旧把老款MAC设备强…

2025年最新版武书连SCD期刊(中国科学引文数据库)来源期刊已更新,可下载PDF版!需要的作者进来了解~

2025年最新版武书连SCD期刊&#xff08;中国科学引文数据库&#xff09;来源期刊已更新&#xff01; 官网是不提供免费查询的。小编给大家两个路径&#xff0c;无需下载PDF&#xff0c;随时随地都能查25版SCD目录。 路径一&#xff1a;中州期刊联盟官网&#xff0c;25版SCD目…

deepseek大模型集成到idea

1 下载插件 安装CodeGPT打开 IntelliJ IDEA&#xff0c;鼠标点击左上角导航栏&#xff0c;File --> Setting 2 申请API key 3 配置deepseek 在 Settings 界面中的搜索框中&#xff0c;搜索 CodeGPT&#xff0c;路径 Tools --> CodeGPT --> Providers --> 如下一…

本地部署DeepSeek,并使用UI界面进行快速交互

一.需要本地部署的原因 1.我们在deepseek的官网界面进行交互时&#xff0c;经常会出现如下问题&#xff0c;不能正常交互&#xff0c;很是困扰&#xff1a; 2.本地部署的好处 就是能够很流畅的与deepseek进行交互&#xff1b;也有缺点&#xff0c;现在官网交互的版本更高一点…

8.flask+websocket

http是短连接&#xff0c;无状态的。 websocket是长连接&#xff0c;有状态的。 flask中使用websocket from flask import Flask, request import asyncio import json import time import websockets from threading import Thread from urllib.parse import urlparse, pars…

深度学习之神经网络框架搭建及模型优化

神经网络框架搭建及模型优化 目录 神经网络框架搭建及模型优化1 数据及配置1.1 配置1.2 数据1.3 函数导入1.4 数据函数1.5 数据打包 2 神经网络框架搭建2.1 框架确认2.2 函数搭建2.3 框架上传 3 模型优化3.1 函数理解3.2 训练模型和测试模型代码 4 最终代码测试4.1 SGD优化算法…

【Matlab优化算法-第15期】基于NSGA-II算法的铁路物流园区功能区布局优化

基于NSGA-II算法的铁路物流园区功能区布局优化 一、前言 铁路物流园区的合理布局对于提高物流效率、降低运营成本具有重要意义。随着铁路物流的快速发展&#xff0c;传统的铁路货场需要升级为综合物流园区&#xff0c;以满足多式联运和综合物流服务的需求。本文将介绍一种基于…

手写一个C++ Android Binder服务及源码分析

手写一个C Android Binder服务及源码分析 前言一、 基于C语言编写Android Binder跨进程通信Demo总结及改进二、C语言编写自己的Binder服务Demo1. binder服务demo功能介绍2. binder服务demo代码结构图3. binder服务demo代码实现3.1 IHelloService.h代码实现3.2 BnHelloService.c…

WebSocket connection failed 解决

WebSocket connection failed 解决 前言 这里如果是新手小白不知道 WebSocket 是什么的&#xff1f; 怎么使用的&#xff1f;或者想深入了解的 那可以 点击这里 几分钟带你快速了解并使用&#xff0c;已经一些进阶讲解&#xff1b; WebSocket&#xff0c;多应用于需要双向数据…

Python截图轻量化工具

一、兼容局限性 这是用Python做的截图工具&#xff0c;不过由于使用了ctypes调用了Windows的API, 同时访问了Windows中"C:/Windows/Cursors/"中的.cur光标样式文件, 这个工具只适用于Windows环境&#xff1b; 如果要提升其跨平台性的话&#xff0c;需要考虑替换cty…

字节跳动后端一面

&#x1f4cd;1. Gzip压缩技术详解 Gzip是一种流行的无损数据压缩格式&#xff0c;它使用DEFLATE算法来减少文件大小&#xff0c;广泛应用于网络传输和文件存储中以提高效率。 &#x1f680; 使用场景&#xff1a; • 网站优化&#xff1a;通过压缩HTML、CSS、JavaScript文件来…

Visual Studio踩过的坑

统计Unity项目代码行数 编辑-查找和替换-在文件中查找 查找内容输入 b*[^:b#/].*$ 勾选“使用正则表达式” 文件类型留空 也有网友做了指定&#xff0c;供参考 !*\bin\*;!*\obj\*;!*\.*\*!*.meta;!*.prefab;!*.unity 打开Unity的项目 注意&#xff1a;只是看&#xff0…

智慧机房解决方案(文末联系,领取整套资料,可做论文)

智慧机房解决方案-软件部分 一、方案概述 本智慧机房解决方案旨在通过硬件设备与软件系统的深度整合&#xff0c;实现机房的智能化管理与服务&#xff0c;提升机房管理人员的工作效率&#xff0c;优化机房运营效率&#xff0c;确保机房设备的安全稳定运行。软件部分包括机房管…

ubuntu中如何在vscode的终端目录后显示(当前的git分支名) 实测有用

效果展示 配置过程&#xff1a; 在 Ubuntu 中&#xff0c;如果你想在 VS Code 的终端提示符后显示当前的 Git 分支名&#xff0c;可以通过修改 Shell 配置文件&#xff08;如 ~/.bashrc 或 ~/.zshrc&#xff09;来实现。以下是具体步骤&#xff1a; 1. 确定使用的 Shell 首…

【机器学习案列】车辆二氧化碳排放量预测

这里是引用 &#x1f9d1; 博主简介&#xff1a;曾任某智慧城市类企业算法总监&#xff0c;目前在美国市场的物流公司从事高级算法工程师一职&#xff0c;深耕人工智能领域&#xff0c;精通python数据挖掘、可视化、机器学习等&#xff0c;发表过AI相关的专利并多次在AI类比赛中…

SpringCloud - Sentinel服务保护

前言 该博客为Sentinel学习笔记&#xff0c;主要目的是为了帮助后期快速复习使用 学习视频&#xff1a;7小快速通关SpringCloud 辅助文档&#xff1a;SpringCloud快速通关 源码地址&#xff1a;cloud-demo 一、简介 官网&#xff1a;https://sentinelguard.io/zh-cn/index.h…

基于 GEE 利用插值方法填补缺失影像

目录 1 完整代码 2 运行结果 利用GEE合成NDVI时&#xff0c;如果研究区较大&#xff0c;一个月的影像覆盖不了整个研究区&#xff0c;就会有缺失的地方&#xff0c;还有就是去云之后&#xff0c;有云量的地区变成空值。 所以今天来用一种插值的方法来填补缺失的影像&#xf…