分布式锁(Redis分布式锁)

news2025/2/23 1:09:12

Redis分布式锁原理及应用

  • 前言
  • 一、基本原理
    • 1.1 什么是分布式锁
    • 1.2 分布式锁满足的条件
    • 1.3 常见的分布式锁
  • 二、Redis分布式锁的实现核心思路
    • 2.1 实现分布式锁时需要实现的两个基本方法
    • 2.2 核心思路
  • 三、实现分布式锁版本
  • 四、Redis分布式锁误删情况说明
    • 4.1 逻辑说明
    • 4.2 解决方案
    • 4.3 解决Redis分布式锁误删问题
    • 4.4加锁
    • 4.5 释放锁
  • 五、分布式锁的原子性问题
  • 六、Lua脚本解决多条命令原子性问题
    • 6.1 redis中lua脚本的应用
    • 6.2 释放锁的流程
    • 6.3 利用Java代码调用Lua脚本改造分布式锁
    • 6.4 改造好的获取锁,释放锁方法
  • 总结


前言

跟随b站黑马虎翼老师学习redis:
这是我认为b站上最好的redis教程,各方面讲解透彻,知识点覆盖比较全。
黑马redis视频链接:B站黑马redis教学视频
本文参考黑马redis课程笔记


一、基本原理

1.1 什么是分布式锁

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

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

思路图解:
在这里插入图片描述

1.2 分布式锁满足的条件

  • 可见性:多个线程都能看到相同的结果,注意:这个地方说的可见性并不是并发编程中指的内存可见性,只是说多个进程之间都能感知到变化的意思

  • 互斥:互斥是分布式锁的最基本的条件,使得程序串行执行

  • 高可用:程序不易崩溃,时时刻刻都保证较高的可用性

  • 高性能:由于加锁本身就让性能降低,所有对于分布式锁本身需要他就较高的加锁性能和释放锁性能

  • 安全性:安全也是程序中必不可少的一环
    在这里插入图片描述

1.3 常见的分布式锁

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

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

Zookeeper:zookeeper也是企业级开发中较好的一个实现分布式锁的方案

在这里插入图片描述

二、Redis分布式锁的实现核心思路

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

  • 获取锁

    • 互斥:确保只能有一个线程获取锁
    • 非阻塞:尝试一次,成功返回true,失败返回false
  • 释放锁

    • 手动释放
    • 超时释放:获取锁时添加一个超时时间

2.2 核心思路

我们利用redis 的setNx方法,当有多个线程进入时,我们就利用该方法,第一个线程进入时,redis 中就有这个key 了,返回了1,如果结果是1,则表示他抢到了锁,那么他去执行业务,然后再删除锁,退出锁逻辑,没有抢到锁的哥们,等待一定时间后重试即可
在这里插入图片描述


三、实现分布式锁版本

利用setnx方法进行加锁,同时增加过期时间防止死锁,此方法可以保证加锁和增加过期时间具有原子性

  • 加锁逻辑
    锁的基本接口
public interface ILock{
 //尝试获取锁
 /**
     * @param timeoutSec 锁持有的超时时间,过期后自动释放
     */
    boolean tryLock(long timeoutSec);

    //释放锁
    void unlock();
}

SimpleRedisLock

利用setnx方法进行加锁,同时增加过期时间,防止死锁,此方法可以保证加锁和增加过期时间具有原子性

public class SimpleRedisLock implements ILock{

    @Resource
    private StringRedisTemplate stringRedisTemplate;
    private String name;
    private static final String KEY_PREFIX="lock:";
    private static final String ID_PREFIX= UUID.randomUUID().toString()+"-";

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

    @Override
    public boolean tryLock(long timeoutSec) {
	    // 获取线程标示
	    String threadId = Thread.currentThread().getId()
	    // 获取锁
	    Boolean success = stringRedisTemplate.opsForValue()
	            .setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
	    return Boolean.TRUE.equals(success);
    }
    
    @Override
    public void unlock() {
	    //通过del删除锁
	    stringRedisTemplate.delete(KEY_PREFIX + name);
    }
}
  • 修改业务代码
