优惠券秒杀案例 - CAS、Redis+Lua脚本解决高并发并行

目录

一、认识悲观锁和乐观锁? 

二、一人一单问题(优化)

三、并行执行带来的问题

3.1Redis实现分布式锁

 3.1.1 基础代码

3.1.2 保证释放的锁是自己的

3.1.3 Lua脚本保证原子性


情景介绍:

        超卖问题在我们业务中很常见,当高并发访问数据库时,可能就会出现该问题,例如有100张优惠券,在1秒内被抢光,如果不考虑线程安全问题,这时候很可能卖出去超过100张。

一、认识悲观锁和乐观锁? 

悲观锁:

  • 概念:认为线程安全问题一定会发生,所以,为每一个线程加锁,让它们串行化执行,例如java中的synchronized,lock这些都是悲观锁。
  • 优点:简单粗暴
  • 缺点:性能一般

乐观锁:

  • 概念:认为线程安全问题不一定发生,所有,当修改数据的时候,再次查询数据库,判断这个值有没有被修改过,这就是CAS锁机制。
  • 优点:性能好
  • 缺点:成功率低

为什么这里会成功率低呢?

        加入有100个线程抢50张票,100个线程同时读取到了数据库,线程1修改了数据库,那么其他99个线程都会失败。。这就出现了还有票却没卖出去的问题

改进方案:

        查询的时候不需要查询是否修改过,只查询是否库存>0即可


二、一人一单问题(优化)

 

经过测试,上面的乐观锁是一个用户下了所有的单,那么现在要求一人一单,该怎么解决呢?

解决办法:我们可以在下单之前啊,查询数据库中该用户是否下单,如果已经下单,那么直接返回,同样,这里也会遇到线程安全问题,这又该如何解决呢?

解决办法:我们还是要加锁,由于这次是判断数据库中的数据存不存在,所以不能加乐观锁了,只能加悲观锁。

