【Redis】分布式锁基本理论与简单实现

news2024/11/27 6:40:56

目录

  • 分布式锁
    • 解释
    • 作用
    • 特性
    • 实现方式
      • MySQL、Redis、Zookeeper三种方式对比
    • 原理
  • reids分布式锁原理
    • 目的
    • 容错
    • redis简单分布式锁实现
      • 锁接口
      • 实现类
      • 下单场景的实现
      • 容错场景1
        • 解决思路
        • 优化代码
      • 容错场景2
        • Lua脚本
          • Redis利用Lua脚本解决多条命令原子性问题
        • 释放锁的业务流程
          • Lua脚本来表示
        • 优化代码
  • 总结

分布式锁

解释

  • 分布式锁是一种用于协调分布式系统中多个节点对共享资源进行访问的机制。
  • 在分布式系统中,多个节点可能同时竞争同一个资源,并且可能同时进行修改操作,这就会导致数据的不一致性和并发冲突的问题。
  • 为了解决这个问题,引入了分布式锁机制。

作用

  • 分布式锁可以确保在同一时刻只有一个节点能够对共享资源进行访问操作,其他节点需要等待该节点释放锁之后才能进行操作。
  • 分布式锁可以通过网络通信来实现,常见的实现方式有基于数据库的锁、基于缓存的锁、基于ZooKeeper的锁等。
  • 使用场景:分布式任务调度、分布式缓存、分布式事务等场景

特性

  1. 互斥性: 同一时刻只有一个节点能够获取到锁,其他节点需要等待。
  2. 可重入性: 同一个节点在获取到锁之后可以再次获取锁而不会被阻塞。
  3. 容错性: 锁的释放需要能够容忍节点的故障,确保锁能够被正常释放。
  4. 高性能: 分布式锁的实现需要保证高性能,避免成为系统的瓶颈。

实现方式

  1. 基于数据库:使用关系型数据库或者其他支持事务的数据库来实现分布式锁。可以通过在数据库中创建一个带有唯一索引的表或者行来确保只有一个进程能够成功获取锁。
  2. 基于文件系统:使用共享的文件系统来实现分布式锁。可以通过创建一个特定的文件来表示锁的状态,进程需要先创建文件或者尝试获得文件的独占写锁来获取锁。
  3. 基于ZooKeeper:使用ZooKeeper来实现分布式锁。可以通过在ZooKeeper中创建一个临时节点来表示锁的状态,只有创建成功的进程才能获取锁。
  4. 基于Redis:使用Redis的原子操作来实现分布式锁。可以通过在Redis中设置一个带有过期时间的键来表示锁的状态,只有成功设置锁的进程才能获取锁。

MySQL、Redis、Zookeeper三种方式对比

 MySQLRedisZookeeper
互斥利用MySQL本身的互斥锁的机制利用redis中setnx的互斥命令利用节点的唯一性和有序性来实现互斥
高可用
高性能一般一般
安全性断开连接,自动释放锁利用锁超时时间。到期自动释放临时节点,断开连接自动释放

原理

在这里插入图片描述

reids分布式锁原理

Redis分布式锁的原理基于Redis的单线程特性以及原子操作的特点。具体原理如下:

  1. 获取锁:当一个节点要获取分布式锁时,它会向Redis发送一个SETNX命令,将一个特定的键值对设置到Redis中。如果该键不存在,节点成功获取锁,并将该键值对设置为锁的持有者标识。如果该键已经存在,表示锁已经被其他节点持有,节点获取锁失败。

  2. 释放锁:当一个节点要释放分布式锁时,它会向Redis发送一个DEL命令,将该键值对从Redis中删除。只有持有锁的节点才能成功释放锁。

目的

  • 这样的实现基于Redis的SETNX命令的原子性保证,SETNX命令的语义是
    • 当键不存在时,设置键值对并返回1;
    • 当键已存在时,不设置值并返回0。
  • 通过SETNX命令的原子性,可以保证同一时刻只有一个节点能够成功获取锁。

