四、分布式锁之自定义分布式锁

1、基本原理和实现方式对比

分布式锁:满足分布式系统或集群模式下多个进程可见并且互斥的锁。分布式锁的核心思想就是多线程都使用同一把锁,实现程序串行执行。
1653374296906.png
分布式锁需要具备的条件:
1653381992018.png

特性含义
可见性多个线程都能感知到变化
互斥性分布式锁的最基本的特性,让程序串行执行
高可用程序不易崩溃,时刻保证较高的可用性
高性能要求分布式锁具备较高的加锁和释放锁性能
安全性要求分布式锁具备一定的安全性

常见的分布式锁有三种:
Mysql: mysql本身就带有锁机制,但是由于mysql性能本身一般,所以采用分布式锁的情况下,其实使用mysql作为分布式锁比较少见
Redis: redis作为分布式锁是非常常见的一种使用方式,现在企业级开发中基本都使用redis或者zookeeper作为分布式锁,利用setnx这个方法,如果插入key成功,则表示获得到了锁,如果有人插入成功,其他人插入失败则表示无法获得到锁,利用这套逻辑来实现分布式锁
Zookeeper: zookeeper也是企业级开发中较好的一个实现分布式锁的方案,这里不过多阐述。
1653382219377.png

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

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

  • 获取锁
    • 互斥:只能有一个线程成功获取到锁
    • 非阻塞:尝试获取一次,成功返回true,失败返回false
  • 释放锁
    • 手动释放
    • 超时释放:避免服务宕机导致出现死锁

核心思路:利用redis的setnx特性实现锁的互斥。当第一个线程setnx返回1,代表它获取锁成功,可以执行业务,然后释放锁;其他线程则等待一段时间后进行重试。
image.png

3、实现分布式锁 V1.0

  • 锁对象接口
public interface ILock {

    /**
   * 尝试获取锁
   * @param timeoutSec 超时时间(秒)
   * @return
   */
    boolean tryLock(long timeoutSec);

    /**
   * 释放锁
   */
    void unlock();
}
  • 锁对象实现类
public class SimpleRedisLock implements ILock {

    private StringRedisTemplate stringRedisTemplate;

    // 锁的名字(一般与当前业务模块相关)
    private String name;
    private String LOCK_PREFIX = "lock:";

    public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.name = name;
    }
    @Override
    public boolean tryLock(long timeoutSec) {
        // value建议设置当前线程的id
        long threadId = Thread.currentThread().getId();
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(LOCK_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
        // 不要直接返回success,自充拆箱可能会出现空指针异常
        return BooleanUtil.isTrue(success);
    }

    @Override
    public void unlock() {
        stringRedisTemplate.delete(LOCK_PREFIX + name);
    }
}
  • 业务类-VoucherOrderServiceImpl

核心代码:

// 使用分布式锁实现一人一单
SimpleRedisLock lock = new SimpleRedisLock(stringRedisTemplate, "order:" + userId);
// 尝试获取锁
boolean isLock = lock.tryLock(1200);
if (!isLock) {
    return Result.fail("不允许重复下单");
}
try {
    return oneUserAndOrder(voucherId);
} finally {
    lock.unlock();
}
/**
 * 一人一单
 *
 * @param voucherId
 * @return
 */
@Transactional
/*
    1、将锁放在方法体上,那么这个方法就是一个同步方法,只有一个线程能够进入,会导致性能问题
 */
public /*synchronized */Result oneUserAndOrder(Long voucherId) {
    Long userId = UserHolder.getUser().getId();
    /*
        2、将锁放在方法体内存在的问题:方法执行完毕后,锁会被释放,但事务是由Spring管理的
        此时,事务还未提交,锁就被释放了,下一个进程进来,仍会出现线程安全问题
     */
//        synchronized (userId.toString().intern()){
    // 保证一人一单
    Integer count = query().eq("voucher_id", voucherId)
            .eq("user_id", userId).count();
    if (count > 0) {
        return Result.fail("id为:" + userId + "的用户已经购买过该秒杀券");
    }

    // 扣减库存,添加乐观锁
    boolean success = seckillVoucherService.update()
            .setSql("stock = stock - 1")
            .eq("voucher_id", voucherId)
            // 这种方式反而会增加下单的失败率
//                .eq("stock", voucher.getStock())
            // 只要我库存还大于0,就允许用户继续下单
            .gt("stock", 0)
            .update();
    if (!success) {
        return Result.fail("秒杀券已售罄");
    }
    // 生成订单
    VoucherOrder order = new VoucherOrder();
    long orderID = redisIdWorker.nextId("order");
    order.setId(orderID);
    order.setVoucherId(voucherId);
    order.setUserId(UserHolder.getUser().getId());
    save(order);
    return Result.ok(orderID);
}
  • 单元测试

