前言
上一篇在springboot中基于自动配置集成了rabbitmq。那么回到最初的话题中就是想在秒杀下单环节增加排队机制,从而达到限流的目的。
优化秒杀下单流程
之前是在控制器里拿到客户端请求后直接入库、减库存。如果碰到羊毛党其实这套机制是不行的。并发量高的时候,库存数量也会不准确。那么引入rabbitmq则在下单时让用户信息产生一条消息入队。然后消费者处理下单(是否重复下单、下单失败、库存不够)。客户端接受到请求已入队列(response引入state处理交互)后发起ajax轮询请求,处理成功则跳转下单成功页或者结束本次交互。
1、下单(秒杀接口)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
@RequestMapping (value= "/{seckillId}/{md5}/execute" ,method = RequestMethod.POST,produces = { "application/json;charset=UTF-8" }) @ResponseBody public SeckillResult<SeckillExecution> execute( @PathVariable ( "seckillId" )Long seckillId, @PathVariable ( "md5" )String md5, @CookieValue (value= "phone" ,required= false )Long phone){ if (phone== null ){ return new SeckillResult<SeckillExecution>( false , "手机号未注册" ); } SeckillResult<SeckillExecution> result= null ; try { SeckillExecution execution=seckillService.executeSeckill(seckillId,phone,md5); result= new SeckillResult<SeckillExecution>( true ,execution); } catch (RepeatKillException e){ SeckillExecution execution= new SeckillExecution(seckillId,- 1 , "重复秒杀" ); result= new SeckillResult<SeckillExecution>( true ,execution); } catch (SeckillCloseException e){ SeckillExecution execution= new SeckillExecution(seckillId, 0 , "秒杀结束" ); result= new SeckillResult<SeckillExecution>( true ,execution); } catch (Exception e){ SeckillExecution execution= new SeckillExecution(seckillId,- 2 , "系统异常" ); result= new SeckillResult<SeckillExecution>( true ,execution); } return result; } |
2、下单业务方法(Service) 这里就要引入排队
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
@Override public SeckillExecution executeSeckill( long seckillId, long phone, String md5) throws SeckillException,RepeatKillException,SeckillCloseException { if (md5 == null || !md5.equals(getMd5(seckillId))) { throw new SeckillException( "非法请求" ); } Date now = new Date(); try { int insertCount = successKillDao.insertSuccessKilled(seckillId, phone); if (insertCount <= 0 ) { throw new RepeatKillException( "重复秒杀" ); } else { //请求入队 MiaoshaUser miaoshaUser= new MiaoshaUser(); miaoshaUser.setPhone(phone); MiaoshaMessage miaoshaMessage= new MiaoshaMessage(); miaoshaMessage.setSeckillId(seckillId); miaoshaMessage.setMiaoshaUser(miaoshaUser); String miaosha=JSON.toJSONString(miaoshaMessage); amqpTemplate.convertAndSend(miaosha); return new SeckillExecution(seckillId, 0 , "请求入队" ); /*** * 直接入库操作 int updateCount = seckillDao.reduceNumber(seckillId, now); if (updateCount <= 0) { throw new SeckillCloseException("秒杀已关闭"); } else { //秒杀成功,可以把秒杀详情和商品详情实体返回 SuccessKilled successKilled = successKillDao.queryByIdWithSeckill(seckillId, phone); return new SeckillExecution(seckillId, 1, "秒杀成功", successKilled); } ***/ } } catch (SeckillCloseException e) { throw e; } catch (RepeatKillException e1) { throw e1; } catch (SeckillException e2) { logger.error(e2.getMessage(), e2); throw new SeckillException( "Unkonwn error:" + e2.getMessage()); } } |
3、下单结果接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
@RequestMapping (value= "/{seckillId}/{md5}/result" ,method = RequestMethod.GET,produces = { "application/json;charset=UTF-8" }) @ResponseBody public SeckillResult<SeckillExecution> result( @PathVariable ( "seckillId" )Long seckillId, @PathVariable ( "md5" )String md5, @CookieValue (value= "phone" ,required= false )Long phone){ SuccessKilled successKilled = seckillService.queryByIdWithSeckill(seckillId, phone); SeckillExecution execution= null ; if (successKilled.getSeckillId()> 0 ){ execution= new SeckillExecution(seckillId, 1 , "下单成功" , successKilled); } else { execution= new SeckillExecution(seckillId, - 2 , "下单失败" , successKilled); } return new SeckillResult<SeckillExecution>( true ,execution); } |
4、消费者(下单处理)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
/** * 秒杀请求消费 **/ public class AmqpConsumer implements MessageListener { @Autowired SeckillDao seckillDao; @Autowired SuccessKillDao successKillDao; @Override public void onMessage(Message message) { Date now = new Date(); MiaoshaMessage miaosha = JSON.parseObject(message.getBody(), MiaoshaMessage. class ); Long seckillId = miaosha.getSeckillId(); int updateCount = seckillDao.reduceNumber(seckillId, now); if (updateCount <= 0 ) { System.out.println( "秒杀下单失败" ); } else { System.out.println( "秒杀下单成功" ); } } } |
5、springmvc集成消息队列配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
<?xml version= "1.0" encoding= "UTF-8" ?> <beans xmlns= "http://www.springframework.org/schema/beans" xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance" xmlns:rabbit= "http://www.springframework.org/schema/rabbit" xsi:schemaLocation="http: //www.springframework.org/schema/beans http: //www.springframework.org/schema/beans/spring-beans.xsd http: //www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd"> <!--spring集成rabbitmq--> <rabbit:connection-factory id= "connectionFactory" host= "192.168.80.128" port= "5672" username= "admin" password= "admin" channel-cache-size= "5" virtual-host= "/" /> <rabbit:admin connection-factory= "connectionFactory" /> <!--声明队列--> <rabbit:queue durable= "true" auto-delete= "false" exclusive= "false" name= "miaosha.queue" /> <!--交换器和队列绑定--> <rabbit:direct-exchange name= "miaosha.exchange" > <rabbit:bindings> <rabbit:binding queue= "miaosha.queue" key= "miaosha.tag.key" /> </rabbit:bindings> </rabbit:direct-exchange> <!--spring rabbitmqTemplate声明--> <rabbit:template id= "rabbitTemplate" exchange= "miaosha.exchange" routing-key= "miaosha.tag.key" connection-factory= "connectionFactory" /> <!--消息监听--> <bean id= "miaoshaConsumer" class = "com.seckill.mq.AmqpConsumer" /> <rabbit:listener-container connection-factory= "connectionFactory" acknowledge= "auto" > <rabbit:listener ref= "miaoshaConsumer" queues= "miaosha.queue" /> </rabbit:listener-container> </beans> |
6、客户端秒杀下单、等待下单结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
|
/**秒杀结果**/ miaosha: function (seckillId,md5,node){ $.get( '/seckill/' +seckillId+ '/' +md5+ '/result' ,{}, function (result){ if (result && result[ "success" ]){ var oData=result[ "data" ]; if (oData[ "state" ]===1){ node.html( "<span class='label label-success'>下单成功</span>" ); clearInterval(miaoshaTimer); } else { console.log( "还在排队种..." ); } } }) }, /**执行秒杀**/ seckill: function (seckillId,node){ //获取秒杀地址、控制node节点显示,执行秒杀 node.hide().html( "<button id='killBtn' class='btn btn-primary btn-lg'>开始秒杀</button>" ) $.get( '/seckill/' +seckillId+ '/exposer' ,{}, function (result){ if (result && result[ "success" ]){ //在回调函数中执行秒杀操作 var exposer=result[ "data" ]; if (exposer[ "exposed" ]){ //秒杀已开始 var md5=exposer[ "md5" ]; var killUrl= '/seckill/' +seckillId+ '/' +md5+ '/execute' ; console.log(killUrl); $( "#killBtn" ).one( 'click' , function (){ //1、禁用秒杀按钮 $( this ).addClass( 'disabled' ); //2、执行秒杀操作 $.post(killUrl,{}, function (result){ if (result && result[ "success" ]){ var killResult=result[ "data" ]; var state=killResult[ "state" ]; var stateInfo=killResult[ "stateInfo" ]; node.html( "<span class='label label-success'>" +stateInfo+ "</span>" ); if (state===0){ //已入队,客户端开始轮询 miaoshaTimer=setInterval( function (){ seckill.miaosha(seckillId,md5,node); },3000); } } }) }); node.show(); } else { //秒杀未开始, 防止浏览器和服务器出现时间差,再次执行倒数计时 var now = exposer[ 'now' ]; var start = exposer[ 'start' ]; var end = exposer[ 'end' ]; seckill.countdown(seckillId, now, start, end); } } else { console.log( 'result:' +result); //没有拿到秒杀地址 } }) } |
好了,贴了这么多代码,没有示意图怎么能行?
总结
秒杀下单增加排队机制来说对于完整的秒杀系统来说只是其中很少的一部分,这里也只是学习rabbitmq的一个过程。对于秒杀系统来说流量主要是查询多下单少。还需要引入redis,把库存量、商品信息能在秒杀开始前预处理。