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

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;}@Overridepublic boolean tryLock(long timeoutSec) {// value建议设置当前线程的idlong threadId = Thread.currentThread().getId();Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(LOCK_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);// 不要直接返回success,自充拆箱可能会出现空指针异常return BooleanUtil.isTrue(success);}@Overridepublic 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;}@Overridepublic boolean tryLock(long timeoutSec) {// value建议设置当前线程的idlong threadId = Thread.currentThread().getId();Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(LOCK_PREFIX + name, uniqueStr + threadId, timeoutSec, TimeUnit.SECONDS);// 不要直接返回success,自充拆箱可能会出现空指针异常return BooleanUtil.isTrue(success);}/*** 通过Lua脚本释放锁,保证操作的原子性*/@Overridepublic 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);
//        }
//    }
}

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

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

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

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

G - Find a way

题目分析 1.双重bfs,遍历两个起点求最短路再计算总和即可 2.唯一的坑点在于对于一个KFC&#xff0c;两人中可能有一个到不了&#xff0c;所以还要对到不了的点距离做处理 #include <bits/stdc.h> using namespace std; using ll long long; const int N 220;struct pos…

Linux/Ubuntu/Debian从控制台启动程序隐藏终端窗口

如果你想从终端运行应用程序但隐藏终端窗口. 你可以这样做&#xff1a; 在后台运行&#xff1a; 你只需在命令末尾添加一个与号 (&) 即可在后台运行它。 例如&#xff1a; your_command &将 your_command 替换为你要运行的命令。 这将在后台启动该命令&#xff0c…

Three.js基础入门介绍——【毕业季】Three.js动态相册

前言 岁月匆匆&#xff0c;又是一年毕业季&#xff0c;这次做个动态相册展示图片&#xff0c;放些有意思的内容&#xff0c;一起回忆下校园生活吧。 预期效果 相册展示和点选切换&#xff0c;利用相机旋转和移动来实现一个点击切图平滑过渡的效果。 实现流程 基本流程 1、搭…

【python】python汽车效能数据集—回归建模(源码+数据集)【独一无二】

&#x1f449;博__主&#x1f448;&#xff1a;米码收割机 &#x1f449;技__能&#x1f448;&#xff1a;C/Python语言 &#x1f449;公众号&#x1f448;&#xff1a;测试开发自动化【获取源码商业合作】 &#x1f449;荣__誉&#x1f448;&#xff1a;阿里云博客专家博主、5…

【学习】软件测试行业未来的发展趋势预测

近年来&#xff0c;随着中国数字经济的蓬勃发展&#xff0c;软件测试行业也迎来了新的春天。从早期的手工测试到自动化测试&#xff0c;再到持续集成和持续交付&#xff0c;中国的软件测试行业经历了快速的发展和变革。各行各业均对软件测试提出了更高的要求&#xff0c;尤其在…

将数据转换成xml格式的文档并下载

现在有一个实体类对象的集合&#xff0c;需要将它们转换为xml文档&#xff0c;xml文档就是标签集合的嵌套&#xff0c;例如一个学生类&#xff0c;有姓名、年龄等&#xff0c;需要转换成一下效果&#xff1a; <student><age>14</age><name>张三</na…

【Java】Oracle发布Java22最新版本

甲骨文&#xff08;ORACLE&#xff09;已经于2023年3月19日正式发布了最新版本的JDK&#xff0c;版本号&#xff1a;22 根据官方声明&#xff0c;Java 22 (Oracle JDK 22) 在性能、稳定性和安全性方面进行了数千种改进&#xff0c;包括对Java 语言、其API 和性能&#xff0c;以…

docker 哲学 - 网络桥接器、容器网络接口 、容器间的通信方式

1、解释 docker0 veth eth 2、vethXX 和 ethXX 是肯定一一对应吗 比如 eth1 对应 veth1 3、如果 A容器使用 默认创建方式 。定义他内部网络为 eth0&#xff0c;容器B使用 --network 连上 已创建的网络 172.89.2.1 。此时假设 B的 ip是 172.89.2.2 &#xff0c;容器网络接口是 e…

Godot 学习笔记(4):一切以场景为中心

