一、秒杀商品的页面展示:

1.1 秒杀列表页面数据的展示:

1.1.1 新建一个zyg-seckill-web工程:

pom.xml文件:

  1. <dependencies>
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-web</artifactId>
  5. </dependency>
  6. <!--1.添加thymeleaf依赖-->
  7. <dependency>
  8. <groupId>org.springframework.boot</groupId>
  9. <artifactId>spring-boot-starter-thymeleaf</artifactId>
  10. </dependency>
  11. <!--2.添加devtools-->
  12. <dependency>
  13. <groupId>org.springframework.boot</groupId>
  14. <artifactId>spring-boot-devtools</artifactId>
  15. <optional>true</optional>
  16. </dependency>
  17. <dependency>
  18. <groupId>com.zelin</groupId>
  19. <artifactId>zyg-dao</artifactId>
  20. <version>2.0</version>
  21. </dependency>
  22. <!--1. 引入cas单点登录的依赖-->
  23. <dependency>
  24. <groupId>org.springframework.boot</groupId>
  25. <artifactId>spring-boot-starter-security</artifactId>
  26. </dependency>
  27. <dependency>
  28. <groupId>org.springframework.security</groupId>
  29. <artifactId>spring-security-cas</artifactId>
  30. </dependency>
  31. <dependency>
  32. <groupId>com.zelin</groupId>
  33. <artifactId>zyg-seckill-interface</artifactId>
  34. <version>2.0</version>
  35. </dependency>
  36. <!--引入支付服务-->
  37. <dependency>
  38. <groupId>com.zelin</groupId>
  39. <artifactId>zyg-pay-interface</artifactId>
  40. <version>2.0</version>
  41. </dependency>
  42. </dependencies>
  43. </project>

application.properties文件:

  1. server.port=9009
  2. logging.level.com.zelin=debug
  3. spring.thymeleaf.cache=false
  4. spring.dubbo.registry.address=zookeeper://192.168.56.10:2181
  5. spring.dubbo.base-package=com.zelin.seckill.web
  6. spring.dubbo.application.name=zyg-seckill-web
  7. cas.server.prefix=http://cas.zeyigou.com:8080/cas
  8. cas.server.login=${cas.server.prefix}/login
  9. cas.server.logout=${cas.server.prefix}/logout
  10. cas.client.prefix=http://seckill.zeyigou.com
  11. cas.client.login=${cas.client.prefix}/login/cas
  12. cas.client.logoutRelative=/logout/cas
  13. cas.client.logout=${cas.client.prefix}${cas.client.logoutRelative}

config目录下关于cas的配置与zyg-cart-web工程。

1.1.2 在nginx服务器下配置关于seckill-web的服务器映射:

/mydata/nginx/conf/conf.d/zeyigou.conf

  1. server {
  2. listen 80;
  3. server_name seckill.zeyigou.com;
  4. location / {
  5. proxy_pass http://seckill;
  6. proxy_set_header Host $host:$server_port;
  7. }
  8. include /etc/nginx/static.conf;
  9. }

/mydata/nginx/conf/nginx.conf文件:

  1. upstream seckill{
  2. server 192.168.56.1:9009;
  3. }

1.1.3 在zyg-seckill-service下定义关于查询秒杀商品信息的方法:

  1. /**
  2. * 功能: 根据商品的时间标识得到商品列表
  3. * 参数:
  4. * 返回值: java.util.List<com.zelin.entity.SeckillGoodsEntity>
  5. * 时间: 2021/8/19 12:40
  6. */
  7. @Override
  8. public List<SeckillGoodsEntity> findSeckillGoods() {
  9. BoundHashOperations<String, String, String> hashOperations = redisTemplate.boundHashOps("SeckillGoods_2021081914" );
  10. List<String> values = hashOperations.values();
  11. System.out.println("values = " + values);
  12. List<SeckillGoodsEntity> collect = values.stream().map(m -> {
  13. SeckillGoodsEntity seckillGoodsEntity = JSON.parseObject(m, SeckillGoodsEntity.class);
  14. return seckillGoodsEntity;
  15. }).collect(Collectors.toList());
  16. System.out.println("collect = " + collect);
  17. return collect;
  18. }

1.1.4 在zyg-seckill-service下定义关于查询菜单的方法:

  1. /**
  2. * 功能: 展示菜单
  3. * 参数:
  4. * 返回值: java.util.List<java.sql.Date>
  5. * 时间: 2021/8/19 13:13
  6. */
  7. @Override
  8. public List<Date> listMenus() {
  9. return MyDate.getDateMenus();
  10. }

