pom文件需要多引入的jar包
<!--手动引入的hutool工具包的依赖(需要使用bloom过滤器)--> <!--这个第三方工具包的作用是用来判别有的人秒杀后重复秒杀--> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.3.9</version> </dependency> <!--手动引入fastjson的依赖--> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.78</version> </dependency>
步骤:
在消息生产者的配置类中声明一个BitMapBloomFilter的bean对象,该对象用来过滤是否由重复的用户进入秒杀
@Bean public BitMapBloomFilter bitMapBloomFilter(){ //构造器参数为位图大小(可以理解为过滤面的大小) return new BitMapBloomFilter(100); }
在消息消费者的类中声明队列+监听器
//配置队列 @Bean public Queue SpikeQueue(){ return new Queue("boot.spike.queue"); }
//消息监听器 //@RabbitListener的concurrency属性用来设置多个线程消费该队列 @RabbitListener(queues = "boot.spike.queue", concurrency = "3-5") public void handlerSpikeMsg(Message message, Channel channel){ //从队列中获取消息 --- json串 --- 拿userId和goodsId String jsonStr = new String(message.getBody()); JSONObject jsonObj = JSON.parseObject(jsonStr); Integer userId = jsonObj.getInteger("userId"); Integer goodsId = jsonObj.getInteger("goodsId"); try { //执行业务 //spikeService.spikeService(userId, goodsId); //签收消息 channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); } catch (IOException e) { throw new RuntimeException("秒杀失败...."); } }
当controller层的方法接收到秒杀请求后,使用注入的BitMapBloomFilter对象来判断。
//注入bloom过滤器 @Autowired private BitMapBloomFilter bitMapBloomFilter; //注入rabbitmq模板 @Autowired private RabbitTemplate rabbitTemplate; //注入redis模板 @Autowired private StringRedisTemplate redisTemplate; /* 处理秒杀请求的方法,请求url为/doSpike,并传参用户id userId、商品id goodsId, 向客户端响应字符串文本; */ @RequestMapping("/doSpike") public String doSpike(Integer userId, Integer goodsId){ /* 1.通过bloom过滤器判断用户是否参与过该商品的秒杀: 一个用户对一个商品只能秒杀一次 */ //通过用户id和商品id生成唯一标识 String spikeId = userId+"-"+goodsId; //判断bloom过滤器的contains()方法判断是否存在该标识,存在则返回true表示已参与秒杀,不存在则返回false表示未参与秒杀 if(bitMapBloomFilter.contains(spikeId)){ return "您已参加过该商品的抢购..."; } /* 2.操作redis判断是否还有库存,如果还有则对库存做扣减 */ //商品库存在redis中以键goods_stock:商品id,值库存数存储; //对键goods_stock:商品id的值即库存数做递减,并返回递减后的库存数; Long stock = redisTemplate.opsForValue().decrement("goods_stock:" + goodsId); if(stock<0){ return "商品已被抢完了,下次早点来哦..."; } //到此说明秒杀成功了 //3.将用户id和商品id生产的唯一标识添加到bloom过滤器 bitMapBloomFilter.add(spikeId); /* 4.将订单信息发送到rabbitmq,即将用户id和商品id发送到rabbitmq */ //将用户id和商品id存到map再转成json字符串发送给rabbitmq Map<String, Integer> map = new HashMap<>(); map.put("userId", userId); map.put("goodsId", goodsId); //使用JSON来处理需要传递的对象,将对象转换为字符串后传入消息队列 String jsonStr = JSON.toJSONString(map); //发送消息到名称为spike.queue的队列 rabbitTemplate.convertAndSend("spike.queue", jsonStr); //5.向客户端返回成功结果 return "正在拼命抢购中,请稍后去订单查看..."; }
这样,在页面发送秒杀url请求后,会来到controller层的方法中,再经过布隆过滤器判断传进来的用户是否已经参与过秒杀,如果参与过则退出方法,如果没有则将用户信息和商品信息传入消息队列中;再经过消费服务项目中的监听器监听对应的消息队列,对该队列的消息进行判断和业务处理
关于如何在项目启动时候,自动查询数据库,将数据库查询到的需要秒杀的商品注入redis中,这里采用了实现CommandLineRunner接口的方式
//实现CommandLineRunner接口并重写run(),run()方法会在boot应用启动时执行; @Component public class MysqlToRedis implements CommandLineRunner { //注入redis模板 @Autowired private StringRedisTemplate redisTemplate; //注入GoodsMapper //商品mapper接口 @Autowired private GoodsMapper goodsMapper; //boot应用启动将mysql数据同步到redis @Override public void run(String... args) throws Exception { //查询所有参数秒杀的商品 List<Goods> spikeGoods = goodsMapper.getSpikeGoods(); //将每个参与秒杀的商品,以goods_stock:商品id为键,库存量为值,存储到redis if(!CollectionUtils.isEmpty(spikeGoods)){ for (Goods goods : spikeGoods) { Integer id = goods.getGoodsid(); Integer stock = goods.getStock(); //key为"goods_stock:"+被秒杀商品的id": //value为被秒杀商品的余额 redisTemplate.opsForValue().set("goods_stock:"+id, stock.toString()); } } } }
秒杀重点:已经参与过秒杀的用户不能再进行秒杀,同时每个请求到达方法后都需要获得秒杀的商品的数量,被秒杀的商品的数量不能比0少;这里判断商品数量是使用的redis判断被秒杀的商品数量,每次业务处理前会先对redis中的商品的数量减一。