▶【SecKill】U6 接口优化
▶【SecKill】U6 接口优化
一、集成RabbitMQ
1、安装erlang(一种通用的面向并发的编程语言,可以应对大规模并发活动的编程语言和运行环境)
(1)下载Erlang安装包
https://github.com/rabbitmq/rabbitmq-server/releases/tag/v3.9.13
(2)安装包上传到服务器tmp目录下,进入到tmp目录进行安装
cd /tmp mkdir -p /usr/local/erlang tar -xzvf otp_src_24.2.1.tar.gz cd otp_src_24.2.1 ./configure --prefix=/usr/local/erlang --with-ssl --enable-threads --enable-smp-support --enable-kernel-poll --enable-hipe --without-javac make -j8 make install
(3)设置环境变量
vim /etc/profile
在末尾加入以下内容
#set erlang environment export PATH=$PATH:/usr/local/erlang/bin
(4)使环境变量生效
source /etc/profile
(5)验证是否安装成功
erl -version
安装成功!
2、安装RabbitMQ
(1)环境准备:阿里云centos8.4服务器
cat /etc/redhat-release
(2)安装RabbitMQ依赖包
yum install -y gcc gcc-c++ glibc-devel make ncurses-devel openssl-devel autoconf
(3)安装Erlang
RabbitMQ和Erlang / OTP兼容性列表
下表提供了当前支持的RabbitMQ版本系列的Erlang兼容性列表。对于已到期的RabbitMQ版本,请参阅不支持的系列兼容性列表。
下载源文件:
wget http://erlang.org/download/otp_src_23.1.tar.gz
(4)解压
tar -zxvf otp_src_23.1.tar.gz
(5)进入otp_src_23.1,安装
cd otp_src_23.1 ./otp_build autoconf ./configure && make && make install
安装成功!
(6)查看是否安装成功
erl -v
(7)安装Socat
yum install -y socat
(8)安装RabbitMQ
rpm -Uvh rabbitmq-server-3.8.9-1.el7.noarch.rpm --nodeps
(9)启动RabbitMQ
启用Rabbit MQ服务
systemctl enable rabbitmq-server
启动Rabbit MQ服务
systemctl start rabbitmq-server
查看服务状态
systemctl status rabbitmq-server
(10)Web插件安装
rabbitmq-plugins enable rabbitmq_management
(1)添加依赖
<!-- 11.添加RabbitMQ依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>
(2)添加配置信息
#rabbitmq spring.rabbitmq.host=192.168.xx.xx spring.rabbitmq.port=5672 spring.rabbitmq.username=guest spring.rabbitmq.password=guest spring.rabbitmq.virtual-host=/ spring.rabbitmq.listener.simple.concurrency= 10 spring.rabbitmq.listener.simple.max-concurrency= 10 spring.rabbitmq.listener.simple.prefetch= 1 spring.rabbitmq.listener.simple.auto-startup=true spring.rabbitmq.listener.simple.default-requeue-rejected= true spring.rabbitmq.template.retry.enabled=true spring.rabbitmq.template.retry.initial-interval=1000 spring.rabbitmq.template.retry.max-attempts=3 spring.rabbitmq.template.retry.max-interval=10000 spring.rabbitmq.template.retry.multiplier=1.0
A. Direct Exchange:任何发送到Direct Exchange的消息都会被转发到RouteKey中指定的Queue
① com.kirin.miaosha.rabbitmq / MQSender.java:消息发送器
package com.kirin.miaosha.rabbitmq;//消息发送器 @Service public class MQSender { private static Logger log = LoggerFactory.getLogger(MQSender.class); @Autowired AmqpTemplate amqpTemplate ; public void send(Object message) { String msg = RedisService.beanToString(message); log.info("send message:"+msg); amqpTemplate.convertAndSend(MQConfig.QUEUE, msg); } }
② com.kirin.miaosha.rabbitmq / MQReceiver.java:消息接受器
package com.kirin.miaosha.rabbitmq;//消息接受器 @Service public class MQReceiver { private static Logger log = LoggerFactory.getLogger(MQReceiver.class); //设置控制台输出 @Autowired RedisService redisService; @Autowired GoodsService goodsService; @Autowired MiaoshaService miaoshaService; //1.接受消息 @RabbitListener(queues=MQConfig.QUEUE) public void receive(String message) { log.info("receive message:"+message); } }
③ com.kirin.miaosha.rabbitmq / MQConfig.java:配置类
package com.kirin.miaosha.rabbitmq; @Configuration public class MQConfig { public static final String QUEUE = "queue"; @Bean public Queue queue() { return new Queue(QUEUE, true); } }
④ com.kirin.miaosha.redis / RedisService.java:
package com.kirin.miaosha.redis; @Service public class RedisService { @Autowired JedisPool jedisPool; //获取当前对象 public <T> T get(KeyPrefix prefix, String key, Class<T> clazz) { Jedis jedis = null; try { jedis = jedisPool.getResource(); String realKey = prefix.getPrefix() + key; String str = jedis.get(realKey); T t = stringToBean(str, clazz); return t; }finally { returnToPool(jedis); } } //设置对象 public <T> boolean set(KeyPrefix prefix, String key, T value) { Jedis jedis = null; try { jedis = jedisPool.getResource(); String str = beanToString(value); if(str == null || str.length() <= 0) { return false; } String realKey = prefix.getPrefix() + key; int seconds = prefix.expireSeconds(); if(seconds <= 0) { jedis.set(realKey, str); }else { jedis.setex(realKey, seconds, str); } return true; }finally { returnToPool(jedis); } } public <T> boolean exists(KeyPrefix prefix, String key) { Jedis jedis = null; try { jedis = jedisPool.getResource(); String realKey = prefix.getPrefix() + key; return jedis.exists(realKey); }finally { returnToPool(jedis); } } public <T> Long incr(KeyPrefix prefix, String key) { Jedis jedis = null; try { jedis = jedisPool.getResource(); //生成真正的key String realKey = prefix.getPrefix() + key; return jedis.incr(realKey); }finally { returnToPool(jedis); } } public <T> Long decr(KeyPrefix prefix, String key) { Jedis jedis = null; try { jedis = jedisPool.getResource(); //生成真正的key String realKey = prefix.getPrefix() + key; return jedis.decr(realKey); }finally { returnToPool(jedis); } } public boolean delete(KeyPrefix prefix, String key) { Jedis jedis = null; try { jedis = jedisPool.getResource(); //生成真正的key String realKey = prefix.getPrefix() + key; long ret = jedis.del(realKey); return ret > 0; }finally { returnToPool(jedis); } }
public static <T> String beanToString(T value) { if(value == null) { return null; } Class<?> clazz = value.getClass(); if(clazz == int.class || clazz == Integer.class) { return ""+value; }else if(clazz == String.class) { return (String)value; }else if(clazz == long.class || clazz == Long.class) { return ""+value; }else { return JSON.toJSONString(value); } }private void returnToPool(Jedis jedis) { if(jedis != null) { jedis.close(); } } }
⑤ com.kirin.miaosha.controller / DemoController.java:
package com.kirin.miaosha.controller; @Controller public class DemoController { @Autowired MQSender sender; @RequestMapping("/mq") @ResponseBody public Result <String> mq() { sender.send("hello,imooc"); return Result.success("helo,kirin"); } }
运行:
B. Topic Exchange:任何发送到Topic Exchange的消息都会被转发到与routingKey匹配的队列上
C. Fanout Exchange广播交换机:任何发送到Fanout Exchange的消息都会被转发到与之绑定的队列上
D. Headers Exchange:任何发送到Headers Exchange的消息,都会和其中存储的条件进行匹配,有whereall和whereAny的区别(全部匹配/任何匹配)
二、Redis预减库存减少数据库访问
① com.kirin.miaosha.controller / MiaoshaController.java:
package com.kirin.miaosha.controller; @Controller @RequestMapping("/miaosha") public class MiaoshaController implements InitializingBean { //实现InitialzingBean接口,重写afterProperties方法 @Autowired MiaoshaUserService userService; @Autowired RedisService redisService; @Autowired GoodsService goodsService; @Autowired OrderService orderService; @Autowired MiaoshaService miaoshaService; @Autowired MQSender sender; @Override public void afterPropertiesSet() throws Exception { List<GoodsVo> goodsList = goodsService.listGoodsVo(); if(goodsList == null) { return; } for(GoodsVo goods : goodsList) { redisService.set(GoodsKey.getMiaoshaGoodsStock, ""+goods.getId(), goods.getStockCount()); //放入缓存 } } //秒杀表单提交 @RequestMapping(value="/do_miaosha", method=RequestMethod.POST) @ResponseBody public Result<Integer> miaosha(Model model,MiaoshaUser user, @RequestParam("goodsId")long goodsId) { model.addAttribute("user", user); if(user == null) { return Result.error(CodeMsg.SESSION_ERROR); } long stock = redisService.decr(GoodsKey.getMiaoshaGoodsStock, ""+goodsId); if(stock < 0) { return Result.error(CodeMsg.MIAO_SHA_OVER); } MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId); if(order != null) { return Result.error(CodeMsg.REPEATE_MIAOSHA); } //入队 MiaoshaMessage mm = new MiaoshaMessage(); mm.setUser(user); mm.setGoodsId(goodsId); sender.sendMiaoshaMessage(mm); return Result.success(0); } /** * 前端轮询服务端 * */ @RequestMapping(value="/result", method=RequestMethod.GET) @ResponseBody public Result<Long> miaoshaResult(Model model,MiaoshaUser user,@RequestParam("goodsId")long goodsId) { model.addAttribute("user", user); if(user == null) { return Result.error(CodeMsg.SESSION_ERROR); } long result = miaoshaService.getMiaoshaResult(user.getId(), goodsId); //通过用户和订单查询是否生成订单 return Result.success(result); } }
② com.kirin.miaosha.redis / GoodsKey.java:
package com.kirin.miaosha.redis; public class GoodsKey extends BasePrefix{ private GoodsKey(int expireSeconds, String prefix) { //expireSeconds:设置有效期 super(expireSeconds, prefix); }public static GoodsKey getMiaoshaGoodsStock= new GoodsKey(0, "gs"); }
③ com.kirin.miaosha.rabbitmq / MQSender.java:
package com.kirin.miaosha.rabbitmq;//消息发送器 @Service public class MQSender { private static Logger log = LoggerFactory.getLogger(MQSender.class); //设置控制台输出 @Autowired AmqpTemplate amqpTemplate ; public void sendMiaoshaMessage(MiaoshaMessage mm) { String msg = RedisService.beanToString(mm); log.info("send message:"+msg); amqpTemplate.convertAndSend(MQConfig.MIAOSHA_QUEUE, msg); //队列名称,信息 } }
- 用SpringBoot框架提供的AmqpTemlplate实例来为我们的秒杀队列发送消息
④ com.kirin.miaosha.rabbitmq / MQReceiverpackage com.kirin.miaosha.rabbitmq;
//消息接受器 @Service public class MQReceiver { private static Logger log = LoggerFactory.getLogger(MQReceiver.class); @Autowired RedisService redisService; @Autowired GoodsService goodsService; @Autowired OrderService orderService; @Autowired MiaoshaService miaoshaService; @RabbitListener(queues=MQConfig.MIAOSHA_QUEUE) public void receive(String message) { log.info("receive message:"+message); MiaoshaMessage mm = RedisService.stringToBean(message, MiaoshaMessage.class); MiaoshaUser user = mm.getUser(); long goodsId = mm.getGoodsId(); GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId); int stock = goods.getStockCount(); if(stock <= 0) { return; } MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId); if(order != null) { return; } //减库存 下订单 写入秒杀订单 miaoshaService.miaosha(user, goods); } }
⑤ com.kirin.miaosha.service / MiaoshaService.java:
package com.kirin.miaosha.service; @Service public class MiaoshaService { @Autowired GoodsService goodsService; @Autowired OrderService orderService; @Autowired RedisService redisService; @Transactional public OrderInfo miaosha(MiaoshaUser user, GoodsVo goods) { //减库存 下订单 写入秒杀订单 boolean success = goodsService.reduceStock(goods); if(success) { return orderService.createOrder(user, goods); }else { setGoodsOver(goods.getId()); //标记商品已被秒杀完 return null; } } //通过用户和订单查询是否生成订单 public long getMiaoshaResult(Long userId, long goodsId) { MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(userId, goodsId); if(order != null) { return order.getOrderId(); }else { boolean isOver = getGoodsOver(goodsId); if(isOver) { return -1; //秒杀失败 }else { return 0; //排队中,继续轮询 } } } }
⑥ com.kirin.miaosha.service / GoodsService.java:
package com.kirin.miaosha.service; @Service public class GoodsService { @Autowired GoodsDao goodsDao; public boolean reduceStock(GoodsVo goods) { MiaoshaGoods g = new MiaoshaGoods(); g.setGoodsId(goods.getId()); int ret = goodsDao.reduceStock(g); return ret > 0; } }
⑦ com.kirin.miaosha.service / OrderService.java:
package com.kirin.miaosha.service; @Service public class OrderService { @Autowired OrderDao orderDao; @Autowired RedisService redisService; public MiaoshaOrder getMiaoshaOrderByUserIdGoodsId(long userId, long goodsId) { return redisService.get(OrderKey.getMiaoshaOrderByUidGid, ""+userId+"_"+goodsId, MiaoshaOrder.class); //查缓存 } public OrderInfo getOrderById(long orderId) { return orderDao.getOrderById(orderId); } //生成订单 @Transactional public OrderInfo createOrder(MiaoshaUser user, GoodsVo goods) { OrderInfo orderInfo = new OrderInfo(); orderInfo.setCreateDate(new Date()); orderInfo.setDeliveryAddrId(0L); orderInfo.setGoodsCount(1); orderInfo.setGoodsId(goods.getId()); orderInfo.setGoodsName(goods.getGoodsName()); orderInfo.setGoodsPrice(goods.getMiaoshaPrice()); orderInfo.setOrderChannel(1); orderInfo.setStatus(0); orderInfo.setUserId(user.getId()); orderDao.insert(orderInfo); MiaoshaOrder miaoshaOrder = new MiaoshaOrder(); miaoshaOrder.setGoodsId(goods.getId()); miaoshaOrder.setOrderId(orderInfo.getId()); miaoshaOrder.setUserId(user.getId()); orderDao.insertMiaoshaOrder(miaoshaOrder); redisService.set(OrderKey.getMiaoshaOrderByUidGid, ""+user.getId()+"_"+goods.getId(), miaoshaOrder); return orderInfo; } }
⑧ com.kirin.miaosha.redis / MiaoshaKey.java:
package com.kirin.miaosha.redis; public class MiaoshaKey extends BasePrefix{ private MiaoshaKey(String prefix) { super(prefix); } public static MiaoshaKey isGoodsOver = new MiaoshaKey("go"); }
运行:
三、内存标记减少Redis访问
MiaoshaController.java:在收到秒杀请求,在缓存中预减库存时,要访问Redis数据库,则访问Redis会产生网络的开销
com.kirin.miaosha.controller / MiaoshaController.java:
package com.kirin.miaosha.controller; @Controller @RequestMapping("/miaosha") public class MiaoshaController implements InitializingBean { @Autowired MiaoshaUserService userService; @Autowired RedisService redisService; @Autowired GoodsService goodsService; @Autowired OrderService orderService; @Autowired MiaoshaService miaoshaService; @Autowired MQSender sender; private HashMap<Long, Boolean> localOverMap = new HashMap<Long, Boolean>(); //秒杀表单提交 @RequestMapping(value="/do_miaosha", method=RequestMethod.POST) @ResponseBody public Result<Integer> miaosha(Model model,MiaoshaUser user, @RequestParam("goodsId")long goodsId) { model.addAttribute("user", user); if(user == null) { return Result.error(CodeMsg.SESSION_ERROR); } boolean over = localOverMap.get(goodsId); //判断标记商品id是否结束 if(over) { return Result.error(CodeMsg.MIAO_SHA_OVER); } long stock = redisService.decr(GoodsKey.getMiaoshaGoodsStock, ""+goodsId); //10 if(stock < 0) { localOverMap.put(goodsId, true); return Result.error(CodeMsg.MIAO_SHA_OVER); } MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId); if(order != null) { return Result.error(CodeMsg.REPEATE_MIAOSHA); } MiaoshaMessage mm = new MiaoshaMessage(); mm.setUser(user); mm.setGoodsId(goodsId); sender.sendMiaoshaMessage(mm); return Result.success(0); //0:排队中 } }
五、压测
1、JMeter打开之前的 2.miaosha.jmx
2、在com.kirin.miaosha.util / UserUtil.java,重新生成一些 tokens.txt
3、项目打jar包
4、将 miaosha_4.jar、2.miaosha.jmx 和 2.tokens.txt 导入虚拟机 /tmp 文件夹下
5、将这个进程的输出结果放进nohup.out文件中
nohup java -jar miaosha_4.jar &
启动
tail -f nohup.out
6、压测
./apache-jmeter-5.4.3/bin/jmeter.sh -n -t 2.miaosha.jmx -l result4.jtl