1.1.5 在zyg-seckill-web中的控制器下定义查看商品的方法:

  1. /**
  2. * 功能: 根据当前时间列表秒杀商品
  3. * 参数:
  4. * 返回值: java.lang.String
  5. * 时间: 2021/8/19 12:08
  6. */
  7. @RequestMapping({"/","seckill-index"})
  8. public String secKillList(Model model){
  9. //1. 得到所有商品(指定日期空间下的某些商品)
  10. List<SeckillGoodsEntity> seckillGoodsEntities = secKillService.findSeckillGoods();
  11. //2. 得到以当前时间小时数为起点的往向5个时间范围
  12. List<Date> menus = secKillService.listMenus();
  13. //3. 将上面的两组数据放到model中
  14. model.addAttribute("seckillGoods",seckillGoodsEntities);
  15. model.addAttribute("menus",menus);
  16. return "seckill-index";
  17. }

1.1.6 在zyg-seckill-web中的控制器下定义查看菜单的方法:

  1. /**
  2. * 功能: 展示菜单
  3. * 参数:
  4. * 返回值:
  5. * 时间: 2021/8/19 13:11
  6. */
  7. @RequestMapping("menus")
  8. public String listMenus(Model model){
  9. List<Date> menus = secKillService.listMenus();
  10. model.addAttribute("menus",menus);
  11. return "seckill-index";
  12. }

1.1.7 在templates/seckill-index.html页面展示商品:

  1. <div class="py-container index">
  2. <!--banner-->
  3. <div class="banner">
  4. <img src="/static/seckill/img/_/banner.png" class="img-responsive" alt="">
  5. </div>
  6. <!--秒杀时间-->
  7. <div class="sectime">
  8. <div th:class="${stat.index==0}? 'item-time active':'item-time' "
  9. th:attr="time=${#dates.hour(menu)}" th:each="menu,stat : ${menus}">
  10. <div class="time-clock" th:text="${#dates.hour(menu)}"></div>
  11. <div class="time-state-on">
  12. <span class="on-text">快抢中</span>
  13. <span class="on-over">距离结束:01:02:34</span>
  14. </div>
  15. </div>
  16. </div>
  17. <!--商品列表-->
  18. <div class="goods-list" id="app">
  19. <ul class="seckill" id="seckill">
  20. <li class="seckill-item" th:each="item : ${seckillGoods}" >
  21. <div class="pic">
  22. <img th:src="${item.smallPic}" alt=''>
  23. </div>
  24. <div class="intro">
  25. <span>[[${item.introduction}]]</span>
  26. </div>
  27. <div class='price'>
  28. <b class='sec-price'>¥[[${item.costPrice}]]</b>
  29. <b class='ever-price'>¥[[${item.price}]]</b>
  30. </div>
  31. <div class='num'>
  32. <div>已售[[${item.num-item.stockCount}]]</div>
  33. <div class='progress'>
  34. <div class='sui-progress progress-danger'>
  35. <span th:style="'width:'+ ${(item.num-item.stockCount)* 100/item.num} + '%'"
  36. class='bar'></span>
  37. </div>
  38. </div>
  39. <div>剩余
  40. <b class='owned'>[[${item.stockCount}]]</b></div>
  41. </div>
  42. <a class='sui-btn btn-block btn-buy' th:href="'/findSeckillGoods?seckillId=' + ${item.id}" target='_blank'>立即抢购</a>
  43. </li>
  44. </ul>
  45. </div>
  46. <div class="cd-top">
  47. <div class="top">
  48. <img src="/static/seckill/img/_/gotop.png" />
  49. <b>TOP</b>
  50. </div>
  51. <div class="code" id="code">
  52. <span>
  53. <img src="/static/seckill/img/_/code.png" />
  54. </span>
  55. </div>
  56. <div class="erweima">
  57. <img src="/static/seckill/img/_/erweima.jpg" alt="">
  58. <s></s>
  59. </div>
  60. </div>
  61. </div>

1.1.8 页面效果如下:

image.png

1.2 秒杀详情页面数据的展示:

1.2.1 在秒杀列表中中的”立即抢购”按钮下添加跳转:

  1. <a class='sui-btn btn-block btn-buy' th:href="'/findSeckillGoods?seckillId=' + ${item.id}"
  2. target='_blank'>立即抢购</a>