public Result seckillVoucher(Long voucherId) {SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {return Result.fail("活动还未开始");}if (voucher.getEndTime().isBefore(LocalDateTime.now())) {return Result.fail("活动已经结束");}// 库存不足if(voucher.getStock() < 1){return Result.fail("库存不足");}// 注意两点// 1.释放锁时机 先提交事务,在释放锁// 2.防止事务失效Long userHolder = UserHolder.getUser().getId();synchronized (userHolder.toString().intern()){// 使用代理对象调用该函数,防止事务失效IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createOrder(voucherId);}}@Transactionalpublic Result createOrder(Long voucherId){// 一人一单Long userHolder = UserHolder.getUser().getId();int count = query().eq("user_id", userHolder).eq("voucher_id", voucherId).count();if(count > 0){return Result.fail("用户已经购买一次");}// 更新库存boolean success = iSeckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).gt("stock", 0).update();if(!success){return Result.fail("库存不足");}// 添加下单数据VoucherOrder voucherOrder = new VoucherOrder();// 全局IDlong nextId = redisIdWorker.nextId("order");voucherOrder.setId(nextId);// voucher_idvoucherOrder.setVoucherId(voucherId);// 用户idvoucherOrder.setUserId(UserHolder.getUser().getId());// 存数据save(voucherOrder);// 返回订单idreturn Result.ok(nextId);}

逻辑也是相当也复杂,其中要注意的是释放锁的时机,还有防止事务失效。


三、并行执行带来的问题

前面说的都是单体项目,也就是只有一个服务器,一个JVM,但是如果同时部署两台服务,又会出现一人两单问题,原因是每个JVM都维护自己的内存,这是synchronized锁只针对自己的那块内存有效,这就是并行问题。

分布式锁实现的三种方式

 

3.1Redis实现分布式锁

  •  获取锁
  •  获取失败不等待,直接返回结果(非阻塞)

问题1:这里要设置过期时间作为保底策略,因为一旦获取锁之后Redis宕机了,那么就永远无法操作这个业务了。

setnx lock thread1 # 普通
# Redis可能宕机
expire lock 10 # 设置过期时间作为保底策略

 问题2:这里宕机发生了过期时间也设置不上,所以也会有问题,我们直接合并两个命令

set lock thread1 EX 10 NX
  • 释放锁
del lock # 手动释放锁

下面进行代码实现,有多个版本。 

 3.1.1 基础代码

第一个版本的代码省略,直接上第二个版本的。

3.1.2 保证释放的锁是自己的

问题:上面逻辑有问题,因为如果线程1执行逻辑耗时比较长,这时候锁过期了,线程2就可以获取了,线程1执行完逻辑释放锁,把线程2的锁给释放了,这样又会导致并行问题。

解决:释放锁的时候只能释放自己的锁,(加锁标识)

public class SimpleRedisTemplate {private String name;private StringRedisTemplate stringRedisTemplate;private static final String KEY_PREFIX = "lock:";private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";public SimpleRedisTemplate(String name, StringRedisTemplate stringRedisTemplate) {this.name = name;this.stringRedisTemplate = stringRedisTemplate;}/*** 获取锁* @param timeoutSec* @return*/public boolean tryLock(Long timeoutSec){// 1.利用UUID区分不同服务的相同线程,拼接上线程IDString threadId = ID_PREFIX + Thread.currentThread().getId();Boolean b = stringRedisTemplate.opsForValue().setIfAbsent(name + KEY_PREFIX, threadId , timeoutSec, TimeUnit.MINUTES);return Boolean.TRUE.equals(b); // 防止b为null}/*** 释放锁*/public void unLock(){// 获取锁 是自己的才释放String lockId = stringRedisTemplate.opsForValue().get(name + KEY_PREFIX);String threadId = ID_PREFIX + Thread.currentThread().getId();if(threadId.equals(lockId)){stringRedisTemplate.delete(name + KEY_PREFIX);}}}
3.1.3 Lua脚本保证原子性

问题:如果释放锁时JVM正在进行垃圾回收,那么该命令也会阻塞,这样也会导致锁过期而没释放,就又会重复上面的问题,所以我们要保证释放锁这一段逻辑的原子性,我们使用Lua脚本

Lua脚本简单使用:

       此处有待补充~~因为我也不是很会

Lua脚本代码

-- 判断线程标识与锁标识是否一致
if(AVGV[1] == redis.call("get", KEYS[1])) then// 释放锁return redis.call("del", KEYS[1]);
end
return 0;

 修改释放锁逻辑

    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;// 提前加载Lua脚本static {UNLOCK_SCRIPT = new DefaultRedisScript<>();UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));UNLOCK_SCRIPT.setResultType(Long.class);}public void unLock(){// 调用Lua脚本stringRedisTemplate.execute(UNLOCK_SCRIPT,Collections.singletonList(name + KEY_PREFIX),ID_PREFIX + Thread.currentThread().getId());}

四、总结:

  • 我们首先使用了悲观锁或乐观锁解决了基本的多线程安全问题
  • 针对一人一单问题 CAS机制+悲观锁,这里注意释放锁的时机还有避免让spring中的事务失效
  • 使用Redis解决并行问题,因为JVM只维护自己的内存(synochrazied失效)
  • Lua脚本+Redis实现最终版本的加锁和释放锁的逻辑

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

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

相关文章

合并有序数组

合并有序数组 题目描述&#xff1a;解法思路&#xff1a;解法代码&#xff1a;运行结果&#xff1a; 题目描述&#xff1a; 输入两个升序排列行的序列&#xff0c;将两个序列合并为一个有序序列并输出。 输入包含三行&#xff0c;第一包含两个正整数n, m&#xff0c;用空格分隔…

来说说看到的求职路上可以提高的地方——简历

要进行求职的时候应该遇到的第一件事情就是简历。 随着看到的简历越来越多&#xff0c;也发现了一些问题&#xff0c;来开个帖子来说说这些问题。 格式 让参加面试的人最头疼的地方就是简历格式没有空格。 最近发现好多人的简历格式上都不空格&#xff0c;很多内容完全都在…

AIGC启示录:深度解析AIGC技术的现代性与系统性的奇幻旅程

✨✨ 欢迎大家来访Srlua的博文&#xff08;づ&#xffe3;3&#xffe3;&#xff09;づ╭❤&#xff5e;✨✨ &#x1f31f;&#x1f31f; 欢迎各位亲爱的读者&#xff0c;感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua小谢&#xff0c;在这里我会分享我的知识和经验。&am…

php使用ElasticSearch

ElasticSearch简介 Elasticsearch 是一个分布式的、开源的搜索分析引擎&#xff0c;支持各种数据类型&#xff0c;包括文本、数字、地理、结构化、非结构化。 Lucene与ElasticSearch Apache Lucene是一款高性能的、可扩展的信息检索&#xff08;IR&#xff09;工具库&#xf…

灯塔:CSS笔记(2)

一 选择器进阶 后代选择器&#xff1a;空格 作用&#xff1a;根据HTML标签的嵌套关系&#xff0c;&#xff0c;选择父元素 后代中满足条件的元素 选择器语法&#xff1a;选择器1 选择器2{ css } 结果&#xff1a; *在选择器1所找到标签的后代&#xff08;儿子 孙子 重孙子…

基于SSM的党务政务服务热线平台(有报告)。Javaee项目。ssm项目。

演示视频&#xff1a; 基于SSM的党务政务服务热线平台&#xff08;有报告&#xff09;。Javaee项目。ssm项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构&#xff0c;通过Spri…

mac本地启动sentinel

启动Sentinel控制台 1&#xff09;下载sentinel控制台jar包 https://github.com/alibaba/Sentinel/releases/download/1.8.6/sentinel-dashboard-1.8.6.jar 2&#xff09;启动sentinel控制台 使用如下命令启动控制台&#xff1a; java -Dserver.port8080 -Dcsp.sentinel.d…

python淘宝网页爬虫数据保存到 csv和mysql(selenium)

数据库连接设置&#xff08;表和字段要提前在数据库中建好&#xff09; # 数据库中要插入的表 MYSQL_TABLE goods# MySQL 数据库连接配置,根据自己的本地数据库修改 db_config {host: localhost,port: 3306,user: root,password: ma*****6,database: may2024,charset: utf8mb…

Flink 物理执行图

文章目录 物理执行图一、Task二、ResultPartition三、ResultSubpartition四、InputGate五、InputChannel 物理执行图 JobManager根据ExecutionGraph对作业进行调度&#xff0c;并在各个TaskManager上部署任务。这些任务在TaskManager上的实际执行过程就形成了物理执行图。物理…

Linux环境下使用线程方式操作UART读写功能

目录 概述 1 Linux环境下UART设备 2 轮询方式操作UART功能实现 2.1 打开串口函数&#xff1a;usr_serial_open 2.2 关闭串口函数&#xff1a; usr_serial_close 2.3 发送数据函数&#xff1a; usr_serial_sendbytes 2.4 接收数据函数&#xff1a; thread_uart_readbytes …

问题解决 | vscode无法连接服务器而ssh和sftp可以

解决步骤 进入家目录删除.vscode-server rm -rf .vscode-server 然后再次用vscode连接服务器时&#xff0c;会重新安装&#xff0c;这时可能报出一些缺少依赖的错 需要联系管理员安装相关依赖&#xff0c;比如 sudo apt-get install libstdc6 至此问题解决

Go语言框架路由Controller控制器设计思路gin路由根据控制器目录分层生成路由地址

Controller设计好处 框架设计用controller分请求路由层级&#xff0c;应用从app目录开始对应请求url路由地址&#xff0c;这样设计师方便开发时候通过请求地址层级快速定位接口方法对应的代码位置。 例如api接口请求路径为&#xff1a;​​http://localhost:8110/​​busines…

部署 LVS(nginx)+keepalived高可用负载均衡集群

目录 一、集群的概述 1、什么是集群 2、普通集群与负载均衡集群 2.1 普通集群&#xff08;Regular Cluster&#xff09; 2.2 负载均衡集群&#xff08;Load Balancing Cluster&#xff09; 2.3 高可用集群&#xff08;High Availability Cluster&#xff09; 2.4 区别 …

【LeetCode 算法专题突破】---二分查找(⭐⭐⭐)

前言 我在算法题目的海洋中畅游已久&#xff0c;也曾在算法竞赛中荣获佳绩。然而&#xff0c;我发现自己对于算法的学习&#xff0c;还缺乏一个系统性的总结和归类。尽管我已经涉猎过不少算法类型&#xff0c;但心中仍旧觉得有所欠缺&#xff0c;未能形成完整的算法体系。 因…

服务器又被挖矿记录

写在前面 23年11月的时候我写过一篇记录服务器被挖矿的情况&#xff0c;点我查看。当时是在桌面看到了bash进程CPU占用异常发现了服务器被挖矿。 而过了几个月没想到又被攻击&#xff0c;这次比上次攻击手段要更高明点&#xff0c;在这记录下吧。 发现过程 服务器用的是4090…

贪心算法(greedy algorithm,又称贪婪算法)详解(附例题)

目录 基本思想一&#xff09;概念二&#xff09;找出全局最优解的要求三&#xff09;求解时应考虑的问题四&#xff09;基本步骤五&#xff09;贪心策略选择六&#xff09;实际应用 1.零钱找回问题2.背包问题3.哈夫曼编码4.单源路径中的Djikstra算法5.最小生成树Prim算法 基本…

TCP包头

TCP包头: 1.序号:发送端发送数据包的编号 2.确认号:已经确认接收到的数据的编号(只有当ACK为1时,确认号才有用) TCP为什么安全可靠: 1.在通信前建立三次握手连接 SYN SYNACK ACK SYN是TCP包头的一个字段 tcp.port 端口号 抓包数据 2.在通信过程中通过序…

使用Apache Kafka的Golang实践指南

您是否在寻找构建可扩展、高性能应用程序的方法&#xff0c;这些应用程序可以实时处理流数据&#xff1f;如果是的话&#xff0c;结合使用Apache Kafka和Golang是一个很好的选择。Golang的轻量级线程非常适合编写类似Kafka生产者和消费者的并发网络应用程序。它的内置并发原语&…

探索HTTP协议:网络通信的基石

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

【CSP试题回顾】202109-2-非零段划分

CSP-202109-2-非零段划分 关键点&#xff1a;差分数组 详见&#xff1a;【CSP考点回顾】差分数组 时间复杂度分析 使用差分数组的优势在于&#xff0c;它将问题转化为了在一次遍历中识别并利用关键变化点&#xff08;波峰和波谷&#xff09;&#xff0c;从而避免了对每个可能…