Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Redis实战12-优惠券实现一人一单功能

Redis实战12-优惠券实现一人一单功能

原创
作者头像
凯哥Java
发布于 2023-02-18 01:13:37
发布于 2023-02-18 01:13:37
1K0
举报
文章被收录于专栏:凯哥Java凯哥Java

本文收获

在上一篇, Redis实战11-实现优惠券秒杀下单 我们已经把超卖问题解决了。接下来,我们来开发,优惠券一人一单功能。通过本文学习,您将有如下收获:

1:悲观锁、乐观锁的使用场景;

2:synchronized关键字,在不同位置,锁的颗粒度是不同的,怎么优化呢;

3:toString方法之后,不能保证唯一,如果要保证唯一,需要在调用String的intern方法;

4:对spring事务有更深入了解-解决spring事务失效一种情况;

5:spring boot怎么开启对AspectJ的支持。

因为涉及到的知识点比较多,所以,这篇文章会比较长,但是凯哥(kaigejava)可以很负责地告诉大家,学习完本篇之后,你一定会有收获的。希望大家能耐心学完。好了,话不多少了,咱们开始学习吧~

我们来看看上一篇,解决超卖问题时候,100个优惠券领取情况:

都是被同一个用户领取了,这肯定不符合实际业务情况。

一个用户只能抢到一个优惠券的业务逻辑:

我们在原有业务中,订单入库之前,添加一人一单相关代码逻辑:

我们同样使用JMeter并发跑下试试:

设置登录状态请求头是一个用户的

我们,来看看执行结果:

异常率是95%。这不对啊,95%,意味着有10个成功的,不是一人一单吗?怎么这一人10单呢?

我们看看数据库中库存情况:

再来看看订单:

果然是10个单子。这个不符合我们实际业务情况啊。出现了一人多单的情况了。

是什么原因导致的呢?

其实和超卖情况是一样的,先查询,再判断。当多线程过来的时候,依然会出现多个线程竞争同一个资源并发安全问题。通过超卖问题,我们知道,可以通过加锁方法来解决。

那么是加乐观锁还是加悲观锁呢?

我们需要知道乐观锁和悲观锁使用的场景:

乐观锁:更新数据的时候,可以使用

悲观锁:插入数据的时候。

那么,在我们这个一人一单场景下,是用乐观锁还是用悲观锁呢?应该用悲观锁。为什么呢?因为,我们查询的是数据是否存在。而不是更新数据的。

我们还需要分析,悲观锁代码块的添加范围是什么?悲观锁代码块范围应该是,查询是否已经抢到过优惠券、扣除库存以及优惠券订单入库这些逻辑都应该被悲观锁锁管理。

所以,我们就来对相关代码做抽取后进行封装:悲观锁,我们使用synchronized关键字来加锁。

如下图:

我们将锁直接加到方法上,可以吗?我们需要知道,如果我们在方法上加锁的话,

会存在以下问题:

1:锁对象就是this.当前类对象。锁的粒度很大

2:整个方法都被锁住了。所有调用这个方法的线程,都要排队等候,前面线程释放锁之后,才可以继续操作。这就将并行强制转成串行了

3:我们其实是想处理的,同一个用户多下单情况。是同一个用户,如果张三和李四都过来抢,这种情况下,锁不应该生效才对。

根据上面的分析,我们将synchronized修改,不放到方法上。放到方法体内。锁对象也不用this。使用用户id

修改后:

我们再来分析,锁对象,userId.toString().真的能保证,不同用户锁对象是不同的,同一个用户锁对象是相同的吗?这里其实就考察了,我们对Long的toString()方法理解了。我们来看看Long对象的toString方法源码:

哦吼~~。看到什么了?竟然是new String的。我们知道,new关键字创建的对象在内存中是地址值是不一样的。我们可以写个小demo测试下:

看到结果了吗?toString后,是false。

通过上面的小demo,我们可以知道,如果我们直接使用用户id.toString()。作为锁对象的话,是会出问题的。既然使用id.toString不行,那么,我们可以考虑怎么改进。

我们知道,Java中String对象都是static fianl的,我们也知道有个常量池这个东西。String对象,在创建时候,先去常量池中获取,若存在,则直接返回常量池中相应Strnig的引用;若不存在,则会在常量池中创建一个等值的String,然后返回这个String在常量池中的引用。那么,我们可以不可以利用String这一特性来实现呢?答案是:可以的。

我们使用String.intern()方法就可以。

知识点扩展

Java的String对象中intern()方法是干嘛的?

  1. 首先明确什么是intern()方法?

String.intern()是一个Native方法,底层调用C++的 StringTable::intern方法实现。当通过语句str.intern()调用intern()方法后,JVM 就会在当前类的常量池中查找是否存在与str等值的String,若存在,则直接返回常量池中相应Strnig的引用;若不存在,则会在常量池中创建一个等值的String,然后返回这个String在常量池中的引用。

  1. intern()方法在jdk6和jdk(7/8)的区别

(1)在jdk6中,字符串常量池在永久代,调用intern()方法时,若常量池中不存在等值的字符串,JVM就会在字符串常量池中创建一个等值的字符串,然后返回该字符串的引用;

