高并发秒杀系统架构
秒杀的方法
我们的秒杀是基于纯redis的秒杀,库存和商品都是放到redis中,然后库存使用redisson和信号量来保证原子性,用户发起秒杀请求,直接走redis进行秒杀商品,如果符合资格,就预扣减库存,并生成预创订单写入redis,然后将单号返回,然后获取一个防重token并查询商品详情,进入商品详情页,用户带着订单号点击确认下单,后端进行参数校验,然后将redis中的预创订单生成真实订单写入数据库courseOrder,并通过RocketMQ发起事务消息,通知支付服务预创支付订单,前端同时携带订单号向支付订单接口发起轮询查询订单是否存在,如果存在则返回支付订单号,用户带着支付订单号去请求支付宝接口完成支付,高并发的流量主要集中在秒杀请求,而对于进入商品页详情以及支付等接口的调用很少,因为用户没有资格秒杀商品就被拦截在外面了,根本进不到下一步
秒杀流程概述
后台在秒杀活动管理界面添加秒杀活动,设置好秒杀时间周期
后台添加秒杀课程到秒杀活动中
通过定时任务把秒杀商品发布到Redis中
秒杀走Redis秒杀,秒杀成功,把预创订单放到Redis中,订单号返回给用户
用户拿着订单号,进入确认订单页面,提交订单
用户拿着订单号去下单,订单服务去Redis获取预创订单数据,保存订单到数据库,同时保存支付单,然后删除预创订单
下单成功,用户拿着订单号去支付,支付服务根据订单号查询支付单,发起支付申请
支付结果处理和普通购买流程一致
1、电商系统架构
秒杀场景简单的来说就是一件商品的购买人数远远大于这件商品的库存,而 且这件商品在很短的时间内就会被抢购一空。比如每年的12306抢票、618、双11大促,小米新品促销等业务场景,就是典型的秒杀业务场景。
我们可以将电商系统的架构简化成下图

由图所示,我们可以简单的将电商系统的核心层分为:负载均衡层、应用层和持久层。接下来,我们就预估下每一层的并发量。
假如负载均衡层使用的是高性能的Nginx,则我们可以预估Nginx最大的并发度为:10W+。 假设应用层我们使用的是Tomcat,而Tomcat的最大并发度可以预估为800左右。 假设持久层的缓存使用的是Redis,数据库使用的是MySQL,MySQL的最大并发度可以预估为1000左右。Redis 的最大并发度可以预估为5W左右。
所以,负载均衡层、应用层和持久层各自支撑的并发度是不同的,那么,为了提升系统的总体并发度和缓存,我们通常可以采取哪些解决方案呢?
1、系统扩容
系统扩容包括垂直扩容和水平扩容,增加设备和机器配置。
2、缓存
本地缓存或者集中式缓存,减少网络IO,基于内存读取数据。
3、读写分离
采用读写分离,分而治之,增加机器的并行处理能力。
2、秒杀系统的特点
对于秒杀系统来说,我们可以从业务和技术两个角度来阐述其自身存在的一些特点。
1、秒杀系统的业务特点
这里,我们可以使用12306网站来举例,每年春运时,12306网站的访问量是非常大的,但是网站平时的访问量却是比较平缓的,也就是说,每年春运时节,12306网站的访问量会出现瞬时突增
的现象。

由图可以看出,秒杀系统的并发量存在瞬时凸峰的特点,也叫做流量突刺现象。
我们可以将秒杀系统的特点总结如下。

1、限时、限量、限价
在规定的时间内进行;秒杀活动中商品的数量有限;商品的价格会远远低于原来的价格,也就是说,在秒杀活动中,商品会以远远低于原来的价格出售。
例如,秒杀活动的时间仅限于某天上午10点到10点半,商品数量只有10万件,售完为止,而且商品的价格非常低,例如:1元购等业务场景。
限时、限量和限价可以单独存在,也可以组合存在。
2、活动预热
需要提前配置活动;活动还未开始时,用户可以查看活动的相关信息;秒杀活动开始前,对活动进行大力宣传。
3、持续时间短
购买的人数数量庞大;商品会迅速售完。
在系统流量呈现上,就会出现一个突刺现象,此时的并发访问量是非常高的,大部分秒杀场景下,商品会在极短的时间内售完。
2、秒杀系统的技术特点

