您现在的位置是:网站首页> 编程资料编程资料
Spring+Redis+RabbitMQ开发限流和秒杀项目功能_Redis_
2023-05-27
593人已围观
简介 Spring+Redis+RabbitMQ开发限流和秒杀项目功能_Redis_
本文将围绕高并发场景中的限流和秒杀需求综合演示Spring Boot整合JPA、Redis缓存和RabbitMQ消息队列的做法。
本项目将通过整合Springboot和Redis以及Lua脚本来实现限流和秒杀的效果,将通过RabbitMQ消息队列来实现异步保存秒杀结果的效果。
一、项目概述
本项目将要实现的秒杀是指商家在某个时间段以非常低的价格销售商品的一种营销活动。
由于商品价格非常低,因此单位时间内发起购买商品的请求会非常多,从而会对系统造巨大的压力。对此,在一些秒杀系统中往往会整合限流的功能,同时会通过消息队列异步地保存秒杀结果。
本章将要实现的限流和秒杀功能归纳如下:
(1)通过Spring Boot的控制器类对外接收秒杀请求。
(2)针对请求进行限流操作,比如秒杀商品的数量是10个,就限定在秒杀开始后的20秒内只有100个请求能参加秒杀,该操作是通过Redis来实现的。
(3)通过限流检验的这些请求将会同时竞争若干个秒杀商品。该操作将通过基于Redis的Lua脚本来实现。
(4)为了降低数据库的压力,秒杀成功的记录将通过RabbitMQ队列以异步的方式记录到数据库中。
(5)同时,将通过RestTemple对象以多线程的方式模拟发送秒杀请求,以此来观察本秒杀系统的运行效果。
也就是说,本系统会综合用到Spring Boot、JPA、Redis和RabbitMQ,相关组件之间的关系如图所示。
二、基于Redis的Lua脚本分析
Lua使用标准C语言开发而成的,它是一种轻量级的脚本语言,可嵌入基于Redis等的应用程序中。Lua脚本可以驻留在内存中,所以具有较高的性能,适用于处理高并发的场景。
Lua脚本的特性
Lua脚本语言是由巴西一所大学的Roberto lerusalimschy 、 Waldemar Celes和 LnHenrique de Figuciredo设计而成的,它具有如下两大特性
(1)轻量性:Lua只具有一些核心和最基本的库,所以非常轻便,非常适合嵌入由其他语言编写的代码中。
(2)扩展性:Lua语言中预留了扩展接口和相关扩展机制,这样在Lua语言中就能很方便地引入其他开发语言的功能,
本章给出的秒杀场景中会向Redis服务器发送多条指令,为了降低网络调用的开销,会把相关Redis命令放在Lua脚本里。通过调用Lua脚本只需要耗费少量的网络调用代价就能执行多条Redis命令。
此外,秒杀相关的Redis语句还需要具备原子性,即这些语句要么全都执行,要么全都不执行。而Lua脚本是作为一个整体来执行的,所以可以充分地确保相关秒杀语句的原子性。
在Redis中引入Lua脚本
在启动Redis服务器以后,可以通过redis-cli命令运行lua脚本,具体步骤如下:
- 可以在
C:work\redisConf\lua
目录中创建redisCallLua.lua
文件,在其中编写Lua脚本,注意,Lua脚本文件的扩展名一般都是.lua
。 - 在第一步创建的
redisCallLua.lua
文件中加入一行代码,在其中通过redis.call
命令执行set name Peter
的命令,
redis.call('set', 'name', 'Peter')
通过rdis.call
方法在Redis中调用Lua脚本时,第一个参数是Redis命令,比如这里是set,第二个参数以及之后的参数是执行该条Redis命令的参数。
- 通过如下的
--eval
命令执行第二步定义的Lua脚本,其中C:work\redisConf\lua
是这条Lua脚本所在的路径,而redisCallLua.lua
是脚本名。
redis-cli --eval C:\work\redisConf\lua\redisCallLua.lua
上述命令运行后,得到的返回结果是空(nil),原因是该Lua脚本只是通过set命令设置了值,并没有返回结果。不过通过get name
命令就能看到通过这条Lua脚本缓存的name值,具体是Peter。
如果Lua脚本包含的语句很少,那么还可以直接用eval命令来执行该脚本,具体做法是,
先通过redis-cli语句连接到Redis服务器,随后再执行如下eval命令:
eval "redis.call('set','BookName','Spring Boot')" 0
从上述语句中能看到,在该条eval命令之后通过双引号引入了待执行的Lua脚本,在该脚本中依然是通过redis.call
语句执行Redis的set命令,进行设置缓存的操作。
在该eval命令之后还指定了Lua脚本中KEYS类型参数的个数,这里是0,表示该Lua脚本没有KEYS类型的参数。注意,这里设置的是KEYS类型的参数,而不是ARGV类型的参数,下文将详细说明这两种参数的差别。
Lua脚本的返回值和参数
在Lua脚本中,可以通过retum语句返回执行的结果,这部分对应的语法比较简单。
同时,Redis在通过eval命令执行Lua脚本时,可以传入KEYS和ARGV这两种不同类型的参数,它们的区别是,可以用KEYS参数来传入Redis命令所需要的参数,可以用ARGV参数来传入自定义的参数,通过如下两个eval执行Lua脚本的命令,可以看到这两种参数的差别。
127.0.0.1:6379> eval "return {KEYS[1],ARGV[1],ARGV[2]" 1 keyono argvone argvtwo 1) "keyone" 2) "argvone" 3) "argvtwo" 127.0.0.1:6379> eval "return {KEYS[1].ARGV[1],ARGV[2]}" 2 keyone argvone argvtwo 1) "key1" 2) "argvtwo"
在第1行eval语句中,KEYS[1]表示KEYS类型的第一个参数,而ARGV[1]和ARGV[2]对应地表示第一个和第二个ARGV类型的参数。
在第1行eval语句中,双引号之后的1表示KEYS类型的参数个数是1,所以统计参数个数时并不把ARGV自定义类型的参数统计在内,随后的keyone, argvone和argvtwo分别对应KEYS[1]、ARGV[1]和ARGV[2].
执行第一行对应的Lua脚本时,会看到如第2~4行所示的输出结果,这里输出了KEYS[1]、
ARGV[1]和ARGV[2]这3个参数对应的值。
第5行脚本和第1行的差别是,表示KEYS参数个数的值从1变成了2。但这里第2个参数是ARGV类型的,而不是KEYS类型的,所以这条Lua脚本语句会抛弃第2个参数,即ARGV[1],通过第6行和第7行的输出结果能验证这点。
所以,在通过eval命令执行Lua脚本时,一定要确保参数个数和类型的正确性。同时,这里再次提醒,eval命令之后传入的参数个数是KEYS类型参数的个数,而不是ARGV类型的。
分支语句
在Lua脚本中,可以通过if…else语句来控制代码的执行流程,具体语法如下:
if(布尔表达式) then 布尔表达式是true时执行的语句 else 布尔表达式是false时执行的语句 end
通过如下的ifDemo.lua范例,读者可以看到在Lua脚本中使用分支语句的做法。
if redis.call('exists','studentID')==1 then return 'Existed' else redis.call('set','StudentID','001'); return 'Not Existed' end
在第1行中,通过if语句判断redis.call命令执行的exists语句是否返回1,如果是,则表示StudentID键存在,就会执行第2行的returm 'Existed’语句返回Existed,否则走第3行的else流程,执行第4行和第5行的语句,设置StudentID的值,并通过retum语句返回Not Existed。
由此可以看到在Lua脚本中使用if分支语句的做法。该脚本的运行结果是:第一次运行时,由于StudentID键不存在,因此会走else流程,从而看到Not Existed的输出,而在第二次运行时,由于此时该键已经存在,因此会直接输出’Existed’的结果。
三、实现限流和秒杀功能
本节将要创建的QuickBuyDemo项目中,一方面会用到上文提到的Lua脚本实现限流和秒杀的功能,另一方面将通过RabbitMQ消息队列实现异步保存秒杀结果的功能。
创建项目并编写配置文件
可以在IDEA集成开发环境中创建名为QuickBuyDemo的Maven项目,在该项目的pom.xml文件中通过如下关键代码引入所需要的依赖包:
org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-amqp org.springframework.boot spring-boot-starter-data-redis org.apache.httpcomponents httpclient 4.5.5 org.apache.httpcomponents httpcore 4.4.10
这里通过第2-5行代码引入了SpringBoot的依赖包,通过第6-9行代码引入了RabbitMQ消息队列相关的依赖包,通过第10-13行代码引入了Redis相关的依赖包,通过第14-23行代码引入了HTTP客户端相关的依赖包,在本项目中将通过HTTP客户端模拟客户请求,从而验证秒杀效果。
在本项目resources目录的application.properties配置文件中,将通过如下代码配置消息队列和Redis缓存:
rabbitmq.host=127.0.0.1 rabbitmq.port=5672 rabbitmq.username=guest rabbitmq.password=guest redis.host=localhost redis.port=6379
在该配置文件中,通过第1~4行代码配置了RabbitMQ的连接参数,通过第5行和第6行代码配置了Redis的连接参数。
编写启动类和控制器类
本项目的启动类如下,由于和大多数的Spring Boot项目启动类完全一致,因此不再重复讲述。
package prj; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class SpringBootApp { public static void main(String[] args) { SpringApplication.run(SpringBootApp.class, args); } }
本项目的控制器类代码如下,在该Controller控制器类的第11-25行代码中封装了实现秒杀服务的quickBuy方法,该方法是以quickBuy/{item}/{person}
格式的URL请求对外提供服务的,其中item参数表示商品,而person参数则表示商品的购买人。
package prj.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import prj.receiver.BuyService; @RestController public class Controller { @Autowired private BuyService buyService; @RequestMapping("/quickBuy/{item}/{person}") public String quickBuy(@PathVariable String item, @PathVariable String person){ //20秒里限流100个请求 if(buyService.canVisit(item, 20,100)) { String result = buyService.buy(item, person); if (!result.equals("0")) { return person + " success"; } else { return person + " fail"; } } else{ return person + " fail"; } }
在quickBuy方法中,首先通过第14行的buyService.canVisit
方法对请求进行了限流操作,这里在20秒中只允许有100个请求访问,如果通过限流验证,那么会继续通过第15行的buyService.buy方法进行秒杀操作。注意,这里的实现限流和秒杀功能的代码都封装在第10行定义的BuyService类中。
消息队列的相关配置
在本项目的RabbitMQConfig类中将配置RabbitMQ的消息队列和消息交换机,具体代码如下:
package prj; import org.springframework.amqp.core.*; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class RabbitMQConfig{ //定义含主题的消息队列 @Bean public Queue objectQueue() { return new Queue("buyRecordQueue"); } //定义交换机 TopicExchange myExchange() { return new TopicExchange("myExchange"); Binding bindingObjectQueue(Queue objectQueue,TopicExchange exchange) { return BindingBuilder.bind(objectQueue).to(exchange).with("buyRecordQueue"); }
其中通过第9行的objectQueue方法创建了名为buyRecordQucue的消息队列,该消息队同将向用户传输秒杀的结果,通过第14行的myExchange方法创建了名为myExhnge的清息交换机,并通过第18行的bindingObjectQueue方法根据buyRecordQucue主题绑定了上述消息以列和消息交换机。
实现秒杀功能的Lua脚本
在本项目中,实现秒杀效果的Lua脚本代码如下:
local item = KEYS[1] local person = ARGV[1] local left = tonumber(redis.call('get',item)) if (left>=1) then redis.call ('decrby',item,1) redis.call ('rpush", 'personList',person) re
点击排行
本栏推荐