(2)在jdk7/8中,字符串常量池被移到了堆空间中,调用intern()方法时,如果常量池已经存在该字符串,则直接返回字符串引用,否则复制该堆空间中字符串对象到常量池中并返回。

根据上面分析,有了理论知识,我们还是来个小demo,测试下:

看到什么了?使用string.intern()方法后,返回的是true.这就保证了,同一个用户id,在多次进入方法后,是同一个锁对象了。所以,我们修改锁对象:

将synchronized关键字由写在方法上,修改到如上代码,锁对象变化。锁的颗粒度变小了,性能比写在方法上有很大的提升。那么上面这么写,还有问题吗?答案是:还存在问题。

还存在什么问题呢?

我们再来看看,synchronized代码块完整的代码如下图:

我们看到,方法上加了@Transactional注解,说明这个方法是在事务里面的。事务是被spring控制的,而synchronized关键字是在方法内部的。也就是说,是在事务内加锁的。这种情况下,可能会导致当前方法事务还没有提交,但是锁已经被释放掉了。因为,执行完save order后,锁的代码块就执行完了,锁就被释放了,但是事务的方法还没执行完成,事务可能还没有提交。事务没提交,根据spring事务传播机制,我们可以知道,可能还会存在问题的。线程1事务未提交,但是已经释放锁了,那么线程2就可以获取到锁,执行查询操作,因为线程1事务还未提交,就导致线程2查询数据库时候,查询count为0,就接着执行插入业务了。从而导致了一个人还是多单的情况。通过上面的分析,我们知道,是因为先释放锁,后提交事务,导致了一人多单情况。那么我们解决方案就是,可不可以先提交事务,在释放锁呢?修改后代码如下:

那么,上面代码是否存在问题呢?还是存在问题的!!存在什么问题呢?事务可能不生效。为什么呢?

我们再来看看整个秒杀抢券代码:

在调用doCreateOrder方法的时候,其实就是this.doCreateOrder().如下图:

这里的this是谁呢?就是我们当前类对象,也就是VoucherOrderServiceImpl这个对象。我们知道,spring的事务,其实是由动态代理对象来操作的。从上面的代码中,我们分析出this了,是真实的目标对象,不是代理对象。所以,事务是否会生效呢?这种情况下,会导致事务失效的。这就是spring事务失效的几种情况之一。

spring事务失效解决方案:

其实,我们在调用doCreateOrder方法的时候,不能直接用this调用,我们需要使用其代理对象来调用才可以。那么怎么获取当前对象的代理对象呢?

我们可以使用:Object proxy = AopContext.currentProxy()

修改代码:

1:在pom文件中引入aspectj

2:在启动类上添加开启对AspectJ的支持注解

3:修改我们的代码逻辑,通过代理对象来调用事务方法

代码都已经写完了。我们重启服务,然后再使用JMeter跑下,查看结果:

异常率是:99.5%,符合我们的预期。我们来看看数据库中的库存:

再来看看订单是否一条数据:

多并发报告、库存以及订单数据都符合我们的预期值,那么我们就解决了一人一单的问题。

结束语

大家好,我是凯哥Java(kaigejava),乐于分享技术文章,欢迎大家关注“凯哥Java”,及时了解更多。让我们一起学Java。也欢迎大家有事没事就来和凯哥聊聊~~~。