1、瞬时并发量非常高
大量用户会在同一时间抢购商品;瞬间并发峰值非常高。
2、读多写少
系统中商品页的访问量巨大;商品的可购买数量非常少;库存的查询访问数量远远大于商品的购买数量。在商品页中往往会加入一些限流措施,例如早期的秒杀系统商品页会加入验证码来平滑前端对系统的访问流量,近期的秒杀系统商品详情页会在用户打开页面时,提示用户登录系统。这都是对系统的访问进行限流的一些措施。
3、流程简单
秒杀系统的业务流程一般比较简单;总体上来说,秒杀系统的业务流程可以概括为:下单减库存。
针对这种短时间内大流量的系统来说,就不太适合使用系统扩容了,因为即使系统扩容了,也就是在很短的时间内会使用到扩容后 的系统,大部分时间内,系统无需扩容即可正常访问。那么,我们可以采取哪些方案来提升系统的秒杀性能呢?
3、秒杀系统方案
1、异步解耦将整体流程进行拆解,核心流程通过队列方式进行控制。
2、限流防刷
控制网站整体流量,提高请求的门槛,避免系统资源耗尽。
3、资源控制
将整体流程中的资源调度进行控制,扬长避短。
由于应用层能够承载的并发量比缓存的并发量少很多。所以,在高并发系统中,我们可以直接使用OpenResty由负载均衡层访问缓 存,避免了调用应用层的性能损耗。大家可以到https://openresty.org/cn/来了解有关OpenResty更多的知识。同时,由于秒杀系统 中,商品数量比较少,我们也可以使用动态渲染技术,CDN技术来加速网站的访问性能。
如果在秒杀活动开始时,并发量太高时,我们可以将用户的请求放入队列中进行处理,并为用户弹出排队页面。
3、秒杀系统时序图
网上很多的秒杀系统和对秒杀系统的解决方案,并不是真正的秒杀系统,他们采用的只是同步处理请求的方案,一旦并发量真的上来了,他们所谓的秒杀系统的性能会急剧下降。我们先来看一下秒杀系统在同步下单时的时序图。
1、同步下单流程

1、用户发起秒杀请求
在同步下单流程中,首先,用户发起秒杀请求。商城服务需要依次执行如下流程来处理秒杀请求的业务。
1、识别验证码是否正确
商城服务判断用户发起秒杀请求时提交的验证码是否正确。
2、判断活动是否已经结束
验证当前秒杀活动是否已经结束。
3、验证访问请求是否处于黑名单
在电商领域中,存在着很多的恶意竞争,也就是说,其他商家可能会通过不正当手段来恶意请求秒杀系统,占用系统大量的带宽和其他系统资源。此时,就需要使用风控系统等实现黑名单机制。为了简单,也可以使用拦截器统计访问频次实现黑名单机制。
4、验证真实库存是否足够
系统需要验证商品的真实库存是否足够,是否能够支持本次秒杀活动的商品库存量。
5、扣减缓存中的库存
在秒杀业务中,往往会将商品库存等信息存放在缓存中,此时,还需要验证秒杀活动使用的商品库存是否足够,并且需要扣减秒杀活动的商品库存数量。
6、计算秒杀的价格
由于在秒杀活动中,商品的秒杀价格和商品的真实价格存在差异,所以,需要计算商品的秒杀价格。
注意:如果在秒杀场景中,系统涉及的业务更加复杂的话,会涉及更多的业务操作,这里,我只是列举出一些常见的业务操作。
2、提交订单
1、订单入口
将用户提交的订单信息保存到数据库中。
2、扣减真实库存
订单入库后,需要在商品的真实库存中将本次成功下单的商品数量扣除。
如果我们使用上述流程开发了一个秒杀系统,当用户发起秒杀请求时,由于系统每个业务流程都是串行执行的,整体上系统的性能不会太高,当并发量太高时,我们会为用户弹出下面的排队页面,来提示用户进行等待。
此时的排队时间可能是15秒,也可能是30秒,甚至是更长时间。这就存在一个问题:在用户发起秒杀请求到服务器返回结果的这段 时间内,客户端和服务器之间的连接不会被释放,这就会占大量占用服务器的资源。
网上很多介绍如何实现秒杀系统的文章都是采用的这种方式,那么,这种方式能做秒杀系统吗?答案是可以做,但是这种方式支撑的并发量并不是太高。此时,有些人可能会问:我们公司就是这样做的秒杀系统啊!上线后一直在用,没啥问题啊!我想说的是:使用同步下单方式确实可以做秒杀系统,但是同步下单的性能不会太高。之所以你们公司采用同步下单的方式做秒杀系统没出现大的问题,那是因为你们的秒杀系统的并发量没达到一定的量级,也就是说,你们的秒杀系统的并发量其实并不高。
所以,很多所谓的秒杀系统,存在着秒杀的业务,但是称不上真正的秒杀系统,原因就在于他们使用的是同步的下单流程,限制了系统的并发流量。之所以上线后没出现太大的问题,是因为系统的并发量不高,不足以压死整个系统。
以上就是同步下单的整个流程操作,如果下单流程更加复杂的话,就会涉及到更多的业务操作。
2、异步下单流程
既然同步下单流程的秒杀系统称不上真正的秒杀系统,那我们就需要采用异步的下单流程了。异步的下单流程不会限制系统的高并发流量。