容错

  • 为了防止分布式锁的死锁问题,可以为获取锁的操作设置一个过期时间。
  • 节点在获取锁的同时,可以为该键设置一个带有过期时间的键值对,确保即使节点在获取锁之后发生故障,如果过期时间到了,Redis也会自动释放该锁。
  • 为了提高分布式锁的可用性和容错性,还需要引入一些额外的机制,例如设置一个超时时间,避免长时间持有锁导致的问题。
  • 还可以使用分布式锁的续约机制,即在获取锁之后,定期向Redis发送续约命令,更新锁的过期时间,确保节点在持有锁的期间不会被自动释放。
    在这里插入图片描述

redis简单分布式锁实现

锁接口

public interface ILock {

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

    /**
     * 释放锁,有加锁就要有释放锁
     */
    void unlock();
}

实现类

public class SimpleRedisLock implements ILock {

	// 业务名称
    private String name;
    private StringRedisTemplate stringRedisTemplate;

	// 通过构造方法将name和stringRedisTemplate传入
    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    private static final String KEY_PREFIX = "lock:";

    @Override
    public boolean tryLock(long timeoutSec) {
        // 获取线程标识
        long 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);
    }
}

下单场景的实现

// 使用Redis分布式锁
// 创建锁对象
SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
// 获取锁对象
boolean isLock = lock.tryLock(5);
// 加锁失败
if (!isLock) {
   return Result.fail("不允许重复下单");
}
try {
   // 获取代理对象(事务)
   IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
   return proxy.createVoucherOrder(voucherId);
} finally {
   // 释放锁
   lock.unlock();
}

容错场景1

  1. 线程1先获取锁后,由于业务阻塞还没执行完成,线程1的锁超时后自动释放
  2. 线程2在线程1的锁超时自动释放后,进行加锁成功
  3. 正好线程1将业务接着执行完后,需要释放锁,此时释放的就是线程2的锁,造成了误删问题
  4. 误删后,线程3又加锁成功,此时,线程2和线程3就出现了并发执行业务,造成并发安全问题

在这里插入图片描述

解决思路
  • 在获取锁时:存入线程标识,比如可以用UUID这类的唯一序列
  • 在释放锁时:先获取锁中的线程标识,判断是否与当前线程标识一致
    • 如果一致则释放锁
    • 如果不一致则不释放锁
  • 不要直接将线程id作为线程标识,因为不同JVM中的线程id可能一样,所以可以用 线程id+UUID 作为线程标识
    在这里插入图片描述
优化代码
public class SimpleRedisLock implements ILock {

    // 业务名称
    private String name;
    private StringRedisTemplate stringRedisTemplate;

    // 通过构造方法将name和stringRedisTemplate传入
    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    private static final String KEY_PREFIX = "lock:";
    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);
    }

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

容错场景2

  1. 线程1执行完业务后,准备释放锁
  2. 先判断完锁一致后,正准备释放时,发生了阻塞(例如:GC时所有线程会阻塞),恰好线程1在阻塞期间,锁超时被释放
  3. 线程2获取锁成功,此时线程1被唤醒后,继续释放锁,由于之前判断过锁的标识,所以直接释放锁,但是此时的锁是线程2的
  4. 线程3又加锁成功,此时,线程2和线程3就出现了并发执行业务,造成并发安全问题
    在这里插入图片描述
Lua脚本
  • Lua脚本是一种轻量级的编程语言,用于嵌入式系统和游戏开发中。其设计目标是为了简单、可扩展和快速。
  • Lua脚本具有简洁的语法和功能强大的特性,包括动态类型、自动内存管理和高阶函数支持。它可以被嵌入到其他程序中,以提供脚本化的功能。由于其轻量级和高性能的特点,Lua脚本被广泛应用于游戏脚本、应用程序的扩展和配置文件等方面。
  • Lua脚本可以通过与其他编程语言的接口交互,例如C、C++和Java,使开发人员可以在应用程序中使用Lua脚本来实现灵活的功能和逻辑。此外,Lua还具有丰富的标准库和大量的第三方库,使开发人员能够快速开发出各种类型的应用程序。