@Override
    public Result seckillVoucher(Long voucherId) {
        // 1.查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        // 2.判断秒杀是否开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            // 尚未开始
            return Result.fail("秒杀尚未开始!");
        }
        // 3.判断秒杀是否已经结束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            // 尚未开始
            return Result.fail("秒杀已经结束!");
        }
        // 4.判断库存是否充足
        if (voucher.getStock() < 1) {
            // 库存不足
            return Result.fail("库存不足!");
        }
        Long userId = UserHolder.getUser().getId();
        //创建锁对象(新增代码)
        SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
        //获取锁对象
        boolean isLock = lock.tryLock(1200);
		//加锁失败
        if (!isLock) {
            return Result.fail("不允许重复下单");
        }
        try {
            //获取代理对象(事务)
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } finally {
            //释放锁
            lock.unlock();
        }
    }

四、Redis分布式锁误删情况说明

4.1 逻辑说明

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

4.2 解决方案

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

4.3 解决Redis分布式锁误删问题

需求:
修改之前的分布式锁实现,满足:在获取锁时存入线程标示(可以用UUID表示)
在释放锁时先获取锁中的线程标示,判断是否与当前线程标示一致

  • 如果一致则释放锁
  • 如果不一致则不释放锁

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

4.4加锁

private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
@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);
}

4.5 释放锁

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);
    }
}

五、分布式锁的原子性问题

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

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

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

Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。Lua是一种编程语言,它的基本语法大家可以参考网站:https://www.runoob.com/lua/lua-tutorial.html,这里重点介绍Redis提供的调用函数,我们可以使用lua去操作redis,又能保证他的原子性,这样就可以实现拿锁比锁删锁是一个原子性动作了,作为Java程序员这一块并不作一个简单要求,并不需要大家过于精通,只需要知道他有什么作用即可。

6.1 redis中lua脚本的应用

这里重点介绍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命令来调用脚本,调用脚本的常见命令如下:
在这里插入图片描述
例如,我们要执行 redis.call(‘set’, ‘name’, ‘jack’) 这个脚本,语法如下:
在这里插入图片描述
如果脚本中的key、value不想写死,可以作为参数传递。key类型参数会放入KEYS数组,其它参数会放入ARGV数组,在脚本中可以从KEYS和ARGV数组获取这些参数:
在这里插入图片描述

6.2 释放锁的流程

接下来我们来回一下我们释放锁的逻辑:

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

​ 1、获取锁中的线程标示

​ 2、判断是否与指定的标示(当前线程标示)一致

​ 3、如果一致则释放锁(删除)

​ 4、如果不一致则什么都不做

如果用Lua脚本来表示则是这样的:

最终我们操作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

6.3 利用Java代码调用Lua脚本改造分布式锁

lua脚本本身并不需要大家花费太多时间去研究,只需要知道如何调用,大致是什么意思即可,所以在笔记中并不会详细的去解释这些lua表达式的含义。

我们的RedisTemplate中,可以利用execute方法去执行lua脚本,参数对应关系就如下图
在这里插入图片描述

6.4 改造好的获取锁,释放锁方法

package com.hmdp.utils;

import cn.hutool.core.lang.UUID;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import javax.annotation.Resource;
import java.util.Collections;
import java.util.concurrent.TimeUnit;

public class SimpleRedisLock implements ILock{

    @Resource
    private StringRedisTemplate stringRedisTemplate;
    private String name;
    private static final String KEY_PREFIX="lock:";
    private static final String ID_PREFIX= UUID.randomUUID().toString()+"-";
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    static {
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        UNLOCK_SCRIPT.setResultType(Long.class);
    }
    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);
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock() {
        stringRedisTemplate.execute(UNLOCK_SCRIPT, Collections.singletonList(KEY_PREFIX + name),ID_PREFIX+Thread.currentThread().getId());
    }
}

总结

基于Redis的分布式锁实现思路:

  • 利用set nx ex获取锁,并设置过期时间,保存线程标示
  • 释放锁时先判断线程标示是否与自己一致,一致则删除锁
    • 特性:
      • 利用set nx满足互斥性
      • 利用set ex保证故障时锁依然能释放,避免死锁,提高安全性
      • 利用Redis集群保证高可用和高并发特性