文章目录 前言场景搭建新建子场景最简单的按钮事件 手动控制场景手动加载场景添加多个场景对象更快速的获取脚本对象 删除多个场景对象脚本命名的问题 总结 前言 Godot的场景是C#与Godot最后的中间连接。我们解决了场景的加载&#xff0c;我们基本可以保证C#和godot之间的彻底…

C++初阶:vector相关练习

目录 1. 只出现一次的数2. 杨辉三角3. 删除有序数组中的重复项4. 只出现一次的数II5. 只出现一次的数III6. 数组中出现次数超过一半的数7. 电话号码的字母组合&#xff08;多叉树遍历&#xff09; 1. 只出现一次的数 题目信息&#xff1a; 题目链接&#xff1a; 只出现一次的数…

工程信号的去噪和(分类、回归和时序)预测

&#x1f680;【信号去噪及预测论文代码指导】&#x1f680; 还为小论文没有思路烦恼么&#xff1f;本人专注于最前沿的信号处理与预测技术——基于信号模态分解的去噪算法和深度学习的信号&#xff08;回归、时序和分类&#xff09;预测算法&#xff0c;致力于为您提供最精确、…

ruoyi-nbcio-plus基于vue3的flowable增加开始节点的表单绑定修改

更多ruoyi-nbcio功能请看演示系统 gitee源代码地址 前后端代码&#xff1a; https://gitee.com/nbacheng/ruoyi-nbcio 演示地址&#xff1a;RuoYi-Nbcio后台管理系统 http://122.227.135.243:9666/ 更多nbcio-boot功能请看演示系统 gitee源代码地址 后端代码&#xff1a…

java.lang.String final

关于String不可变的问题&#xff1a;从毕业面试到现在&#xff0c;一个群里讨论的东西&#xff0c;反正码农面试啥都有&#xff0c;这也是我不咋喜欢面试代码&#xff0c;因为对于我而言&#xff0c;我并不喜欢这些面试。知道或不知道基本没啥含氧量&#xff0c;就是看看源代码…

蓝桥杯刷题(十三)

1.煤球数目 代码 cnt ans 0 start 1 a [] while cnt<100:ansstartstart 1t ansstartcnt1a.append(ans) print(sum(a))2.奖券数目 代码 def f(x)->bool:while x:if x%104:return Falsex//10return True ans 0 for i in range(10000,100000):if f(i):ans1 print(a…

二叉搜索树(二叉排序树)(含力扣相关题及题解)

文章目录 二叉搜索树&#xff08;二叉排序树&#xff09;1、二叉搜索树概念2、二叉搜索树的操作2.1、二叉搜索树的查找2.2、二叉搜索树的插入2.2、二叉树的删除 3、二叉搜索树的实现&#xff08;含递归版本&#xff09;4、二叉搜索树的应用4.1、K模型4.2、KV模型 5、二叉搜索树…

C语言例:设 int x; 则表达式 (x=4*5,x*5),x+25 的值

代码如下&#xff1a; #include<stdio.h> int main(void) {int x,m;m ((x4*5,x*5),x25);printf("(x4*5,x*5),x25 %d\n",m);//x4*520//x*5100//x2545return 0; } 结果如下&#xff1a;

拌合楼管理系统开发(十) 不谈技术只谈管理之大宗物资虚假贸易

前言:不谈技术只谈管理 大宗物资往往都是虚假贸易的重灾区,多年前规模就是面子的口号下,一大批国央企挖空心思做大规模,开展了一大批虚假贸易,同时为了面上的合规性,往往会有三方甚至更多方进入到整个链条中,钱货在这个链条中流转,甚至有些就是钱在流转,如果整个链条有一个环节…

电视盒子哪款好?数码小编分享电视盒子品牌排行榜

电视盒子是我们使用最多的数码产品自已&#xff0c;在挑选电视盒子这块超多朋友踩过雷&#xff0c;广告多&#xff0c;频繁卡顿&#xff0c;收费节目多&#xff0c;究竟电视盒子哪款好&#xff1f;本期小编要分享的就是目前最值得入手的电视盒子品牌排行榜&#xff0c;想买电视…

数据结构中单向链表(无头)的学习

一.数据结构 1.定义 一组用来保存一种或者多种特定关系的数据的集合&#xff08;组织和存储数据&#xff09; 程序的设计&#xff1a;将现实中大量而复杂的问题以特定的数据类型和特定的存储结构存储在内存中&#xff0c; 并在此基础上实现某个特定的功能的操…