1.2.2 在zyg-seckill-web中添加findSeckillGoods方法:

  1. /**
  2. * 功能: 根据秒杀商品id查询秒杀商品
  3. * 参数:
  4. * 返回值:
  5. * 时间: 2021/8/19 14:49
  6. */
  7. @RequestMapping("findSeckillGoods")
  8. public String findSeckillGoods(String seckillId,Model model){
  9. //1. 在redis中根据secKillId查询秒杀商品
  10. SeckillGoodsEntity seckillGoodsEntity = secKillService.findSecKillGoodsById(seckillId);
  11. //2. 放到model 中
  12. model.addAttribute("seckillGoods",seckillGoodsEntity);
  13. //3. 返回到seckill-item.html
  14. return "seckill-item";
  15. }

1.2.3 在zyg-seckill-service中通过redis中进行查询

  1. /**
  2. * 功能: 根据秒杀商品id查询商品
  3. * 参数:
  4. * 返回值: com.zelin.entity.SeckillGoodsEntity
  5. * 时间: 2021/8/19 14:52
  6. */
  7. @Override
  8. public SeckillGoodsEntity findSecKillGoodsById(String seckillId) {
  9. String seckillGoods = (String) redisTemplate.boundHashOps("SeckillGoods_2021081914").get(seckillId);
  10. return JSON.parseObject(seckillGoods,SeckillGoodsEntity.class);
  11. }

1.2.4 页面展示:

  1. <div class="fr itemInfo-wrap">
  2. <div class="sku-name">
  3. <h4>[[${seckillGoods.title}]]</h4>
  4. </div>
  5. <div class="news">
  6. <span><img src="/static/seckill/img/_/clock.png"/>青橙秒杀</span>
  7. <span class="overtime">距离结束:01:56:78</span>
  8. </div>
  9. <div class="summary">
  10. <div class="summary-wrap">
  11. <div class="fl title">
  12. <i>价  格</i>
  13. </div>
  14. <div class="fl price">
  15. <i>¥</i>
  16. <em>[[${seckillGoods.costPrice}]]</em>
  17. <span>降价通知</span>
  18. </div>
  19. <div class="fr remark">
  20. <i>累计评价</i><em>612188</em>
  21. </div>
  22. </div>
  23. <div class="summary-wrap">
  24. <div class="fl title">
  25. <i>促  销</i>
  26. </div>
  27. <div class="fl fix-width">
  28. <i class="red-bg">加价购</i>
  29. <em class="t-gray">满999.00另加20.00元,或满1999.00另加30.00元,或满2999.00另加40.00元,即可在购物车换购热销商品</em>
  30. </div>
  31. </div>
  32. </div>
  33. <div class="support">
  34. <div class="summary-wrap">
  35. <div class="fl title">
  36. <i>支  持</i>
  37. </div>
  38. <div class="fl fix-width">
  39. <em class="t-gray">以旧换新,闲置手机回收 4G套餐超值抢 礼品购</em>
  40. </div>
  41. </div>
  42. <div class="summary-wrap">
  43. <div class="fl title">
  44. <i>配 送 至</i>
  45. </div>
  46. <div class="fl fix-width">
  47. <em class="t-gray">满999.00另加20.00元,或满1999.00另加30.00元,或满2999.00另加40.00元,即可在购物车换购热销商品</em>
  48. </div>
  49. </div>
  50. </div>
  51. <div class="clearfix choose">
  52. <div id="specification" class="summary-wrap clearfix">
  53. <dl>
  54. <dt>
  55. <div class="fl title">
  56. <i>选择颜色</i>
  57. </div>
  58. </dt>
  59. <dd><a href="javascript:;" class="selected">金色<span title="点击取消选择">&nbsp;</span>
  60. </a></dd>
  61. <dd><a href="javascript:;">银色</a></dd>
  62. <dd><a href="javascript:;">黑色</a></dd>
  63. </dl>
  64. <dl>
  65. <dt>
  66. <div class="fl title">
  67. <i>内存容量</i>
  68. </div>
  69. </dt>
  70. <dd><a href="javascript:;" class="selected">16G<span title="点击取消选择">&nbsp;</span>
  71. </a></dd>
  72. <dd><a href="javascript:;">64G</a></dd>
  73. <dd><a href="javascript:;" class="locked">128G</a></dd>
  74. </dl>
  75. <dl>
  76. <dt>
  77. <div class="fl title">
  78. <i>选择版本</i>
  79. </div>
  80. </dt>
  81. <dd><a href="javascript:;" class="selected">公开版<span title="点击取消选择">&nbsp;</span>
  82. </a></dd>
  83. <dd><a href="javascript:;">移动版</a></dd>
  84. </dl>
  85. <dl>
  86. <dt>
  87. <div class="fl title">
  88. <i>购买方式</i>
  89. </div>
  90. </dt>
  91. <dd><a href="javascript:;" class="selected">官方标配<span title="点击取消选择">&nbsp;</span>
  92. </a></dd>
  93. <dd><a href="javascript:;">移动优惠版</a></dd>
  94. <dd><a href="javascript:;" class="locked">电信优惠版</a></dd>
  95. </dl>
  96. <dl>
  97. <dt>
  98. <div class="fl title">
  99. <i>套  装</i>
  100. </div>
  101. </dt>
  102. <dd><a href="javascript:;" class="selected">保护套装<span title="点击取消选择">&nbsp;</span>
  103. </a></dd>
  104. <dd><a href="javascript:;" class="locked">充电套装</a></dd>
  105. </dl>
  106. </div>
  107. <div class="summary-wrap">
  108. <div class="fl title">
  109. <div class="control-group">
  110. <div class="controls">
  111. <input autocomplete="off" type="text" value="1" minnum="1" class="itxt" />
  112. <a href="javascript:void(0)" class="increment plus">+</a>
  113. <a href="javascript:void(0)" class="increment mins">-</a>
  114. </div>
  115. </div>
  116. </div>
  117. <div class="fl">
  118. <ul class="btn-choose unstyled">
  119. <li>
  120. <a href="cart.html" target="_blank" class="addshopcar">抢单</a>
  121. </li>
  122. </ul>
  123. </div>
  124. </div>
  125. </div>
  126. </div>