笔者总结:我们一路走来,利用添加过期时间,防止死锁问题的发生,但是有了过期时间之后,可能出现误删别人锁的问题,这个问题我们开始是利用删之前 通过拿锁,比锁,删锁这个逻辑来解决的,也就是删之前判断一下当前这把锁是否是属于自己的,但是现在还有原子性问题,也就是我们没法保证拿锁比锁删锁是一个原子性的动作,最后通过lua表达式来解决这个问题

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

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

相关文章

FreeRTOS(4):软件定时器、中断管理

目录 一、延时函数 延时函数分类 vTaskDelay 与 HAL_Delay 的区别 二、软件定时器 什么是定时器&#xff1f; 软件定时器优缺点 软件定时器原理 软件定时器相关配置 单次定时器和周期定时器 1. 创建软件定时器 2. 开启软件定时器 3. 停止软件定时器 4. 复位软件定时…

【剑指 Offer 27】二叉树的镜像

题目&#xff1a; 请完成一个函数&#xff0c;输入一个二叉树&#xff0c;该函数输出它的镜像。 输入&#xff1a;root [4,2,7,1,3,6,9] 输出&#xff1a;[4,7,2,9,6,3,1] 输入输出样例 思考1&#xff1a; 二叉树的镜像&#xff0c;就是交换二叉树的每个节点的左右结点 所…

应用在多媒体手机中的低功率立体声编解码器

多媒体手机一般是指可以录制或播放视频的手机。多媒体的定义是多种媒体的综合&#xff0c;一般是图像、文字、声音等多种结合&#xff0c;所以多媒体手机是可以处理和使用图像文字声音相结合的移动设备。目前流行的多媒体概念&#xff0c;主要是指文字、图形、图像、声音等多种…

【数据结构】图文并茂,通过逻辑图带你轻松拿捏链表,实现各种接口功能(2)

君兮_的个人主页 勤时当勉励 岁月不待人 C/C 游戏开发 Hello,米娜桑们&#xff0c;这里是君兮_&#xff0c;我们接着之前讲过的顺序表来继续介绍初阶数据结构的内容&#xff0c;今天给大家带来的是有关链表的基本知识和各种接口功能的实现的第二部分。 好了&#xff0c;废话不…

Gitignore忽略文件

默认情况下&#xff0c;Git会监视我们项目中的所有内容&#xff0c;但是有些内容比如mode_modules中的内容&#xff0c;我们不希望他被Git所管理。 我们可以在我们项目目录中添加一个 .gitignore 文件来设置那些需要git忽略的文件。

Burpxss自动化测试工具validator配置和使用教程

一、配置教程 下载Phantomjs&#xff1a; http://phantomjs.org/download.html 下载xss.js https://github.com/nVisium/xssValidator 将xss.js和phantomjs.exe放在一起 利用phantomjs运行xss.js C:\xss>phantomjs.exe xss.js Bapp store里搜索xss validator,然后安装它 安…

基于jeecg-boot的flowable流程提供一种动态设置发起人部门负责人的方式

更多功能看演示系统 gitee源代码地址 后端代码&#xff1a; https://gitee.com/nbacheng/nbcio-boot 前端代码&#xff1a;https://gitee.com/nbacheng/nbcio-vue.git 在线演示&#xff08;包括H5&#xff09; &#xff1a; http://122.227.135.243:9888 这里给大家提供一种…

node.js系列-常见问题处理方案(持续更新)

问题1&#xff1a;nodejs 如何使用 atob、btoa 解决方案&#xff08;base64与uint8array转换&#xff09;&#xff0c;btoa和atob在nodejs中应该怎么写&#xff1f; 浏览器中我们可以这样使用&#xff1a; btoa(123456) MTIzNDU2 atob(MTIzNDU2) 123456node.js中实现方案 con…

国内GitHub加速访问工具-Fetch GitHub Hosts

一、工具介绍 Fetch GitHub Hosts是一款开源跨平台的国内GitHub加速访问工具&#xff0c;主要为解决研究及学习人员访问 Github 过慢或其他问题而提供的 Github Hosts 同步工具。 项目原理&#xff1a;是通过部署此项目本身的服务器来获取 github.com 的 hosts&#xff0c;而…

LeetCode //C - 289. Game of Life