1、用户发起秒杀请求
用户发起秒杀请求后,商城服务会经过如下业务流程。
检测验证码是否正确
用户发起秒杀请求时,会将验证码一同发送过来,系统会检验验证码是否有效,并且是否正确。
是否限流
系统会对用户的请求进行是否限流的判断,这里,我们可以通过判断消息队列的长度来进行判断。因为我们将用户的请求放在了消息队列中,消息队列中堆积的是用户的请求,我们可以根据当前消息队列中存在的待处理的请求数量来判断是否需要对用户的请求进行限流处理。
例如,在秒杀活动中,我们出售1000件商品,此时在消息队列中存在1000个请求,如果后续仍然有用户发起秒杀请求,则后续的请 求我们可以不再处理,直接向用户返回商品已售完的提示。
所以,使用限流后,我们可以更快的处理用户的请求和释放连接的资源。
发送MQ
用户的秒杀请求通过前面的验证后,我们就可以将用户的请求参数等信息发送到MQ中进行异步处理,同时,向用户响应结果信息。在商城服务中,会有专门的异步任务处理模块来消费消息队列中的请求,并处理后续的异步流程。
在用户发起秒杀请求时,异步下单流程比同步下单流程处理的业务操作更少,它将后续的操作通过MQ发送给异步处理模块进行处理,并迅速向用户返回响应结果,释放请求连接。
2、异步处理
我们可以将下单流程的如下操作进行异步处理。
判断活动是否已经结束 判断本次请求是否处于系统黑名单,为了防止电商领域同行的恶意竞争可以为系统增加黑名单机制,将恶意的请求放入系统的黑名单中。可以使用拦截器统计访问频次来实现。 扣减缓存中的秒杀商品的库存数量。 生成秒杀Token,这个Token是绑定当前用户和当前秒杀活动的,只有生成了秒杀Token的请求才有资格进行秒杀活动。这里我们引入了异步处理机制,在异步处理中,系统使用多少资源,分配多少线程来处理相应的任务,是可以进行控制的。
3、短轮询查询秒杀结果
这里,可以采取客户端短轮询查询是否获得秒杀资格的方案。例如,客户端可以每隔3秒钟轮询请求服务器,查询是否获得秒杀资 格,这里,我们在服务器的处理就是判断当前用户是否存在秒杀Token,如果服务器为当前用户生成了秒杀Token,则当前用户存在 秒杀资格。否则继续轮询查询,直到超时或者服务器返回商品已售完或者无秒杀资格等信息为止。
采用短轮询查询秒杀结果时,在页面上我们同样可以提示用户排队处理中,但是此时客户端会每隔几秒轮询服务器查询秒杀资格的状态,相比于同步下单流程来说,无需长时间占用请求连接。
此时,可能会有人会问:采用短轮询查询的方式,会不会存在直到超时也查询不到是否具有秒杀资格的状态呢?答案是:有可 能! 这里我们试想一下秒杀的真实场景,商家参加秒杀活动本质上不是为了赚钱,而是提升商品的销量和商家的知名度,吸引更多的用户来买自己的商品。所以,我们不必保证用户能够100%的查询到是否具有秒杀资格的状态。
4、秒杀结算
验证下单Token
客户端提交秒杀结算时,会将秒杀Token一同提交到服务器,商城服务会验证当前的秒杀Token是否有效。
加入秒杀购物车
商城服务在验证秒杀Token合法并有效后,会将用户秒杀的商品添加到秒杀购物车。
5、提交订单
订单入库
将用户提交的订单信息保存到数据库中。
删除Token
秒杀商品订单入库成功后,删除秒杀Token。
这里大家可以思考一个问题:我们为什么只在异步下单流程的粉色部分采用异步处理,而没有在其他部分采取异步削峰措施呢?
这是因为在异步下单流程的设计中,无论是在产品设计上还是在接口设计上,我们在用户发起秒杀请求阶段对用户的请求进行了限流操作,可以说,系统的限流操作是非常前置的。在用户发起秒杀请求时进行了限流,系统的高峰流量已经被平滑解决了,再往后走,其实系统的并发量和系统流量并不是非常高了。
所以,网上很多的文章和帖子中在介绍秒杀系统时,说是在下单时使用异步削峰来进行一些限流操作,那都是在扯淡! 因为下单操作在整个秒杀系统的流程中属于比较靠后的操作了,限流操作一定要前置处理,在秒杀业务后面的流程中做限流操作是没啥卵用的。
4、Redis做缓存的一些问题与解决
假设,在秒杀系统中我们使用Redis实现缓存,假设Redis的读写并发量在5万左右。我们的商城秒杀业务需要支持的并发量在100万左右。如果这100万的并发全部打入Redis中,Redis很可能就会挂掉,那么,我们如何解决这个问题呢?
在高并发的秒杀系统中,如果采用Redis缓存数据,则Redis缓存的并发处理能力是关键,因为很多的前缀操作都需要访问 Redis。而异步削峰只是基本的操作,关键还是要保证Redis的并发处理能力。
解决这个问题的关键思想就是:分而治之,将商品库存分开放。
我们在Redis中存储秒杀商品的库存数量时,可以将秒杀商品的库存进行“分割”存储来提升Redis的读写并发量。
例如,原来的秒杀商品的id为10001,库存为1000件,在Redis中的存储为(10001, 1000),我们将原有的库存分割为5份,则每份的 库存为200件,此时,我们在Redia中存储的信息为(10001_0, 200),(10001_1, 200),(10001_2, 200),(10001_3, 200),(10001_4, 200)。