1.2.5 页面效果:

image.png

二、秒杀功能实现:

2.1 用户下单功能细节:

1、细节功能,添加排队信息(SeckillStatus)

  1. 在秒杀模块实现过程中,为了公平起见,先来的可以先下单 ,我们可以将用户的排队信息定义在一个实体类,并把这个实体类放到redis中的队列中(redislist数据类型)<br />**1.1 SeckillStatus类型定义**
  1. /**
  2. * ------------------------------
  3. * 功能:
  4. * 作者:WF
  5. * 微信:hbxfwf13590332912
  6. * 创建时间:2021/8/19-17:01
  7. * ------------------------------
  8. */
  9. @Data
  10. public class SeckillStatus implements Serializable {
  11. //秒杀用户名
  12. private String username;
  13. //创建时间
  14. private Date createTime;
  15. //秒杀状态 1:排队中,2:秒杀等待支付,3:支付超时,4:秒杀失败,5:支付完成
  16. private Integer status;
  17. //秒杀的商品ID
  18. private Long secKillId;
  19. //应付金额
  20. private Float money;
  21. //订单号
  22. private Long orderId;
  23. //时间段
  24. private String time;
  25. public SeckillStatus(String username, Date createTime, Integer status, Long secKillId, String time) {
  26. this.username = username;
  27. this.createTime = createTime;
  28. this.status = status;
  29. this.secKillId = secKillId;
  30. this.time = time;
  31. }
  32. }

1.2 在下单前,应该把排队的信息(SeckillStatus对象)存放到redis中的list中:

  1. @Override
  2. public boolean save(String secKillId, String time, String username) {
  3. ...
  4. // 将此对象入队列,目的是为了公平原则(先排队先下单)
  5. redisTemplate.boundListOps("SeckillOrderQueue").leftPush(JSON.toJSONString(seckillStatus));
  6. ...
  7. }

1.3 在下单前,从redis中取得从左边放入的队列数据(先排队的就先弹出)

  1. public void createOrder() {
  2. ...
  3. String orderQueue = redisTemplate.boundListOps("SeckillOrderQueue").rightPop();
  4. //转换为 SeckillStatus类型,取得排队时放入的信息
  5. SeckillStatus seckillStatus = JSON.parseObject(orderQueue, SeckillStatus.class);
  6. String time = seckillStatus.getTime();
  7. String secKillId = seckillStatus.getSecKillId()+"";
  8. String username = seckillStatus.getUsername();
  9. ...
  10. }

原理图:

1、秒杀-排队顺序问题.jpg

2、细节功能:添加排队标识

将用户排队信息写入到redis中,方便后面更新用户的排队状态。
2.1 存用户排队标识的key (用于存储 谁 买了什么商品 以及抢单的状态)

  1. @Override
  2. public boolean save(String secKillId, String time, String username) {
  3. ...
  4. // 为了后面查询订单状态,我们将上面的seckillStatus又保存一份到redis中(因上面的会被立即弹出)
  5. redisTemplate.boundHashOps("UserQueueStatus").put(username,JSON.toJSONString(seckillStatus));
  6. //异步下单
  7. multiThreadingCreateOrder.createOrder();
  8. return true;
  9. }