289. Game of Life According to Wikipedia’s article: “The Game of Life, also known simply as Life, is a cellular automaton devised by the British mathematician John Horton Conway in 1970.” The board is made up of an m x n grid of cells, where each cell…

【雕爷学编程】MicroPython动手做(33)——物联网之天气预报

天气&#xff08;自然现象&#xff09; 是指某一个地区距离地表较近的大气层在短时间内的具体状态。而天气现象则是指发生在大气中的各种自然现象&#xff0c;即某瞬时内大气中各种气象要素&#xff08;如气温、气压、湿度、风、云、雾、雨、闪、雪、霜、雷、雹、霾等&#xff…

Prometheus + Grafana安装

Prometheus是一款基于时序数据库的开源监控告警系统&#xff0c;非常适合Kubernetes集群的监控。Prometheus的基本原理是通过HTTP协议周期性抓取被监控组件的状态&#xff0c;任意组件只要提供对应的HTTP接口就可以接入监控。不需要任何SDK或者其他的集成过程。这样做非常适合做…

Gogs Git windos服务搭建指南

Gogs Git服务器搭建指南 背景&#xff1a; 近期在Linux 麒麟 v10 系统上开发&#xff1b;为了团队协同编程&#xff1b;选用了Git服务器&#xff1b;之前在windos开始时候使用的visualSVN server; visualSVN server从4.x.x.x开始收费&#xff1b;限制15个开发者用户&#xff…

带头循环双向链表详解

目录 一、什么是带头循环双向链表&#xff1f; 1.特点&#xff1a; 2.优点&#xff1a; 二、实现接口 1.前置准备 1.1需要的三个文件 1.2结构体的创建和头文件的引用 2.接口实现 2.1函数创建新节点 2.2打印链表内容 2.3尾插新节点 2.4头插新节点 2.5头删节点 2.6尾删…

Vue3_04_ref 函数和 reactive 函数

ref 函数 声明变量时&#xff0c;赋值的值要写在 ref() 函数中修改变量时&#xff0c;变量名.value xxx在模板中使用时可以省略掉 .value&#xff0c;直接使用变量名即可 <template><h1>一个人的信息</h1><h2>姓名&#xff1a;{{name}}</h2><…

pjsip、pjsua2+bcg729 windows下编译java版本

文章目录 简要说明流程步骤 简要说明 基本参考的这里 https://docs.pjsip.org/en/latest/get-started/windows/build_instructions.html#building-the-projects 我这里主要是为了生成pjsua2.dll 用于在java下调用。 其中 libbcg729.dll 是通过vcpkg来进行安装。 pjsip使用vs2…

【Autoresizing案例03-通过代码设置Autoresizing Objective-C语言】

一、通过代码实现Autoresizing 1.好,那么,接下来,我们就给大家看一下,怎么来通过代码实现Autoresizing, 好,那么,接下来,我们为什么要说这个通过代码,实现Autoresizing, 那么,注意,我们能通过我们的storyboard来操作,就不要通过代码来实现, 以后的趋势,就是苹…

使用Git在GitHub上部署静态页面

在GitHub中&#xff0c;我们可以将自己的静态页面部署到GitHub中&#xff0c;它会给我们提供一个地址使得我们的页面变成一个真正的网站&#xff0c;可以供用户访问。 一、在GitHub下创建仓库 二、将项目部署到GitHub上 1. 初始化Git仓库 2. 提交代码 3. 关联远程仓库 在Gi…

Vulnhub: BlueMoon: 2021靶机

kali&#xff1a;192.168.111.111 靶机&#xff1a;192.168.111.174 信息收集 端口扫描 nmap -A -sC -v -sV -T5 -p- --scripthttp-enum 192.168.111.174 80端口目录爆破&#xff0c;发现文件&#xff1a;hidden_text gobuster dir -u http://192.168.111.174 -w /usr/sha…

二重积分1

目录 二重积分 二重积分的性质 ​编辑 中值定理 二重积分的计算 方法1&#xff1a;利用直角坐标计算 方法2&#xff1a;利用极坐标进行计算 适用于极坐标的二重积分的特征 对称性和奇偶性的应用 题目 例题1&#xff1a; 题目2&#xff1a; 题目3&#xff1a; 题目4&#x…