此时,我们将库存进行分割后,每个分割后的库存使用商品id加上一个数字标识来存储,这样,在对存储商品库存的每个Key进行 Hash运算时,得出的Hash结果是不同的,这就说明,存储商品库存的Key有很大概率不在Redis的同一个槽位中,这就能够提升 Redis处理请求的性能和并发量。
分割库存后,我们还需要在Redis中存储一份商品id和分割库存后的Key的映射关系,此时映射关系的Key为商品的id,也就是 10001,Value为分割库存后存储库存信息的Key,也就是10001_0,10001_1,10001_2,10001_3,10001_4。在Redis中我们可 以使用List来存储这些值。
在真正处理库存信息时,我们可以先从Redis中查询出秒杀商品对应的分割库存后的所有Key,同时使用AtomicLong来记录当前的请 求数量,使用请求数量对从Redia中查询出的秒杀商品对应的分割库存后的所有Key的长度进行求模运算,得出的结果为0,1,2, 3,4。再在前面拼接上商品id就可以得出真正的库存缓存的Key。此时,就可以根据这个Key直接到Redis中获取相应的库存信息。
在高并发业务场景中,我们可以直接使用Lua脚本库(OpenResty)从负载均衡层直接访问缓存。
这里,我们思考一个场景:如果在秒杀业务场景中,秒杀的商品被瞬间抢购一空。此时,用户再发起秒杀请求时,如果系统由负载均衡层请求应用层的各个服务,再由应用层的各个服务访问缓存和数据库,其实,本质上已经没有任何意义了,因为商品已经卖完了,再通过系统的应用层进行层层校验已经没有太多意义了!!而应用层的并发访问量是以百为单位的,这又在一定程度上会降低系统的并发度。
为了解决这个问题,此时,我们可以在系统的负载均衡层取出用户发送请求时携带的用户id,商品id和秒杀活动id等信息,直接通过Lua脚本等技术来访问缓存中的库存信息。如果秒杀商品的库存小于或者等于0,则直接返回用户商品已售完的提示信息,而不用再 经过应用层的层层校验了。
引入Redisson分布式锁
<!--redisson分布式锁--><dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.13.6</version></dependency><!--整合Redis , 底层可以用jedis--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId><!--排除redis默认的java开发客户端依赖,因为高并发情况下会有内存溢出问题,我们使用jedis来操作Java--><exclusions><exclusion><groupId>io.lettuce</groupId><artifactId>lettuce-core</artifactId></exclusion></exclusions></dependency><!--引入redis的java客户端包,jedis依赖--><dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId></dependency>
package com.xx.redis.config;import org.redisson.Redisson;import org.redisson.api.RedissonClient;import org.redisson.config.Config;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;/*** 注册redisson*/@Configurationpublic class RedissonConfig {//创建客户端@Beanpublic RedissonClient redissonClient(){Config config = new Config();config.useSingleServer().setAddress("redis://127.0.0.1:6379").setPassword("root");return Redisson.create(config);}}
package com.xx.redis.config;import com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.data.redis.connection.RedisConnectionFactory;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.data.redis.serializer.StringRedisSerializer;import javax.annotation.Resource;/*** RedisJson序列化*///缓存的配置@Configurationpublic class RedisConfig {@Resourceprivate RedisConnectionFactory factory;//使用JSON进行序列化@Beanpublic RedisTemplate<Object, Object> redisTemplate() {RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();redisTemplate.setConnectionFactory(factory);//JSON格式序列化GenericFastJsonRedisSerializer serializer = new GenericFastJsonRedisSerializer();//key的序列化redisTemplate.setKeySerializer(serializer);//value的序列化redisTemplate.setValueSerializer(serializer);//hash结构key的序列化redisTemplate.setHashKeySerializer(new StringRedisSerializer());//hash结构value的序列化redisTemplate.setHashValueSerializer(serializer);return redisTemplate;}}
/*** 发布活动,需要判断是否重复发布,需要判断此活动中是否有商品,判断是否在有效时间之内*/@Overridepublic void publish(Long activityId) {AssertUtil.isNotNull(activityId, GlobalErrorCode.PARAM_IS_NULL);//获取活动信息KillActivity killActivity = selectById(activityId);//秒杀活动不存在AssertUtil.isNotNull(killActivity, GlobalErrorCode.KILL_ACTIVITY_EMPTY);//秒杀活动重复发布AssertUtil.isEqualsTrim(killActivity.getPublishStatus(),1,GlobalErrorCode.KILL_ACTIVITY_REPEAT);//查询活动下的商品Wrapper<KillCourse> wrapper = new EntityWrapper<>();wrapper.eq("activity_id",killActivity.getId());List<KillCourse> killCourses = killCourseService.selectList(wrapper);//活动下没有商品AssertUtil.isNotNull(killCourses,GlobalErrorCode.KILL_ACTIVITY_PRODUCT_EMPTY);Date date = new Date();//活动过期boolean time = date.before(killActivity.getBeginTime());AssertUtil.isTrue(time,GlobalErrorCode.KILL_ACTIVITY_EXPIRED);//发布活动,使用hash结构将活动与对应的课程缓存进redis,并设置库存的信号量killCourses.forEach(e->{//设置信号量RSemaphore semaphore = redissonClient.getSemaphore(e.getId().toString());semaphore.trySetPermits(e.getKillCount());//hash结构存入redis————Long,Map<Long,Value>String Akey = "activity"+":"+activityId;redisTemplate.opsForHash().put(Akey,e.getId().toString(),e);});killActivity.setPublishStatus(1);//修改状态为已发布killActivity.setPublishTime(new Date()); //修改发布时间updateById(killActivity);}
/*** 从redis获取秒杀商品*/@Overridepublic List<KillCourse> online() {//获取redis中所有key以activity开头的值Set<Object> keys = redisTemplate.keys("activity:*");//秒杀活动不存在AssertUtil.isNotNull(keys,GlobalErrorCode.KILL_ACTIVITY_DOES_NOT_EXIST);List<KillCourse> killCourses = new ArrayList<>();keys.forEach(k->{//从redis取出所有大key为k的值List values = redisTemplate.opsForHash().values(k);killCourses.addAll(values);});return killCourses;}
/*** 从redis获取获取一个秒杀商品的信息*/@Overridepublic KillCourse one(Long killId, Long activityId) {//先对参数进行判空AssertUtil.isNotNull(killId,GlobalErrorCode.PARAM_IS_NULL);AssertUtil.isNotNull(activityId,GlobalErrorCode.PARAM_IS_NULL);//从redis中获取商品信息String Akey = "activity"+":"+activityId;KillCourse killCourse = (KillCourse) redisTemplate.opsForHash().get(Akey, killId.toString());return killCourse;}
/*** 用户商品详情页点击立即秒杀*/@Overridepublic String kill(Long activityId, Long killId) {//1.判空AssertUtil.isNotNull(activityId,GlobalErrorCode.PARAM_IS_NULL);AssertUtil.isNotNull(killId,GlobalErrorCode.PARAM_IS_NULL);//2.查询当前用户是否秒杀过,一个用户只能秒杀一个Long loginId = 3l;String UKey = "loginId"+":"+killId;//请勿重复秒杀,只能秒杀一件商品AssertUtil.isNull(redisTemplate.opsForValue().get(UKey),GlobalErrorCode.KILL_COURSE_REPEAT);//3.查询是否存在此秒杀活动对应的课程String Akey = "activity"+":"+activityId;KillCourse killCourse = (KillCourse) redisTemplate.opsForHash().get(Akey, killId.toString());AssertUtil.isNotNull(killCourse,GlobalErrorCode.KILL_ACTIVITY_DOES_NOT_EXIST);//4.执行预扣减信号量【库存】//获取信号量RSemaphore semaphore = redissonClient.getSemaphore(killId.toString());//尝试扣减信号量boolean b = semaphore.tryAcquire(1);//扣减失败,提示商品售罄AssertUtil.isTrue(b,GlobalErrorCode.KILL_COURSE_SOLD_OUT);//5.秒杀成功,预创订单,订单号作为key值存入redis,将订单号返回//创建订单号String orderNo = CodeGenerateUtils.generateOrderSn(loginId);KillOrderRedisDto killOrderRedisDto = new KillOrderRedisDto(orderNo,1,loginId,killId,killCourse.getCourseId(),killCourse.getActivityId());redisTemplate.opsForValue().set(orderNo,killOrderRedisDto);//将用户秒杀记录存入redis,防止一个用户多次秒杀redisTemplate.opsForValue().set(UKey,killCourse);return orderNo;}
/*** 获取防重token* @param courseIds* @return JSONResult*/@Overridepublic JSONResult createToken(String courseIds) {AssertUtil.isNotNull(courseIds, GlobalErrorCode.PARAM_IS_NULL);//生成tokenString token = StrUtils.getComplexRandomString(6);//用户id暂时写死Long loginId = 3l;//存入redisString key = loginId+":"+courseIds+":"+"token"; // loginId:courseId 为keyredisTemplate.opsForValue().set(key,token);return JSONResult.success(token);}
/*** 获取秒杀订单详情*/@Overridepublic JSONResult getKillOrderInfo(Long courseId, String orderNo) {//获取预创订单KillOrderRedisDto killOrderRedisDto = (KillOrderRedisDto) redisTemplate.opsForValue().get(orderNo);AssertUtil.isNotNull(killOrderRedisDto,GlobalErrorCode.KILL_COURSE_ORDER_EMPTY);//获取redis中的秒杀课程详情KillCourse killCourse = (KillCourse) redisTemplate.opsForHash().get("activity" + ":" + killOrderRedisDto.getActivityId(), killOrderRedisDto.getKillId().toString());AssertUtil.isNotNull(killCourse,GlobalErrorCode.KILL_COURSE_ORDER_EMPTY);CourseOrder order = new CourseOrder();//封装课程信息Course course = new Course();course.setName(killCourse.getCourseName());course.setId(killCourse.getCourseId());course.setTeacherNames(killCourse.getTeacherNames());course.setPic(killCourse.getCoursePic());order.setCourse(course);//封装课程营销信息CourseMarket courseMarket = new CourseMarket();courseMarket.setPrice(killCourse.getKillPrice());order.setCourseMarket(courseMarket);//创建返回给前端数据的VOCourseOrderVo courseOrderVo = new CourseOrderVo();courseOrderVo.getCourseInfos().add(order);courseOrderVo.setTotalAmount(killCourse.getKillPrice());return JSONResult.success(courseOrderVo);}
/*** 前端下单,保存秒杀订单详情* 需要做的事:* 1.验证防重token是否正确* 2.将redis的预创订单同步到CourseOrder订单* 3.发送事务消息,创建支付订单,需要最后将支付订单返回给前端带着去下单* 4.发送延迟消息,订单超时自动关闭订单*/@Overridepublic JSONResult killPlaceOrder(KillOrderDto killOrderDto) {Long loginId = 3l; //登录用户id暂时写死//从redis中获取预创订单KillOrderRedisDto killOrderRedisDto = (KillOrderRedisDto) redisTemplate.opsForValue().get(killOrderDto.getOrderNo());//1.预创订单异常AssertUtil.isNotNull(killOrderRedisDto,GlobalErrorCode.KILL_COURSE_ORDER_EMPTY);//2.获取防重tokenString tokenKey = loginId +":"+killOrderRedisDto.getCourseId()+":"+"token";Object tokenRedis = redisTemplate.opsForValue().get(tokenKey);//防重token异常AssertUtil.isNotNull(tokenRedis,GlobalErrorCode.TOKEN_EMPTY_ERROR);//3.创建主订单List<Long> list = new ArrayList<>();list.add(killOrderRedisDto.getCourseId());//从redis中获取到秒杀课程详情KillCourse killCourse = (KillCourse) redisTemplate.opsForHash().get("activity" + ":" + killOrderRedisDto.getActivityId(), killOrderRedisDto.getKillId().toString());//判空,秒杀商品不存在AssertUtil.isNotNull(killCourse,GlobalErrorCode.KILL_ACTIVITY_DOES_NOT_EXIST);//主订单属性设置完成CourseOrder order = saveKillOrder(killOrderDto.getPayType(), killOrderRedisDto.getOrderNo(), killCourse.getKillPrice());//4.保存订单明细【包含了主订单信息和订单详情信息】CourseOrder courseOrderAndItem = saveKillDeatil(order, killOrderRedisDto.getCourseId(), killCourse.getCoursePic(), killCourse.getCourseName());//5.发送事务消息,让mq去执行本地事务,然后发送消息到mq队列,让支付服务拿到message去创建支付单Map<String, Long> map = new HashMap<>();map.put("loginId",loginId);map.put("courseIds",killCourse.getCourseId());Message<CourseOrder2PayOrder> message = MessageBuilder.withPayload(new CourseOrder2PayOrder(order.getTotalAmount(),order.getPayType(),order.getOrderNo(),order.getUserId(),order.getTitle(),JSON.toJSONString(map))).build();TransactionSendResult result = rocketMQTemplate.sendMessageInTransaction("tx-killOrder-service","topic-killorder:tag-killorder",message,courseOrderAndItem);//获取本地事务执行状态和消息发送状态LocalTransactionState localTransactionState = result.getLocalTransactionState();SendStatus sendStatus = result.getSendStatus();boolean b = localTransactionState == LocalTransactionState.COMMIT_MESSAGE || sendStatus == SendStatus.SEND_OK;AssertUtil.isTrue(b,GlobalErrorCode.CREATE_ORDER_ERROR);//消息发送成功,删除redis中的预创订单redisTemplate.delete(order);//发送一个延迟消息,如果超时未支付,则取消订单rocketMQTemplate.syncSend("topic-killOrderTimeOut:tag-killOrderTimeOut",MessageBuilder.withPayload(killOrderRedisDto.getOrderNo()).build(), //延迟消息内容,订单号3000, //超过3s未发送成功就不发了7 //延迟消息的等级,必须在3分钟之内支付,否则支付超时);return JSONResult.success(killOrderRedisDto.getOrderNo());}
package com.xx.mq;import com.xx.domain.CourseOrder;import com.xx.dto.CourseOrder2PayOrder;import com.xx.enums.GlobalErrorCode;import com.xx.service.ICourseOrderService;import com.xx.util.AssertUtil;import com.alibaba.fastjson.JSON;import com.baomidou.mybatisplus.mapper.EntityWrapper;import com.baomidou.mybatisplus.mapper.Wrapper;import org.apache.rocketmq.spring.annotation.RocketMQTransactionListener;import org.apache.rocketmq.spring.core.RocketMQLocalTransactionListener;import org.apache.rocketmq.spring.core.RocketMQLocalTransactionState;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.messaging.Message;import java.nio.charset.StandardCharsets;@RocketMQTransactionListener(txProducerGroup = "tx-killOrder-service")public class KillOrderListener implements RocketMQLocalTransactionListener {@Autowiredprivate ICourseOrderService orderService;/*** 执行本地事务* @param message* @param o* @return*/@Overridepublic RocketMQLocalTransactionState executeLocalTransaction(Message message, Object o) {try {AssertUtil.isNotNull(o, GlobalErrorCode.PARAM_IS_NULL);CourseOrder order = (CourseOrder) o;//将此对象传给courseOrder业务去保存主订单和详情orderService.saveCourseOrderAndItem(order);return RocketMQLocalTransactionState.COMMIT;} catch (Exception e) {e.printStackTrace();return RocketMQLocalTransactionState.ROLLBACK;}}/*** 回调* @param message* @return*/@Overridepublic RocketMQLocalTransactionState checkLocalTransaction(Message message) {AssertUtil.isNotNull(message,GlobalErrorCode.PARAM_IS_NULL);String s = new String((byte[]) message.getPayload(), StandardCharsets.UTF_8);CourseOrder2PayOrder courseOrder2PayOrder = JSON.parseObject(s, CourseOrder2PayOrder.class);Wrapper<CourseOrder> wrapper = new EntityWrapper<>();wrapper.eq("order_no",courseOrder2PayOrder.getOrderNo());CourseOrder order = orderService.selectOne(wrapper);if(order!=null){return RocketMQLocalTransactionState.COMMIT;}else{return RocketMQLocalTransactionState.ROLLBACK;}}}
package com.xx.mq;import com.xx.domain.CourseOrder;import com.xx.service.ICourseOrderService;import com.baomidou.mybatisplus.mapper.EntityWrapper;import com.baomidou.mybatisplus.mapper.Wrapper;import org.apache.rocketmq.common.message.MessageExt;import org.apache.rocketmq.spring.annotation.MessageModel;import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;import org.apache.rocketmq.spring.core.RocketMQListener;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import java.nio.charset.StandardCharsets;@Component@RocketMQMessageListener(consumerGroup = "killOrderTimeOut-consumer",topic = "topic-killOrderTimeOut",selectorExpression = "tag-killOrderTimeOut",messageModel = MessageModel.BROADCASTING)public class KillOrderTimeOutConsumer implements RocketMQListener<MessageExt> {@Autowiredprivate ICourseOrderService courseOrderService;@Overridepublic void onMessage(MessageExt messageExt) {if(messageExt.getBody()==null){return;}String orderNo = new String(messageExt.getBody(), StandardCharsets.UTF_8);Wrapper<CourseOrder> wrapper = new EntityWrapper<>();wrapper.eq("order_no",orderNo);CourseOrder order = courseOrderService.selectOne(wrapper);if(order==null){return;}if(order.getStatusOrder()==0){order.setStatusOrder(4);courseOrderService.updateById(order);}}}
package com.xx.mq;import com.xx.domain.PayOrder;import com.xx.dto.CourseOrder2PayOrder;import com.xx.service.IPayOrderService;import com.xx.service.IPayService;import com.alibaba.fastjson.JSON;import org.apache.rocketmq.common.message.MessageExt;import org.apache.rocketmq.spring.annotation.MessageModel;import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;import org.apache.rocketmq.spring.core.RocketMQListener;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import java.nio.charset.StandardCharsets;@Component@RocketMQMessageListener(consumerGroup = "killOrder-consumer",topic = "topic-killorder",selectorExpression = "tag-killorder",messageModel = MessageModel.BROADCASTING)public class KillOrderConsumer implements RocketMQListener<MessageExt> {@Autowiredprivate IPayService payService;@Autowiredprivate IPayOrderService payOrderService;@Overridepublic void onMessage(MessageExt messageExt) {if(messageExt.getBody()==null){return;}String string = new String(messageExt.getBody(), StandardCharsets.UTF_8);//转为我们封装的对象CourseOrder2PayOrder courseOrder2PayOrder = JSON.parseObject(string, CourseOrder2PayOrder.class);//保存支付单PayOrder payOrder = payService.selectOrderNo(courseOrder2PayOrder.getOrderNo());if(payOrder!=null){return; //保证幂等性}payOrderService.saveOrder(courseOrder2PayOrder);}}
package com.xx.mq;import com.xx.service.IPayService;import org.apache.rocketmq.common.message.MessageExt;import org.apache.rocketmq.spring.annotation.MessageModel;import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;import org.apache.rocketmq.spring.core.RocketMQListener;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import java.nio.charset.StandardCharsets;@Component@RocketMQMessageListener(consumerGroup = "killOrderTimeOut-consumer",topic = "topic-killOrderTimeOut",selectorExpression = "tag-killOrderTimeOut",messageModel = MessageModel.BROADCASTING)public class KillOrderTimeOutConsumer implements RocketMQListener<MessageExt> {@Autowiredprivate IPayService payService;@Overridepublic void onMessage(MessageExt messageExt) {if(messageExt.getBody()==null){return; //让mq别发了}//获取订单号String orderNo = new String(messageExt.getBody(), StandardCharsets.UTF_8);//取消支付订单,并且要关闭支付交易payService.cancelPayOrder(orderNo);}}
/*** 查询是否已经将MQ中的消息消费【保存支付订单,前端要根据支付订单查询是否已经创建,返回订单号* @param orderNo* @return*/@GetMapping("/checkPayOrder/{orderNo}")public JSONResult checkPayOrder(@PathVariable("orderNo") String orderNo){AssertUtil.isNotNull(orderNo, GlobalErrorCode.PARAM_IS_NULL);PayOrder payOrder = payService.selectOrderNo(orderNo);return payOrder !=null ? JSONResult.success(payOrder.getOrderNo()):JSONResult.error();}