image.png
image.png

可以发现,集群模式下,两个线程同时争抢锁,只有一个线程成功获取到锁,实现了分布式锁的互斥!

4、分布式锁误删问题

4.1、误删问题

现考虑一种在分布式锁情况下仍会导致线程安全问题的极端情况:

  1. 线程1获取锁,获取成功,但因业务阻塞问题,导致分布式锁的TTL过期,锁失效
  2. 线程2获取锁,获取成功。
  3. 线程1执行完业务,释放锁,也就是把线程2的锁给释放掉了。
  4. 线程3获取锁,获取成功。
  5. 线程2执行完业务,释放锁,也就是释放了线程3的锁
  6. 线程3执行完业务,执行释放锁。

这种情况下,线程2和线程3存在线程安全问题。
导致该问题出现的本质原因在于线程在去释放锁的时候,不加判断,都不看这锁是不是自己的就给人家释放了。

4.2、解决方案

分布式锁会被误删的关键是redis再去删除数据的时候,没有做判断,当前线程没有判断在redis中存储的锁是不是自己的那把锁就直接给删掉了。
解决方案:给锁添加唯一标识(UUID),删除前做一次查询,判断是不是自己的那把锁,如果是,再做删除操作。

  • 核心代码更新

获取锁
image.png
删除锁
image.png

  • 测试

准备两个线程
image.png
线程1成功获取锁
image.png
image.png
通过手动删除锁,模拟线程1因业务阻塞导致锁过期被删除
线程2成功获取锁
image.png
线程1执行完业务,删除锁
image.png
线程2执行完业务,删除锁
image.png

至此,就避免了分布式锁误删的问题!

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

5.1、原子性问题

目前仍存在一种更为极端的情况会导致分布式锁误删问题

  1. 线程1正常获取锁,执行业务逻辑,执行完毕准备删除锁
  2. 经过判断的确是自己的锁,此事发生线程阻塞等意外导致分布式锁TTL到期
  3. 线程2进入,获取到锁
  4. 切回到线程1,由于之前已经判断过是自己的锁了,直接执行释放锁操作

由此造成了分布式锁的误删问题
造成该问题出现的本质原因是:释放锁的查询判断和删除操作不具备原子性

5.2、通过Lua脚本解决原子性问题

Lua 是一种轻量级的编程语言,具有简洁的语法和强大的功能。它是一种动态类型的语言,支持函数式编程和面向对象编程。Lua 是一种嵌入式脚本语言,可以轻松地集成到其他应用程序中。
Redis提供了对Lua的支持实现
Spring提供了调用Lua脚本的API
基于这些特性,保证分布式锁删除操作原子性的实现思路:

  1. 将锁查询及删除操作写入到Lua脚本;
  2. 通过Spring调用编写好的Lua脚本

由于在Java中只有调用Lua脚本这一行操作语句,从而保证了原子性

  • unlock.lua
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
  • 释放锁核心代码
public class SimpleRedisLock implements ILock {

    private StringRedisTemplate stringRedisTemplate;

    // 锁的名字(一般与当前业务模块相关)
    private String name;
    private String LOCK_PREFIX = "lock:";
    final String uniqueStr = UUID.randomUUID().toString(true) + "-";

    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(StringRedisTemplate stringRedisTemplate, String name) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.name = name;
    }

    @Override
    public boolean tryLock(long timeoutSec) {
        // value建议设置当前线程的id
        long threadId = Thread.currentThread().getId();
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(LOCK_PREFIX + name, uniqueStr + threadId, timeoutSec, TimeUnit.SECONDS);
        // 不要直接返回success,自充拆箱可能会出现空指针异常
        return BooleanUtil.isTrue(success);
    }

    /**
     * 通过Lua脚本释放锁,保证操作的原子性
     */
    @Override
    public void unlock() {
        stringRedisTemplate.execute(UNLOCK_SCRIPT, Collections.singletonList(LOCK_PREFIX + name), uniqueStr + Thread.currentThread().getId());
    }