2.2 在下单完成后,修改排队信息:

  1. public void createOrder() {
  2. ...
  3. //重新改回状态值
  4. seckillStatus.setStatus(2); //下单后,等待支付
  5. seckillStatus.setOrderId(seckillOrderEntity.getId());
  6. seckillStatus.setMoney(seckillOrderEntity.getMoney().floatValue());
  7. redisTemplate.boundHashOps("UserQueueStatus").put(username,JSON.toJSONString(seckillStatus));
  8. ...
  9. }

3、细节功能:避免重复排队

思路:可以以UserQueueCount为key,以当前登录名为小key,让其从1开始自增,如果自增后的值大于1,说明不能排队。

  1. @Override
  2. public boolean save(String secKillId, String time, String username) {
  3. // 定义用户状态信息保存到redis中的队列中
  4. SeckillStatus seckillStatus = new SeckillStatus(username,new Date(),1, Long.parseLong(secKillId),time);
  5. //避免重复排队
  6. Long queueCount = redisTemplate.boundHashOps("UserQueueCount").increment(username, 1);
  7. if(queueCount > 1){
  8. throw new RuntimeException(StatusCode.REPERROR + "");
  9. }
  10. // 将此对象入队列,目的是为了公平原则(先排队先下单)
  11. redisTemplate.boundListOps("SeckillOrderQueue").leftPush(JSON.toJSONString(seckillStatus));
  12. // 为了后面查询订单状态,我们将上面的seckillStatus又保存一份到redis中(因上面的会被立即弹出)
  13. redisTemplate.boundHashOps("UserQueueStatus").put(username,JSON.toJSONString(seckillStatus));
  14. //异步下单
  15. multiThreadingCreateOrder.createOrder();
  16. return true;
  17. }

4、细节功能:避免商品超卖

2、秒杀-内存超卖.jpg
思路:
我们可以在定时任务放商品信息时,以SeckillGoodsCountList商品id为key,以一个这样的数组为值(数组的个数为商品的个数,数组的元素内容为商品id【可以放任意值】)放到redis的队列中。
方案一【推荐】
在我们队Redis进行操作的时候,很多时候,都是先将数据查询出来,在内存中修改,然后存入到Redis,在并发场景,会出现数据错乱问题,为了控制数量准确,我们单独将商品数量整一个自增键,自增键是线程安全的,所以不担心并发场景的问题。
方案二:
可以根据SeckillGoodsCountList
商品id这个队列中的商品的个数就知道了商品的数量。

  1. /**
  2. * 功能: 将数据库中的数据加载到redis中
  3. * 参数:
  4. * 返回值: void
  5. * 时间: 2021/8/18 16:24
  6. */
  7. @Scheduled(cron = "0/30 * * * * ?")
  8. public void loadDataToRedis(){
  9. //1. 得到日期菜单
  10. List<Date> dateMenus = MyDate.getDateMenus();
  11. //2. 遍历日期菜单
  12. for (Date dateMenu : dateMenus) {
  13. //2.1 定义查询条件
  14. QueryWrapper<SeckillGoodsEntity> queryWrapper = new QueryWrapper<SeckillGoodsEntity>()
  15. .eq("status", 1)
  16. .ge("start_time",dateMenu)
  17. // .lt("start_time",MyDate.addDateHour(dateMenu,2))
  18. .lt("end_time",MyDate.addDateHour(dateMenu,2))
  19. .gt("stock_count",0);
  20. //2.2 查询redis中指定大key中是否包含有小key集合
  21. Set keys = redisTemplate.boundHashOps("SeckillGoods_" + MyDate.getDateStr(dateMenu)).keys();
  22. //2.3 判断是否keys存在
  23. if(keys != null && keys.size() > 0){
  24. queryWrapper.notIn("id",keys);
  25. }
  26. //2.4 查询商品列表
  27. List<SeckillGoodsEntity> goodsEntities = seckillGoodsDao.selectList(queryWrapper);
  28. //2.5 将商品放到redis中
  29. for (SeckillGoodsEntity goodsEntity : goodsEntities) {
  30. BoundHashOperations<String, String, String> hashOperations = redisTemplate.boundHashOps("SeckillGoods_" + MyDate.getDateStr(dateMenu));
  31. hashOperations.put(goodsEntity.getId() + "", JSON.toJSONString(goodsEntity));
  32. //设置过期时间
  33. //[添加的内容一]:以SeckillGoodsCountList_商品id为key,将一个指定的数组(数组的长度为商品数量,元素是商品id)放到redis中
  34. String[] ids = pushGoods(goodsEntity.getStockCount(), goodsEntity.getId());
  35. redisTemplate.boundListOps("SeckillGoodsCountList_"+goodsEntity.getId()).leftPushAll(ids);
  36. //[添加的内容二]:以SeckillGoodsCount为大key,以商品id为小key,作一个对应商品的自增键,以后下单扣完库存的实际数量,就是这里的自增键
  37. redisTemplate.boundHashOps("SeckillGoodsCount").put(goodsEntity.getId()+"",goodsEntity.getStockCount());
  38. }
  39. }
  40. }
  41. /**
  42. * 功能: 以商品数量为数组长度,以商品id为数组元素
  43. * 参数:
  44. * 返回值:
  45. * 时间: 2021/8/20 15:36
  46. */
  47. private String[] pushGoods(int len,long id){
  48. String[] ids = new String[len];
  49. for (int i = 0; i < len; i++) {
  50. ids[i] = id + "";
  51. }
  52. return ids;
  53. }