如操作有问题欢迎去 我的 个人博客(www#kaigejava#com)​​留言或者 微号(凯哥Java。Kaigejava或者kaigejava2022)​​​​留言交流哦。

凯哥推荐

Redis系列教程

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Redis实战11-实现优惠券秒杀下单
1:查下优惠券、2:判断是否秒杀开始;3:判断秒杀是否结束;4:判断库存是否充足;5:扣减库存;6:创建订单;
凯哥Java
2023/02/11
1.2K0
Redis实战11-实现优惠券秒杀下单
Redis实战13-集群下线程并发安全问题
通过前面两篇(Redis实战11-实现优惠券秒杀下单 Redis实战12-优惠券实现一人一单功能)的学习,我们已经解决了单机情况下优惠券秒杀及一人一单功能。其中,在优惠券秒杀时候,使用到了乐观锁处理的,在一人一单功能时候,使用了悲观锁,synchronized关键字及处理了spring事务失效情况。但是随着业务的增长,单机服务已经不能满足我们需求了,这个时候,需要多台机器来支撑。这就构成了集群,那么在集群模式下,我们再来测试我们的优惠券秒杀及一人一单情况。
凯哥Java
2023/02/25
5220
Redis实战13-集群下线程并发安全问题
【 Redis | 实战篇 秒杀实现 】
实现全局ID生成器,秒杀优惠券(基于乐观锁解决超卖问题),秒杀的一人一单(单机与集群线程安全问题)
张哈大
2025/05/31
780
【 Redis | 实战篇 秒杀实现 】
Redis解决秒杀下单
上述就是实现最基本的优惠卷下单功能。当然真实的业务场景绝对不会是向我们这么简单的。
用户11097514
2024/05/30
1920
Redis解决秒杀下单
优惠券超发问题
做商城相关的小伙伴经常会有优惠劵的需求,如果没有处理好,很容易导致优惠劵超发,超出领取一系列的问题,影响还是很大的。
adu
2022/10/30
1.1K0
优惠券超发问题
java高并发锁的3种实现
提到锁,大家可能都会想到synchronized关键字,使用它的确可以解决一切并发问题,但是对于系统吞吐要求更高的,在这里提供了几个小技巧,帮助大家减小锁粒度,提高系统并发能力。
哲洛不闹
2018/09/14
3.1K0
java高并发锁的3种实现
公司新来一个同事,把优惠券系统设计的炉火纯青!
如:A优惠券一共发行120张,每一个用户可以领取140张,当一个用户领取优惠券成功的时候,把领取的记录写入到另外一个表中(这张表我们暂且称为表B)
Java团长
2022/12/20
1.6K1
公司新来一个同事,把优惠券系统设计的炉火纯青!
【Java面试】第一章:P5级面试
答案:理论:第一章:HashMap底层实现原理,红黑树,B+树,B树的结构原理,volatile关键字,CAS(比较与交换)实现原理_廖志伟-CSDN博客_hashmap底层实现原理红黑树
全栈程序员站长
2022/07/23
14.1K0
JAVA高并发的三种实现
提到锁,大家肯定想到的是sychronized关键字。是用它可以解决一切并发问题,但是,对于系统吞吐量要求更高的话,我们这提供几个小技巧。帮助大家减小锁颗粒度,提高并发能力。
全栈程序员站长
2022/07/21
1.1K0
JAVA高并发的三种实现
北京某金融公司面试题,精选10道讲解!
面试造火箭工作拧螺丝,最近一位朋友在面试中被问到各种各样的分布式微服务的面试题,也回答上来了。可是,等正式入职后,发现这家公司居然全部是使用单体项目,完全没有分布式微服务的东东,失望至极!
田维常
2023/08/31
3090
北京某金融公司面试题,精选10道讲解!
小公司工作 6 年,后面怎么走?
对于基本数据类型(如 int, double, char 等),== 比较的是值是否相等。
沉默王二
2024/04/26
1850
小公司工作 6 年,后面怎么走?
Android 面试之必问Java基础
面向过程:面向过程性能比面向对象高。因为对象调用需要实例化,开销比较大,较消耗资源,所以当性能是最重要的考量因素的时候,比如单片机、嵌入式开发、Linux/Unix 等,一般采用面向过程开发。但是,面向过程没有面向对象易维护、易复用、易扩展。面向对象:面向对象易维护、易复用、易扩展。因为面向对象有封装、继承、多态性的特性,所以可设计出低耦合的系统,使得系统更加灵活、更加易于维护。
Rouse
2021/05/28
8840
Android 面试之必问Java基础
不是哥们?你也没说使用intern方法把字符串对象添加到字符串常量池中还有这么大的坑啊
不知道大家有没有写过“黑马点评”这个项目,这个项目中有一个功能模块是用户秒杀优惠卷。在这个过程中需要保证一个用户只能抢到一单。在这个过程中我们就需要对用户id进行加锁。
程序员牛肉
2024/12/19
1700
不是哥们?你也没说使用intern方法把字符串对象添加到字符串常量池中还有这么大的坑啊
Java对象的创建过程
虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
葆宁
2019/04/18
9680
Java对象的创建过程
Synchronized锁在Spring事务管理下,为啥还线程不安全?
简单来说:多线程跑一个使用synchronized关键字修饰的方法,方法内操作的是数据库,按正常逻辑应该最终的值是1000,但经过多次测试,结果是低于1000。这是为什么呢?
Java3y
2019/03/07
7580
Synchronized锁在Spring事务管理下,为啥还线程不安全?
java面试知识点
1、static不能修饰局部变量。因为static修饰的变量属于类所有但是如果修饰局部变量则和static意义相反
用户9131103
2023/07/17
1620
java面试知识点
浅谈分布式锁
分布式锁就是在分布式系统中,为解决共享资源排他性式访问而设定的锁。用于解决分布式系统中操作共享资源数据一致性问题。
灬沙师弟
2023/09/06
3020
浅谈分布式锁
微服务架构之:Redis分布式锁
在单体架构上,乐观锁和悲观锁可以锁住并发情况下的同步代码块,我们多使用synchronized来对方法加锁。但是在配上负载均衡的集群模式下, 普通的synchronized是无法锁住从两台服务器同时进入的请求 。
用户6256742
2024/07/15
1360
微服务架构之:Redis分布式锁
Java 面试题全记录 多处搜集 灵魂拷问 持续更新
大佬总结的面试题纲: https://github.com/farmerjohngit/myblog/issues/21
heasy3
2020/08/02
7730
Java基础系列2:深入理解String类
String是Java中最为常用的数据类型之一,也是面试中比较常被问到的基础知识点,本篇就聊聊Java中的String。主要包括如下的五个内容:
王金龙
2020/02/14
6650
相关推荐
Redis实战11-实现优惠券秒杀下单
更多 >
加入讨论
的问答专区 >
1KOL擅长5个领域
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档