//    @Override
//    public void unlock() {
//        // 查询当前线程的锁
//        String lock = stringRedisTemplate.opsForValue().get(LOCK_PREFIX + name);
//        // 如果当前线程的锁是自己的,才能删除
//        if (lock != null && lock.equals(uniqueStr + Thread.currentThread().getId())){
//            stringRedisTemplate.delete(LOCK_PREFIX + name);
//        }
//    }
}

至此,解决了因操作原子性而造成的分布式锁误删问题

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

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

相关文章

Orange3数据预处理(行选择组件)

选择行 根据数据特征的条件选择数据实例。 输入 数据&#xff1a;输入数据集 输出 匹配数据&#xff1a;满足条件的实例 不匹配数据&#xff1a;不满足条件的实例 数据&#xff1a;带有额外列的数据&#xff0c;显示实例是否被选中 这个小部件根据用户…

数据库系统概论-第16章 数据仓库与联机分析处理技术

概念性的介绍&#xff0c;一略而过&#xff0c;不重要。 16.1 数据仓库技术 16.2 联机分析处理技术 16.3 数据挖掘技术 16.4 大数据时代的新型数据仓库 16.5 小结

jetson nano torch1.6 torchvision0.7.0 yolov5

pytorch版本对应关系查看网址&#xff1a; pytorch torchvision pytorch安装方式 点击pytorch链接&#xff1a;pytorch torchvision安装方式 sudo apt-get install libjpeg-dev zlib1g-dev libavcodec-dev libavformat-dev libswscale-dev git clone --branch v0.7.0 https…

第113讲:Mycat实践指南:按照单位为天的日期实现水平分表

文章目录 1.按天分片的概念1.按天分片的概念 2.按照天数对某张表进行水平拆分2.1.在所有的分片节点中创建表结构2.2.配置Mycat实现字符串按天分片的水平分表2.2.1.配置Schema配置文件2.2.2.配置Rule分片规则配置文件2.2.3.配置Server配置文件2.2.4.重启Mycat 2.3.写入数据观察分…

[每周一练][NewStarCTF 2023 公开赛道]EasyLogin

一打开是个登录界面&#xff0c;注册账号进去看了一下似乎没有什么提示。按照经验这种登录系统的一般就是sql或者爆破。先试试简单的爆破。 猜测管理员账号&#xff1a;admin,密码&#xff1a;123456。抓包看到传入的密码是被加密了的。应该是MD5加密。 爆破的话就必须用MD5的密…

短视频矩阵系统/短视频矩阵系统/自研独立框架

短视频矩阵系统/短视频矩阵系统/自研独立框架&#xff0c; 短视频综合矩阵营销管理系统,一键分发多个平台,帮助企业管理海量视频账号&#xff0c;包含抖音视频、AI混剪、矩阵导流&#xff0c;客户获取等功能。通过将视频分发、到各账号&#xff0c;提高品牌曝光率、且可以同时管…

系列直播预告:Apache Doris 2.1 新版本特性解读来袭,惊喜周边等你拿!

不久之前&#xff0c;Apache Doris 2.1.0 版本迎来正式发布&#xff0c;在盲测性能提升 100% 的同时&#xff0c;更在数据湖分析、半结构化数据分析、数据写入与更新、数据存储与负载隔离等方面推出众多核心特性&#xff0c;实时性和易用性的到全面提升。 为了让更多关注和喜爱…

transformer的学习:Attention is all you need

目录 整体概述&#xff1a;​编辑​编辑 encoder&#xff1a; embedding&#xff1a; ​编辑 self-attention&#xff1a; 向量的相似度计算&#xff1a; qkv怎么来的​编辑 softmax&#xff1a; code multi-head-attention 位置编码&#xff1a; 残差&&FFN&…

leetcode 2617. 网格图中最少访问的格子数【单调栈优化dp+二分】

原题链接&#xff1a;2617. 网格图中最少访问的格子数 题目描述&#xff1a; 给你一个下标从 0 开始的 m x n 整数矩阵 grid 。你一开始的位置在 左上角 格子 (0, 0) 。 当你在格子 (i, j) 的时候&#xff0c;你可以移动到以下格子之一&#xff1a; 满足 j < k < gri…

嵌入式开发--STM32G431RBTx-定时器中断流水灯