Redis利用Lua脚本解决多条命令原子性问题
  • Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性

    # 执行Redis命令
    redis.call('命令名称', 'key', '其他参数', ...)
    
  • 例如,我们要先执行set name zhangsan,再执行get name,则脚本如下:

    # 先执行 set name zhangsan
    redis.call('set', 'name', 'zhangsan')
    # 再执行 get name
    local name = redis.call('get', 'name')
    # 返回
    return name
    
  • 写好脚本以后,需要用Redis命令来调用脚本,例如,我们要执行 redis.call(‘set’, ‘name’, ‘jack’) 这个脚本,语法如下:

    • 双引号内表示脚本内容
    • 最后的0表示脚本需要的key类型的参数个数
      EVAL "return redis.call('set','name','zhangsan')" 0
      
  • 如果脚本中的key、value不想写死,可以作为参数传递。key类型参数会放入KEYS数组,其它参数会放入ARGV数组,在脚本中可以从KEYS和ARGV数组获取这些参数:

    • name传给KEYS[1]
    • zhangsan传给ARGV[1]
      EVAL "return redis.call('set',KEYS[1],ARGV[1])" 1 name zhangsan
      
释放锁的业务流程
  1. 获取锁中的线程标识
  2. 判断是否与指定的标识(当前线程标识)一致
    • 如果一致则释放锁(删除)
    • 如果不一致则什么都不做
Lua脚本来表示
-- 这里的 KEYS[1] 就是锁的key,这里的ARGV[1] 就是当前线程标识
-- 获取锁中的标识,判断是否与当前线程标识一致
if (redis.call('GET', KEYS[1]) == ARGV[1]) then
  -- 一致,则删除锁
  return redis.call('DEL', KEYS[1])
end
-- 不一致,则直接返回
return 0
优化代码
  • 基于Lua脚本实现分布式锁的释放锁逻辑
  • RedisTemplate调用Lua脚本的API如下:
    在这里插入图片描述
public class SimpleRedisLock implements ILock {

    // 业务名称
    private String name;
    private StringRedisTemplate stringRedisTemplate;

    // 通过构造方法将name和stringRedisTemplate传入
    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    private static final String KEY_PREFIX = "lock:";
    private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
    // 加载Lua脚本
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    static {
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        //将编写的Lua脚本放在resources目录下,比如名称为:unlock.lua
        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);
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlockL() {
        // 调用Lua脚本
        stringRedisTemplate.execute(
                UNLOCK_SCRIPT,
                Collections.singletonList(KEY_PREFIX + name),
                ID_PREFIX + Thread.currentThread().getId());
    }

总结

  • Redis的分布式锁实现其实就是利用setnx/setex获取锁,并设置过期时间,保存线程标识

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