问题一:在哪里控制?在下单 前我们可以从SeckillGoodsCountList_商品id这个队列的右边弹出一项,看是否有内容,有内容就证明还有商品,否则,没有商品,清理排队信息。。。

  1. public void createOrder() {
  2. try {
  3. // String time = "2021081916";
  4. // String username = "test";
  5. // String secKillId = "1";
  6. //从redis中得到SeckillOrderQueue这个队列的值
  7. Thread.sleep(20000);
  8. String orderQueue = redisTemplate.boundListOps("SeckillOrderQueue").rightPop();
  9. //转换为 SeckillStatus类型
  10. SeckillStatus seckillStatus = JSON.parseObject(orderQueue, SeckillStatus.class);
  11. //从SeckillGoodsCountList_商品id的redis队列中取出队列,看是否有内容,如果有证明此商品还没有卖完
  12. String rightPop = redisTemplate.boundListOps("SeckillGoodsCountList_" + seckillStatus.getSecKillId()).rightPop();
  13. if(rightPop == null){ //如果为null就证明没有商品了,清除排队数据
  14. clearQueue(seckillStatus.getUsername());
  15. return;
  16. }
  17. 。。。
  18. }
  19. /**
  20. * 功能: 清除排队相关的数据
  21. * 参数:
  22. * 返回值: void
  23. * 时间: 2021/8/20 15:43
  24. */
  25. private void clearQueue(String username) {
  26. //1. 清除是否重复排队标识
  27. redisTemplate.boundHashOps("UserQueueCount").delete(username);
  28. //2. 清除排队的标识信息
  29. redisTemplate.boundHashOps("UserQueueStatus").delete(username);
  30. }

问题二:如何控制商品库存量的精准?在下单前判断商品数量是否为0,此时,要得到精准数量判断。
3、秒杀-控制库存量精准.jpg

  1. public void createOrder() {
  2. //2.2 通过自增键得到商品的真实库存量,因为高并发场景,可能存在着数据的错乱(得到精准的库存量)
  3. Long goodsCount = redisTemplate.boundHashOps("SeckillGoodsCount").increment(seckillGoodsEntity.getId() + "", -1);
  4. seckillGoodsEntity.setStockCount(new Integer(goodsCount + ""));
  5. }

总结秒杀环节的问题:

1、排队的顺序问题:用redis的list队列解决。 2、排队的标识:最后通过它修改状态,直接在redis中保存一份排队信息。 3、库存超卖问题: 以SeckillGoodsCountList_商品id为key,以数组(长度为商品数量,内容为商品id)为值在定时任务放商品时就放到redis中,以后每次下单前就右边弹出数据,如果有证明可以下单 ,否则,不能下单 ,清空排队信息。 4、重复排队: 在下单 前添加以UserQueueCount为大key,以登录用户为小key,放一个自增值1,自增后如果大于就证明重复排队,可以抛出异常! 5、在高并发场景下,数据的不精准性控制: 利用在定时任务开始时添加自增键,自增键的值就是商品的数量,在下单前,查看商品数量前,减少自增键,得到当前的商品真实数量。

2.2 秒杀支付:

2.2.1 在zyg-pay-service的application.yml文件中定义关于秒杀支付的同步及异步回调地址:

  1. alipay:
  2. notify_url1: http://42d323h910.qicp.vip/seckillOrder/payed/notify
  3. return_url1: http://seckill.zeyigou.com/orderlist

2.2.2 在zyg-pay-service的AlipayTempalte.java中定义上面两个属性:

  1. private String notify_url1;
  2. private String return_url1;

2.2.3 在zyg-pay-service的pay()方法中新增一个代表支付类型的参数:

  1. @ConfigurationProperties(prefix = "alipay")
  2. @Component
  3. @Data
  4. public class AlipayTemplate {
  5. //在支付宝创建的应用的id
  6. private String app_id ;//= "2016110200786382";
  7. // 商户私钥,您的PKCS8格式RSA2私钥
  8. private String merchant_private_key ;
  9. // 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥。
  10. private String alipay_public_key ;
  11. // 服务器[异步通知]页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
  12. // 支付宝会悄悄的给我们发送一个请求,告诉我们支付成功的信息
  13. private String notify_url ;//= "http://localhost:8000/payed/notify";
  14. // 页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
  15. //同步通知,支付成功,一般跳转到成功页
  16. private String return_url ;
  17. private String notify_url1;
  18. private String return_url1;
  19. // 签名方式
  20. private String sign_type ;
  21. // 字符编码格式
  22. private String charset ;
  23. private String timeout ;
  24. // 支付宝网关; https://openapi.alipaydev.com/gateway.do
  25. private String gatewayUrl ;
  26. public String pay(PayVo vo,int type) throws AlipayApiException {
  27. //1、根据支付宝的配置生成一个支付客户端
  28. AlipayClient alipayClient = new DefaultAlipayClient(gatewayUrl,
  29. app_id, merchant_private_key, "json",
  30. charset, alipay_public_key, sign_type);
  31. //2、创建一个支付请求 //设置请求参数
  32. AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();
  33. if(type == 1){
  34. alipayRequest.setReturnUrl(return_url);
  35. alipayRequest.setNotifyUrl(notify_url);
  36. }
  37. if(type == 2){
  38. alipayRequest.setReturnUrl(return_url1);
  39. alipayRequest.setNotifyUrl(notify_url1);
  40. }
  41. //商户订单号,商户网站订单系统中唯一订单号,必填
  42. String out_trade_no = vo.getOut_trade_no();
  43. //付款金额,必填
  44. String total_amount = vo.getTotal_amount();
  45. //订单名称,必填
  46. String subject = vo.getSubject();
  47. //商品描述,可空
  48. String body = vo.getBody();
  49. alipayRequest.setBizContent("{\"out_trade_no\":\""+ out_trade_no +"\","
  50. + "\"total_amount\":\""+ total_amount +"\","
  51. + "\"subject\":\""+ subject +"\","
  52. + "\"body\":\""+ body +"\","
  53. + "\"timeout_express\":\""+timeout+"\","
  54. + "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}");
  55. String result = alipayClient.pageExecute(alipayRequest).getBody();
  56. //会收到支付宝的响应,响应的是一个页面,只要浏览器显示这个页面,就会自动来到支付宝的收银台页面
  57. System.out.println("支付宝的响应:"+result);
  58. return result;
  59. }
  60. }

2.2.4 在zyg-seckill-web中定义pay方法调用上面的支付函数:[形成支付的页面]

  1. /**
  2. * 功能: 秒杀支付
  3. * 参数:
  4. * 返回值:
  5. * 时间: 2021/8/20 16:47
  6. */
  7. @RequestMapping("pay")
  8. @ResponseBody
  9. public String pay(){
  10. //0. 得到登录名
  11. String name = SecurityContextHolder.getContext().getAuthentication().getName();
  12. //1. 从redis中查询出订单并返回payVo对象
  13. PayVo payVo = seckillOrderService.getPayVo(name);
  14. String result = payService.pay(payVo, 2);
  15. return result;
  16. }

2.2.5 在zyg-seckill-web中定义关于支付的同步回调函数:

  1. /**
  2. * 功能: 支付成功后的同步回调
  3. * 参数:
  4. * 返回值:
  5. * 时间: 2021/8/20 17:41
  6. */
  7. @RequestMapping("orderlist")
  8. public String orderlist(Model model){
  9. //0. 得到登录id
  10. String name = SecurityContextHolder.getContext().getAuthentication().getName();
  11. //1. 根据登录用户id查询出此用户的订单列表及此订单的秒杀商品列表
  12. List<SeckillOrderEntity> orderEntities = seckillOrderService.findOrders(name);
  13. System.out.println("orderEntities = " + orderEntities);
  14. model.addAttribute("orderList",orderEntities);
  15. return "seckill-order";
  16. }

2.2.6 在zyg-page-web中定义异步回调函数:

  1. /**
  2. * 功能: 支付宝发送异步通知【订单支付模块】
  3. * 参数:
  4. * 返回值: java.lang.String
  5. * 时间: 2021/8/17 16:46
  6. */
  7. @PostMapping("/seckillOrder/payed/notify")
  8. public String payedNotify(PayAsyncVo vo, HttpServletRequest request){
  9. System.out.println("vo = " + vo);
  10. //1. 得到登录用户
  11. //String userId = SecurityContextHolder.getContext().getAuthentication().getName();
  12. //2. 得支支付宝后台传入的数据
  13. Map<String,String> params = new HashMap<>();
  14. Map<String,String[]> requestParams = request.getParameterMap();
  15. for (Iterator<String> iter = requestParams.keySet().iterator(); iter.hasNext();) {
  16. String name = (String) iter.next();
  17. String[] values = (String[]) requestParams.get(name);
  18. String valueStr = "";
  19. for (int i = 0; i < values.length; i++) {
  20. valueStr = (i == values.length - 1) ? valueStr + values[i]
  21. : valueStr + values[i] + ",";
  22. }
  23. //乱码解决,这段代码在出现乱码时使用
  24. //valueStr = new String(valueStr.getBytes("ISO-8859-1"), "utf-8");
  25. params.put(name, valueStr);
  26. }
  27. String result = payService.orderPayNotify(vo,params);
  28. System.out.println("result = " + result);
  29. return result;
  30. }

2.2.6 在zyg-pay-service中定义orderPayNotify()异步回调方法:

  1. @Transactional
  2. @Override
  3. public String orderPayNotify(PayAsyncVo vo, Map<String, String> params) {
  4. try {
  5. //1. 进行验签
  6. boolean signVerified = AlipaySignature.rsaCheckV1(params, alipayTemplate.getAlipay_public_key(),
  7. alipayTemplate.getCharset(), alipayTemplate.getSign_type()); //调用SDK验证签名
  8. //2. 判断是否验签成功
  9. if(signVerified){
  10. //第一部分:修改秒杀订单日志
  11. //1.1 得到秒杀订单号
  12. String out_trade_no = vo.getOut_trade_no();
  13. //1.2 根据订单号得到登录名
  14. String username = redisTemplate.opsForValue().get(out_trade_no);
  15. //1.3 根据登录名得到redis中的秒杀订单
  16. String secKillStr = (String) redisTemplate.boundHashOps("SeckillOrder").get(username);
  17. SeckillOrderEntity orderEntity = JSON.parseObject(secKillStr,SeckillOrderEntity.class);
  18. if (vo.getTrade_status().equals("TRADE_SUCCESS") || vo.getTrade_status().equals("TRADE_FINISHED")){
  19. //第二部分:修改订单
  20. //2.1 修改redis中的订单信息
  21. orderEntity.setTransactionId(vo.getTrade_no()); //设置流水号
  22. orderEntity.setPayTime(new Date());
  23. orderEntity.setStatus("1"); //代表支付成功
  24. //2.2 保存到db中
  25. seckillOrderDao.insert(orderEntity); //添加到数据库中
  26. //2.3 在db中将购买的商品的库存减掉
  27. String time = MyDate.getDateStr(MyDate.getDateMenus().get(0));
  28. String s = (String) redisTemplate.boundHashOps("SeckillGoods_" + time).get(orderEntity.getSeckillId() + "");
  29. SeckillGoodsEntity seckillGoodsEntity = JSON.parseObject(s,SeckillGoodsEntity.class);
  30. goodsDao.updateById(seckillGoodsEntity);
  31. //2.4 从redis中删除订单
  32. redisTemplate.boundHashOps("SeckillOrder").delete(username);
  33. //2.5 也要删除该用户的排队相关的信息
  34. //2.5.1 清除是否重复排队标识
  35. redisTemplate.boundHashOps("UserQueueCount").delete(username);
  36. //2.5.2 清除排队的标识信息
  37. redisTemplate.boundHashOps("UserQueueStatus").delete(username);
  38. }
  39. System.out.println("支付成功");
  40. return "success";
  41. }
  42. } catch (AlipayApiException e) {
  43. e.printStackTrace();
  44. }
  45. System.out.println("支付失败!");
  46. return "fail";
  47. }

2.2.7 最后的效果页面:

image.png
image.png
image.png