嵌入式开发–STM32G431RBTx-定时器中断流水灯 定时器工作原理 如图有反映stm32g431的定时器资源。 共10个定时器 定时器定时器类型个数TIM6&#xff0c;7基本定时器2TIM2&#xff0c;3&#xff0c;4全功能通用定时器3TIM15&#xff0c;16&#xff0c;17通用定时器(只有1或2个…

Linux_开发工具_yum_vim_gcc/g++_gdb_make/makefile_进度条_git_2

文章目录 一、Linux软件包管理器yum1. centos7 中安装软件方式2.安装&#xff0c;卸载&#xff0c;查看3.yum源4.安装lrzsz5.安装扩展源 二、Linux编辑器-vim1.安装vim2.vim的三种模式3.命令模式-文本批量化操作4.vim配置 三、Linux编译器-gcc/g使用1.安装2.gcc如何完成1、 预处…

SpringBoot3使用响应Result类返回的响应状态码为406

Resolved [org.springframework.web.HttpMediaTypeNotAcceptableException: No acceptable representation] 解决方法&#xff1a;Result类上加上Data注解

【算法刷题】Day33

文章目录 1. 最长湍流子数组题干&#xff1a;算法原理&#xff1a;1. 状态表示&#xff1a;2. 状态转移方程3. 初始化4. 填表顺序5. 返回值 代码&#xff1a; 2. 最长递增子序列题干&#xff1a;算法原理&#xff1a;1. 状态表示&#xff1a;2. 状态转移方程3. 初始化4. 填表顺…

链表递归-leetcode两两交换相邻链表中的结点

两两交换相邻链表中的结点 题目&#xff1a; 给定一个链表&#xff0c;两两交换其中相邻的节点&#xff0c;并返回交换后的链表。 你不能只是单纯的改变节点内部的值&#xff0c;而是需要实际的进行节点交换。 示例1 输入&#xff1a;head [1,2,3,4] 输出&#xff1a;[2,1…

文件怎么做扫码预览?创建文件活码的步骤有哪些?

现在文件可以通过扫描二维码的方式来获取&#xff0c;与传统的通过聊天软件来传输相比&#xff0c;二维码方式的应用更加的方便&#xff0c;其他人只需要通过扫描一张二维码就可以在手机上浏览或者下载文件&#xff0c;通过手机就可以预览、存储。 文件二维码的制作方法也很简…

C语言牛客网刷题

1.最大公约数和最小公倍数的组合问题 &#xff08;1&#xff09;在调试的过程中涉及到很大的数据&#xff0c;我们我们在定义变量的时候定义为long long类型 &#xff08;2&#xff09;这个里面我们自定义了max2用来求最大公约数&#xff0c;min2用来求最小公倍数 &#xff0…

稀碎从零算法笔记Day23-LeetCode:翻转二叉树

题型&#xff1a;链表、二叉树 链接&#xff1a;226. 翻转二叉树 - 力扣&#xff08;LeetCode&#xff09; 来源&#xff1a;LeetCode 题目描述 给你一棵二叉树的根节点 root &#xff0c;翻转这棵二叉树&#xff0c;并返回其根节点。 这道题适合就着样例来做 题目样例 …

sqlite3 交叉编译

#1.下载源码并解压 源码路径如下&#xff0c;下载autoconf版本 SQLite Download Page 解压 tar -zxvf sqlite-autoconf-3450200.tar.gz cd sqlite-autoconf-3450200 mkdir build # 2. 配置源代码 # 假设你已经安装了交叉编译工具链&#xff0c;如gcc-arm-linux-gnueabih…

2024年React初学者入门路线指南

在这篇文章中&#xff0c;我们一步一步探索了如何从零基础开始学习React&#xff0c;并逐渐成长为一名初级开发者。通过理解基础概念、实践构建静态和动态项目&#xff0c;最终发展到创建复杂的应用程序并加入到个人作品集中&#xff0c;您现在已经准备好迈向React开发者的职业…

Vue3:网页项目中路由的设计和配置

为了避免我每次建项目配路由的时候都回去翻网课&#xff0c;打算整一博客 路由设计 不同网页的路由设计思路基本相同&#xff0c;分为一级路由和二级路由&#xff0c;基本设计思路如下图 以我之前做过的招新系统管理端为例&#xff0c;可设计出如下路由 路由配置 还是以招新系…