  • Redis的分布式的优点:

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

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

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

相关文章

SpringMVC系列九: 数据格式化与验证及国际化

SpringMVC 数据格式化基本介绍基本数据类型和字符串自动转换应用实例-页面演示方式Postman完成测试 特殊数据类型和字符串自动转换应用实例-页面演示方式Postman完成测试 验证及国际化概述应用实例代码实现注意事项和使用细节 注解的结合使用先看一个问题解决问题 数据类型转换…

游泳耳机哪个牌子好性价比高?精选高性价比的四大游泳耳机!

在现代社会中&#xff0c;随着健身和水中运动的普及&#xff0c;游泳耳机作为一种关键的健身配件&#xff0c;正日益受到广泛关注和需求。无论是在游泳池畅游还是深潜海底&#xff0c;好的游泳耳机不仅能提供高品质的音乐享受&#xff0c;更能保护耳朵免受水压和湿润环境的侵害…

训练营第四十一天| 1035.不相交的线53. 最大子序和392.判断子序列115.不同的子序列

1035.不相交的线 力扣题目链接(opens new window) 我们在两条独立的水平线上按给定的顺序写下 A 和 B 中的整数。 现在&#xff0c;我们可以绘制一些连接两个数字 A[i] 和 B[j] 的直线&#xff0c;只要 A[i] B[j]&#xff0c;且我们绘制的直线不与任何其他连线&#xff08;…

线上扭蛋机小程序开发,潮玩时代的创新发展

随着互联网的发展&#xff0c;扭蛋机市场也进行了创新发展&#xff0c;线上扭蛋机小程序为市场带来了新活力。扭蛋机小程序将传统的模式与互联网结合&#xff0c;打造一个便捷有趣的扭蛋机市场。 一、扭蛋机小程序 在扭蛋机小程序上&#xff0c;用户通过扭蛋机抽取各种系列的…

实现锚点链接点击tab跳转到指定位置 并且滚动鼠标顶部锚点的样式也跟随变化

实现效果如下 不管是点击还是 滚动鼠标 顶部的样式也会跟随变化 点击会跳转到指定的位置 通过IntersectionObserver 监听是否可见 下面代码可以直接执行到vue的文件 <template><div><ul class"nav"><li v-for"tab in tabs" :key…

【Java】已解决Spring框架中的org.springframework.dao.DuplicateKeyException异常

文章目录 一、问题背景二、可能出错的原因三、错误代码示例四、正确代码示例五、注意事项 已解决Spring框架中的org.springframework.dao.DuplicateKeyException异常 一、问题背景 在使用Spring框架进行数据库操作时&#xff0c;有时会遇到org.springframework.dao.Duplicate…

HTML 全局属性介绍及示例

HTML 全局属性是一组可以在任何HTML元素中使用的属性。这些属性提供了一种方式来定义元素的通用行为或外观。以下是一些常见的HTML全局属性及其示例。 id id 属性为元素提供了一个唯一的标识符。它不能在 <head>, <html>, <meta>, <script>, <sty…

LearnOpenGL 及 ShaderToy 的 CMake 构建框架

文章目录 构建目标具体框架根目录src 目录app 目录import.cmake其他 CMake 函数 使用框架实际效果摄像机坐标变换使用 assimp 库加载模型shadertoy 测试 framebuffer 离屏渲染 其他 为了复习 OpenGL&#xff08;主要是看到 shadertoy 上有好玩的着色器&#xff09;&#xff0c;…

Python类的优势及应用场景深度分析(代码封装与组织、继承与代码复用、多态与接口、状态管理与行为封装)(python class)

文章目录 Python 类的优势及应用场景深度分析1. 代码封装与组织1.1 封装性示例代码&#xff1a;用户账户管理 1.2 组织性 2. 继承与代码复用2.1 继承性示例代码&#xff1a;员工管理系统 3. 多态与接口3.1 多态性示例代码&#xff1a;图形渲染 4. 状态管理与行为的封装4.1 状态…

记录一下 Chrome浏览器打印时崩溃问题

问题描述&#xff1a; 为了查看页面内存占用情况&#xff0c;按F2,打开Memory chrome浏览器点击“打印”按钮&#xff0c;或Ctrl P 时出现如下页面 一直以为是页面问题&#xff0c;每次打印的时候遇到这种 崩溃现象 就是重新刷新页面 但今天刚开一个页面&#xff0c;内存 …

微信小程序 - 出于性能原因,对长行跳过令牌化。长行的长度可通过 “editor.maxTokenizationLineLength” 进行配置

问题描述 出于性能原因&#xff0c;对长行跳过令牌化。长行的长度可通过 “editor.maxTokenizationLineLength” 进行配置。 解决方案 设置 - 编辑器设置 - 更多编辑器设置... 搜索&#xff1a;maxtoken&#xff0c;原来是 20000&#xff0c;我改成了 200000 即可~

电脑已删除的文件在回收站找不到怎么办?数据恢复办法分享!

电脑中的数据已经成为了我们生活和工作的重要部分。无论是珍贵的照片、重要的文档&#xff0c;还是日常的工作文件&#xff0c;我们都希望能够妥善保存很久。 然而&#xff0c;误删除文件的情况时有发生&#xff0c;而当我们急切地打开回收站试图找回这些文件时&#xff0c;却…

Bev感知:sparse query

文章目录 1. 显示Bev方法介绍1.1 2D to 3D: LSS-based1.1.1 优点1.1.2 缺点1.2. 3D to 2D: BevFormer1.2.1 缺点1.2.2优点1.3 常见的Bev感知的问题2. Sparse query2.1 PETRv1创新点3D 位置编码实验对比2.2 PETRv22.2.1 时序对齐2.2.2 Feature guided 3D PE2.2.3 多任务2.2.3 性…

功能测试的内容与目的是什么?

在软件开发与测试过程中&#xff0c;功能测试是不可或缺的关键步骤&#xff0c;它主要关注软件产品是否能够按照设计规格和用户需求实现预定的功能。功能测试的内容与目的&#xff0c;简单来讲&#xff0c;就是验证软件的各种特性和功能是否正确、完整且符合预期&#xff0c;确…

【C#】汽车租赁系统设计与实现

目的&#xff1a; 设计一个简单的汽车租赁系统&#xff0c;包含以下功能&#xff1a; 添加车辆&#xff1a;用户可以添加新的车辆到系统中&#xff0c;包括车辆的品牌、型号、车牌号、日租金等信息。查找车辆&#xff1a;用户可以通过车牌号或者品牌来查找车辆&#xff0c;并…

SFNC —— 采集控制(四)

系列文章目录 SFNC —— 标准特征命名约定&#xff08;一&#xff09; SFNC —— 设备控制&#xff08;二&#xff09; SFNC —— 图像格式控制&#xff08;三&#xff09; SFNC —— 采集控制&#xff08;四&#xff09; 文章目录 系列文章目录5、采集控制&#xff08;Acquisi…

第6章 设备驱动程序(2)

目录 6.3 和文件系统关联 6.3.1 inode的设备文件成员 6.3.2 标准文件操作 6.3.3 字符设备的标准操作 6.3.4 块设备的标准操作 6.4 字符设备操作 6.4.1 表示字符设备 6.4.2 打开设备文件 6.4.3 读写操作 本专栏文章将有70篇左右&#xff0c;欢迎关注&#xff0c;查看后…

Vue项目中实现骨架占位效果-demo

创建组件 Skeleton.vue <template><div class"skeleton"><div class"skeleton-item" v-for"n in count" :key"n"></div></div> </template><script> export default {props: {count: {ty…

物联网技术-第3章物联网感知技术-3.2定位技术

目录 1.1位置信息和位置服务 1.1.1位置信息 1.1.2位置服务 1.2主流定位系统 1.2.1卫星定位系统&#xff08;Satellite Positioning Systems&#xff09; 1.2.2移动通信蜂窝基站定位&#xff08;Cellular Triangulation or Advanced Forward Link Trilateration&#xff09…

Unity2D游戏制作入门 | 14( 之人物实装攻击判定 )

上期链接&#xff1a;Unity2D游戏制作入门 | 13 ( 之人物三段攻击 )-CSDN博客 上期我们聊到给人物添加三段攻击的动画&#xff0c;通过建立新的图层动画当我们按下攻击按键就会自动切换进攻击的动画&#xff0c;如果我们连续按下攻击键&#xff0c;我们还可以进行好几段的攻击…