秒杀服务-----功能实现逻辑2

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 秒杀服务-----功能实现逻辑2

通过HashMap结构把每个秒杀商品的详细信息以下面的格式存到Redis中,Redis缓存中具体格式如下:


hash值是seckill:skus key: 4_47 value: SeckillSkuRedisTo对象

其中key的4表示秒杀的场次id,47表示秒杀商品的skuId,由于用的是hashMap结构,其中hash值是seckill:skus

Redission实现分布式信号量设置时就会把信号量以key-value的格式存到reids缓存中,Redis缓存中信号量格式如下:


key: seckill:stock:随机码 value:每个秒杀商品的总数量

其中key的seckill:stock是固定前缀,随机码就是随机成功的uuid值,把每个秒杀商品的总数量作为信号量的值

saveSessionSkuInfo方法具体流程如下:


准备hash操作,绑定seckill:skus关键字的hash

遍历封装存入redis的秒杀活动的秒杀商品项

生成随机码

封装SeckillSkuRedisTo对象并序列化后存入redis缓存

远程调用product商品服务

使用Redission实现分布式信号量来把秒杀商品的库存总数量作为信号量存入redis缓存(限流)

秒杀活动的商品项的详细信息存入redis缓存

设置商品的随机码(防止恶意攻击)

设置当前商品的秒杀时间信息

封装秒杀活动中秒杀商品项信息


2、查询秒杀商品

查询秒杀商品具体实现

1、商城首页每次刷新都发这个请求来获取当前的所有秒杀商品信息

 // 秒杀服务请求
  $.get("http://seckill.saodaimall.com/getCurrentSeckillSkus", function (res) {
    if (res.data.length > 0) {
      res.data.forEach(function (item) {
        $("<li onclick='toDetail(" + item.skuId + ")'></li>").append($("<img style='width: 130px; height: 130px' src='" + item.skuInfo.skuDefaultImg + "' />"))
                .append($("<p>"+item.skuInfo.skuTitle+"</p>"))
                .append($("<span>" + item.seckillPrice + "</span>"))
                .append($("<s>" + item.skuInfo.price + "</s>"))
                .appendTo("#seckillSkuContent");
      })
    }
  })


2、编写秒杀服务的SeckillController控制器来处理上面的请求/getCurrentSeckillSkus

  @Autowired
    private SeckillService seckillService;
    /**
     * 当前时间可以参与秒杀的商品信息
     * @return
     */
    @GetMapping(value = "/getCurrentSeckillSkus")
    @ResponseBody
    public R getCurrentSeckillSkus() {
        //获取到当前可以参加秒杀商品的信息
        List<SeckillSkuRedisTo> vos = seckillService.getCurrentSeckillSkus();
        return R.ok().setData(vos);
    }
    /**
     * 获取到当前可以参加秒杀商品的信息
     * @return
     */
    @Override
    public List<SeckillSkuRedisTo> getCurrentSeckillSkus() {
            //1、确定当前属于哪个秒杀场次
           //获取当前时间
            long currentTime = System.currentTimeMillis();
            //查找所有redis里的秒杀活动的信息,从Redis中查询到所有key以seckill:sessions开头的所有数据(其实就是查找所有redis里的秒杀活动的信息)
            Set<String> keys = redisTemplate.keys(SESSION__CACHE_PREFIX + "*");
            for (String key : keys) {
                // redis中秒杀活动取出来的格式是seckill:sessions:1594396764000_1594453242000
                //把seckill:sessions:前缀给去掉
                String replace = key.replace(SESSION__CACHE_PREFIX, "");
                String[] s = replace.split("_");
                //获取存入Redis商品的开始时间
                long startTime = Long.parseLong(s[0]);
                //获取存入Redis商品的结束时间
                long endTime = Long.parseLong(s[1]);
                //判断是否是当前秒杀场次 seckill:sessions:1648099200000_1648123200000
                if (currentTime >= startTime && currentTime <= endTime) {
                    //2、获取这个秒杀场次需要的所有商品信息也就是key对应的value(这个key是当前秒杀活动的key)-100到100表示取出所有的key
                    List<String> range = redisTemplate.opsForList().range(key, -100, 100);
                    //SECKILL_CHARE_PREFIX是seckill:skus
                    BoundHashOperations<String, String, String> hasOps = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
                    //3、multiGet是多个key对应的value值(获取所有的秒杀商品的详细信息)
                    List<String> listValue = hasOps.multiGet(range);
                    if (listValue != null && listValue.size() >= 0) {
                        List<SeckillSkuRedisTo> collect = listValue.stream().map(item -> {
                            SeckillSkuRedisTo redisTo = JSON.parseObject(items, SeckillSkuRedisTo.class);
                            // redisTo.setRandomCode(null);当前秒杀开始需要随机码
                            return redisTo;
                        }).collect(Collectors.toList());
                        return collect;
                    }
                    break;
                }
            }
        return null;
    }


首先拿到所有秒杀活动信息的key值keys,然后判断秒杀活动的开始和结束时间是否在当前时间内,也就是选中当前时间参加秒杀的秒杀活动,存到Redis缓存中秒杀活动的格式如下:


key:seckill:sessions:1648099200000_1648123200000,value:4-47

其中key的seckill:sessions:是前缀,1648099200000表示秒杀活动开始的时间,1648123200000表示秒杀活动结束的时间,value的4表示秒杀的场次id,47表示秒杀商品的skuId,一个秒杀活动里有多个秒杀商品。

通过遍历所有的key,判断每个key的时间戳来选中当前时间参加秒杀的秒杀活动,然后拿出所有参加的秒杀活动里的所有秒杀商品信息range,通过HashMap结构把每个秒杀商品的详细信息以下面的格式存到Redis中,Redis缓存中具体格式如下:


hash值是seckill:skus key: 4_47 value: SeckillSkuRedisTo对象

其中key的4表示秒杀的场次id,47表示秒杀商品的skuId,由于用的是hashMap结构,其中hash值是seckill:skus

这里可以看出上面秒杀活动的值是作为下面秒杀商品的key值的,通过这个来把秒杀活动和秒杀商品关联起来,可以获取所有的秒杀商品的详细信息,最后把json字符串转为SeckillSkuRedisTo对象(秒杀商品的详细信息)封装成一个集合List<SeckillSkuRedisTo>


分流程:


获取redis里所有的秒杀活动的key,key的格式seckill:sessions:1594396764000_1594453242000(这里注意从Redis中获取到的数据都是Json类型)

遍历获取到的所有秒杀活动的key(下面的1、2步是操作redis中的秒杀活动)

判断有没有属于当前时间的秒杀活动

获取当前秒杀活动所有key对应的所有value,value的格式为4-47

获取所有的秒杀活动商品项的详细信息

把获取到的所有秒杀活动商品项的详细信息转为SeckillSkuRedisTo对象并收集成 List<SeckillSkuRedisTo>


3、完善商品详细信息类(新增秒杀)

(1)给sku实体类新增秒杀属性

//新增的秒杀属性(用于判断该商品项有没有参加秒杀活动)

private SeckillSkuVo seckillSkuVo;
package com.saodai.saodaimall.product.vo;
import com.saodai.saodaimall.product.entity.SkuImagesEntity;
import com.saodai.saodaimall.product.entity.SkuInfoEntity;
import com.saodai.saodaimall.product.entity.SpuInfoDescEntity;
import lombok.Data;
import lombok.ToString;
import java.util.List;
/**
 * sku详细信息
 **/
@ToString
@Data
public class SkuItemVo {
    //1、sku基本信息的获取  pms_sku_info
    private SkuInfoEntity info;
    private boolean hasStock = true;
    //2、sku的图片信息    pms_sku_images
    private List<SkuImagesEntity> images;
    //3、获取spu的销售属性组合
    private List<SkuItemSaleAttrVo> saleAttr;
    //4、获取spu的介绍
    private SpuInfoDescEntity desc;
    //5、获取spu的规格参数信息
    private List<SpuItemAttrGroupVo> groupAttrs;
    //6、秒杀商品信息
    private SeckillSkuVo seckillSkuVo;
}
package com.saodai.saodaimall.product.vo;
import lombok.Data;
import java.math.BigDecimal;
/**
 * 秒杀商品的信息
 **/
@Data
public class SeckillSkuVo {
    /**
     * 活动id
     */
    private Long promotionId;
    /**
     * 活动场次id
     */
    private Long promotionSessionId;
    /**
     * 商品id
     */
    private Long skuId;
    /**
     * 秒杀价格
     */
    private BigDecimal seckillPrice;
    /**
     * 秒杀总量
     */
    private Integer seckillCount;
    /**
     * 每人限购数量
     */
    private Integer seckillLimit;
    /**
     * 排序
     */
    private Integer seckillSort;
    //当前商品秒杀的开始时间
    private Long startTime;
    //当前商品秒杀的结束时间
    private Long endTime;
    //当前商品秒杀的随机码
    private String randomCode;
}


(2)在SkuInfoServiceImpl实现类的item方法中添加封装秒杀属性

流程

1、远程调用秒杀服务查看当前商品有没有从参加秒杀活动

2、如果超过了秒杀活动的最后期限就设置秒杀信息为空

3、把远程查询到的该商品的秒杀信息封装在SkuItemVo对象中

 //秒杀
        CompletableFuture<Void> seckillFuture = CompletableFuture.runAsync(() -> {
            //3、远程调用查询当前sku是否参与秒杀优惠活动
            R skuSeckilInfo = seckillFeignService.getSkuSeckilInfo(skuId);
            if (skuSeckilInfo.getCode() == 0) {
                //查询成功
                SeckillSkuVo seckilInfoData = skuSeckilInfo.getData("data", new TypeReference<SeckillSkuVo>() {
                });
                //如果超过了秒杀活动的最后期限就设置秒杀信息为空
                if (seckilInfoData != null) {
                    long currentTime = System.currentTimeMillis();
                    if (currentTime > seckilInfoData.getEndTime()) {
                        skuItemVo.setSeckillSkuVo(null);
                    }
                }
                 skuItemVo.setSeckillSkuVo(seckilInfoData);
            }
        }, executor);
        //等到所有任务都完成
        CompletableFuture.allOf(saleAttrFuture,descFuture,baseAttrFuture,imageFuture,seckillFuture).get();

(3)远程调用秒杀服务的接口

package com.saodai.saodaimall.product.feign;
import com.saodai.common.utils.R;
import com.saodai.saodaimall.product.fallback.SeckillFeignServiceFallBack;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
/**
 * 远程调用秒杀接口
 **/
@FeignClient(value = "saodaimall-seckill",fallback = SeckillFeignServiceFallBack.class)
public interface SeckillFeignService {
    /**
     * 根据skuId查询商品是否参加秒杀活动
     * @param skuId
     * @return
     */
    @GetMapping(value = "/sku/seckill/{skuId}")
    R getSkuSeckilInfo(@PathVariable("skuId") Long skuId);
}

(4)秒杀服务的SeckillController来查询当前商品有没有参加秒杀活动

 /**
     * 根据skuId查询商品是否参加秒杀活动
     * @param skuId
     * @return
     */
    @GetMapping(value = "/sku/seckill/{skuId}")
    @ResponseBody
    public R getSkuSeckilInfo(@PathVariable("skuId") Long skuId) {
        SeckillSkuRedisTo to = seckillService.getSkuSeckilInfo(skuId);
        return R.ok().setData(to);
    }

(5)getSkuSeckilInfo具体实现

/**
     * 根据skuId查询商品是否参加秒杀活动
     * @param skuId
     * @return
     */
    @Override
    public SeckillSkuRedisTo getSkuSeckilInfo(Long skuId) {
        //1、绑定HashMap操作,Hash值为seckill:skus
        BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
        //拿到所有的key
        Set<String> keys = hashOps.keys();
        if (keys != null && keys.size() > 0) {
            //4-45 正则表达式进行匹配
            String reg = "\\d-" + skuId;
            for (String key : keys) {
                //如果匹配上了
                if (Pattern.matches(reg,key)) {
                    //从Redis中取出数据来
                    String redisValue = hashOps.get(key);
                    //进行序列化
                    SeckillSkuRedisTo redisTo = JSON.parseObject(redisValue, SeckillSkuRedisTo.class);
                    Long currentTime = System.currentTimeMillis();
                    Long startTime = redisTo.getStartTime();
                    Long endTime = redisTo.getEndTime();
                    //如果当前时间大于等于秒杀活动开始时间并且要小于活动结束时间
                    if (currentTime >= startTime && currentTime <= endTime) {
                        return redisTo;
                    }
                    redisTo.setRandomCode(null);
                    return redisTo;
                }
            }
        }
        return null;
    }

流程:


1、绑定HashMap操作,Hash值为seckill:skus


2、获取所有的key,key的格式为4-47


3、遍历所有的key并且用正则表达式来判断这个商品有没有对应的key


4、如果这个商品参加了秒杀就取出reids中的数据并且转为SeckillSkuRedisTo对象


5、如果当前时间大于等于秒杀活动开始时间并且要小于活动结束时间


4、渲染秒杀页面

(1)首页的秒杀商品项的渲染

<div class="swiper-slide">
     <!-- 动态拼装秒杀商品信息 -->
   <ul id="seckillSkuContent"></ul>
</div>

动态拼装秒杀商品信息


发送http://seckill.saodaimall.com/getCurrentSeckillSkus这个get请求后由秒杀服务的SeckillController的getCurrentSeckillSkus方法来处理,处理后返回List<SeckillSkuRedisTo>集合,也就是res代指的是后端封装好的List<SeckillSkuRedisTo>集合


  // 秒杀服务请求
  $.get("http://seckill.saodaimall.com/getCurrentSeckillSkus", function (res) {
    if (res.data.length > 0) {
      res.data.forEach(function (item) {
        //给每一个秒杀商品项绑定一个点击事件toDetail 
        $("<li onclick='toDetail(" + item.skuId + ")'></li>")
            .append($("<img style='width: 130px; height: 130px' src='" + item.skuInfo.skuDefaultImg + "' />"))
                .append($("<p>"+item.skuInfo.skuTitle+"</p>"))
                .append($("<span>" + item.seckillPrice + "</span>"))
                .append($("<s>" + item.skuInfo.price + "</s>"))
                .appendTo("#seckillSkuContent");
      })
    }
  })
      //点击秒杀商品后跳到商品详细界面
    function toDetail(skuId) {
    location.href = "http://item.saodaimall.com/" + skuId + ".html";
  }

上面动态拼装秒杀商品信息就是按下面的模板拼装的,只是说用的dom动态拼装,而下面的是静态的

<div class="swiper-slide">
  <ul>
    <li>
      <img src=" /static/index/img/section_second_list_img6.jpg" alt="">
      <p>Apple iMac 21.5英寸一体机(2017新款四核Core i5 处理器/8GB内存/1TB/RP555显卡/4K屏 MNDY2CH/A) Apple iMac 21.5英寸一体机(2017新款四核Core i5 处理</p>
      <span>¥9588.00</span><s>¥10288.00</s>
    </li>
  </ul>
</div>

(2)商品详细信息的渲染

下面的item是商品服务web里的ItemController控制器返回的SkuItemVo对象(其实就是商品项详细页面发送的请求来获取商品详细信息的,seckillSkuVo是SkuItemVo对象里的秒杀商品信息)

package com.saodai.saodaimall.product.vo;
import com.saodai.saodaimall.product.entity.SkuImagesEntity;
import com.saodai.saodaimall.product.entity.SkuInfoEntity;
import com.saodai.saodaimall.product.entity.SpuInfoDescEntity;
import lombok.Data;
import lombok.ToString;
import java.util.List;
/**
* sku详细信息
**/
@ToString
@Data
public class SkuItemVo {
    //1、sku基本信息的获取  pms_sku_info
    private SkuInfoEntity info;
    private boolean hasStock = true;
    //2、sku的图片信息    pms_sku_images
    private List<SkuImagesEntity> images;
    //3、获取spu的销售属性组合
    private List<SkuItemSaleAttrVo> saleAttr;
    //4、获取spu的介绍
    private SpuInfoDescEntity desc;
    //5、获取spu的规格参数信息
    private List<SpuItemAttrGroupVo> groupAttrs;
    //6、秒杀商品的优惠信息
    private SeckillSkuVo seckillSkuVo;
}

代码讲解

1、${#dates.createNow().getTime() < item.seckillSkuVo.startTime}


#dates表示日期工具类,.createNow().getTime() < item.seckillSkuVo.startTime表示获取当前时间然后比秒杀开始时间要早就提示哪个时候开始秒杀


2、[[${#dates.format(new java.util.Date(item.seckillSkuVo.startTime),"yyyy-MM-dd HH:mm:ss")}]]


表示获取秒杀活动开始的时间并进行日期格式化,格式化的样式yyyy-MM-dd HH:mm:ss


3、${#dates.createNow().getTime() >= item.seckillSkuVo.startTime && #dates.createNow().getTime() <= item.seckillSkuVo.endTime}


如果当前商品的当前时间在秒杀活动的时间范围内就显示秒杀价


4、[[${#numbers.formatDecimal(item.seckillSkuVo.seckillPrice,1,2)}]]


对秒杀价进行格式化,1表示小数点左边为1位数,2表示小数点右边为两位数,例如9.99


<li style="color: red" th:if="${item.seckillSkuVo != null}">
  <span th:if="${#dates.createNow().getTime() < item.seckillSkuVo.startTime}">
    商品将会在[[${#dates.format(new java.util.Date(item.seckillSkuVo.startTime),"yyyy-MM-dd HH:mm:ss")}]]进行秒杀
  </span>
  <span th:if="${#dates.createNow().getTime() >= item.seckillSkuVo.startTime && #dates.createNow().getTime() <= item.seckillSkuVo.endTime}">
    秒杀价  [[${#numbers.formatDecimal(item.seckillSkuVo.seckillPrice,1,2)}]]
  </span>
</li>

代码讲解

th:attr="skuId=${item.info.skuId},sessionId=${item.seckillSkuVo.promotionSessionId},code=${item.seckillSkuVo.randomCode}


自定义属性名skuId(商品项的id)、sessionId(秒杀活动场次的id)、code(随机码,用于防止恶意抢单),自定义这三个属性是因为后面实现立即请购商品时需要用(也就是第四步)


<div class="box-btns-two"
     th:if="${item.seckillSkuVo == null }">
  <a class="addToCart" href="http://cart.saodaimall.com/addToCart" th:attr="skuId=${item.info.skuId}">
    加入购物车
  </a>
</div>
<div class="box-btns-two"
     th:if="${item.seckillSkuVo != null && (#dates.createNow().getTime() >= item.seckillSkuVo.startTime && #dates.createNow().getTime() <= item.seckillSkuVo.endTime)}">
  <a class="seckill" href="#"
     th:attr="skuId=${item.info.skuId},sessionId=${item.seckillSkuVo.promotionSessionId},code=${item.seckillSkuVo.randomCode}">
    立即抢购
  </a>
</div>
package com.saodai.saodaimall.product.vo;
import lombok.Data;
import java.math.BigDecimal;
/**
 * 秒杀商品的信息
 **/
@Data
public class SeckillSkuVo {
    /**
     * 活动id
     */
    private Long promotionId;
    /**
     * 活动场次id
     */
    private Long promotionSessionId;
    /**
     * 商品id
     */
    private Long skuId;
    /**
     * 秒杀价格
     */
    private BigDecimal seckillPrice;
    /**
     * 秒杀总量
     */
    private Integer seckillCount;
    /**
     * 每人限购数量
     */
    private Integer seckillLimit;
    /**
     * 排序
     */
    private Integer seckillSort;
    //当前商品秒杀的开始时间
    private Long startTime;
    //当前商品秒杀的结束时间
    private Long endTime;
    //当前商品秒杀的随机码
    private String randomCode;
}


5、点击立即抢购发送请求

(1)判断登录状态,只有登录了才可以抢购


(2)发请求"http://seckill.saodaimall.com/kill?killId=" + killId + "&key=" + code + "&num=" + num


例如http://seckill.saodaimall.com/kill?killId=4-39&key=


9b4b6e45923b4d88ae9739a5240d0459&num=1


其中killId是秒杀Id(其实就是封装成redis中的格式),key是随机码,num是抢购数量


// 立即抢购之前必须登录
$(".seckill").click(function () {
    var isLogin = [[${session.loginUser != null}]];     
    if (isLogin) {
        var killId = $(this).attr("sessionid") + "-" + $(this).attr("skuid");
        var code = $(this).attr("code");
        var num = $("#productNum").val();
        location.href = "http://seckill.saodaimall.com/kill?killId=" + killId + "&key=" + code + "&num=" + num;
    } else {
        alert("秒杀请先登录");
    }
    return false;
    });

6、实现秒杀

(1)立即抢购的请求交给秒杀服务的SeckillController的seckill方法来处理

 /**
     * 当前商品进行秒杀(秒杀开始)
     * @param killId 秒杀号
     * @param key  随机码
     * @param num  秒杀商品的数量
     * @return
     */
    //http://seckill.saodaimall.com/kill?killId=4-39&key=9b4b6e45923b4d88ae9739a5240d0459&num=1
    @GetMapping(value = "/kill")
    public String seckill(@RequestParam("killId") String killId,
                          @RequestParam("key") String key,
                          @RequestParam("num") Integer num,
                          Model model) {
        String orderSn = null;
        try {
           //秒杀实现
            orderSn = seckillService.kill(killId,key,num);
            model.addAttribute("orderSn",orderSn);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "success";
    }


(2)秒杀的具体实现

    /**
     * 当前商品进行秒杀(秒杀开始)
     * @param killId 秒杀号
     * @param key  随机码
     * @param num  秒杀商品的数量
     * @return
     */
    @Override
    public String kill(String killId, String key, Integer num) throws InterruptedException {
        //测试这个方法执行的时间要多久
        long s1 = System.currentTimeMillis();
        //获取当前用户的信息
        MemberResponseVo user = LoginUserInterceptor.loginUser.get();
        //1、绑定hashMap操作redis,绑定的Hash值为seckill:skus
        BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
        //2、根据killId获取当前sku商品的详细信息
        String skuInfoValue = hashOps.get(killId);
        if (StringUtils.isEmpty(skuInfoValue)) {
            return null;
        }
        SeckillSkuRedisTo redisTo = JSON.parseObject(skuInfoValue, SeckillSkuRedisTo.class);
        Long startTime = redisTo.getStartTime();
        Long endTime = redisTo.getEndTime();
        long currentTime = System.currentTimeMillis();
        //判断当前这个秒杀请求是否在活动时间区间内(效验时间的合法性)
        if (currentTime >= startTime && currentTime <= endTime) {
            //2、效验随机码和商品id
            String randomCode = redisTo.getRandomCode();
            String skuId = redisTo.getPromotionSessionId() + "-" +redisTo.getSkuId();
            if (randomCode.equals(key) && killId.equals(skuId)) {
                //3、验证购物数量是否合理和库存量是否充足
                //获取秒杀的商品的限制数量
                Integer seckillLimit = redisTo.getSeckillLimit();
                //获取信号量(也就是库存数量)
                String seckillCount = redisTemplate.opsForValue().get(SKU_STOCK_SEMAPHORE + randomCode);
                Integer count = Integer.valueOf(seckillCount);
                //判断信号量是否大于0,并且买的数量不能超过库存
                if (count > 0 && num <= seckillLimit && count > num ) {
                    //4、验证这个人是否已经买过了(幂等性处理),防止有人每次的确只买限制次数的一个,但是他买多次
                    // 如果秒杀成功,就去占位。也就是在redis中存个标记位,表示这个用户买过了,不能再买
                    //SETNX 原子性处理
                    /**格式key:userId-skuId value:用户秒杀时买的数量**/
                    String redisKey = user.getId() + "-" + skuId;
                    //设置自动过期(活动结束时间-当前时间)
                    Long ttl = endTime - currentTime;
                    Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
                    if (aBoolean) {
                        //占位成功说明从来没有买过,分布式锁(获取信号量-1)
                        RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
                        //TODO 秒杀成功,快速下单
                        //尝试快速拿到信号量,100毫秒没有用拿到就返回false
                        boolean semaphoreCount = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);
                        //保证Redis中还有商品库存
                        if (semaphoreCount) {
                            //创建订单号和订单信息发送给MQ
                            // 秒杀成功 快速下单 发送消息到 MQ 整个操作时间在 10ms 左右
                            //快速生成一个秒杀订单号
                            String timeId = IdWorker.getTimeId();
                            SeckillOrderTo orderTo = new SeckillOrderTo();
                            //设置秒杀订单号
                            orderTo.setOrderSn(timeId);
                            orderTo.setMemberId(user.getId());
                            orderTo.setNum(num);
                            orderTo.setPromotionSessionId(redisTo.getPromotionSessionId());
                            orderTo.setSkuId(redisTo.getSkuId());
                            orderTo.setSeckillPrice(redisTo.getSeckillPrice());
                            rabbitTemplate.convertAndSend("order-event-exchange","order.seckill.order",orderTo);
                            long s2 = System.currentTimeMillis();
                            log.info("耗时..." + (s2 - s1));
                            return timeId;
                        }
                    }
                }
            }
        }
        long s3 = System.currentTimeMillis();
        log.info("耗时..." + (s3 - s1));
        return null;
    }

流程:


1、绑定hashMap操作redis,绑定的Hash值为seckill:skus


2、根据killId获取当前sku商品的详细信息,也就是获取这个key对应的value(这个killId其实就是redis中的key,格式为4-47)


3、判断当前这个秒杀商品是否在秒杀活动时间区间内(效验时间的合法性)


4、效验随机码是否和缓存里的一致


5、判断信号量是否大于0(也就是看有没有库存),并且买的数量不能超过秒杀商品数量的限制


6、进行幂等性处理(验证这个人是否已经买过了,防止有人每次的确只买限制次数的一个,但是他买多次,相当于发多次请求)


7、占坑成功后进行信号量处理(其实就是减信号量,表示业务为减库存)


8、快速生成一个秒杀订单号


9、封装SeckillOrderTo对象,然后发给RabbitMQ队列


7、订单服务来监听上面发送给RabbitMQ的order.seckill.order.queue队列的消息

(1)在订单服务的listener的OrderSeckillListener来监听消息队列的消息,然后自己创建秒杀订单

package com.saodai.saodaimall.order.listener;
import com.rabbitmq.client.Channel;
import com.saodai.common.to.mq.SeckillOrderTo;
import com.saodai.saodaimall.order.service.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
 * 秒杀订单的监听
 */
@Slf4j
@Component
@RabbitListener(queues = "order.seckill.order.queue")
public class OrderSeckillListener {
    @Autowired
    private OrderService orderService;
    @RabbitHandler
    public void listener(SeckillOrderTo orderTo, Channel channel, Message message) throws IOException {
        log.info("准备创建秒杀单的详细信息...");
        try {
            orderService.createSeckillOrder(orderTo);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        } catch (Exception e) {
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }
    }
}

(2)监听队列后在OrderServiceImpl创建秒杀订单

流程:

1、封装订单实体类

1>封装秒杀的信息到订单里

2>保存订单到数据库

2、封装秒杀订单项

1>封装秒杀信息

2>远程调用查询spu信息来封装到秒杀订单项

3>远程调用查询sku信息来封装到秒杀订单项

4>保存秒杀订单项到数据库


 /**
     * 创建秒杀单
     */
    @Override
    public void createSeckillOrder(SeckillOrderTo orderTo) {
       //封装订单实体类
        OrderEntity orderEntity = new OrderEntity();
        // 保存秒杀信息(getOrderSn这个订单号是秒杀的订单号)
        orderEntity.setOrderSn(orderTo.getOrderSn());
        orderEntity.setMemberId(orderTo.getMemberId());
        orderEntity.setCreateTime(new Date());
        //秒杀总共价格
        BigDecimal totalPrice = orderTo.getSeckillPrice().multiply(BigDecimal.valueOf(orderTo.getNum()));
        //应该支付的价格
        orderEntity.setPayAmount(totalPrice);
        //CREATE_NEW(0,"待付款")
        orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
        //保存秒杀订单
        this.save(orderEntity);
        //保存秒杀订单项信息
        OrderItemEntity orderItem = new OrderItemEntity();
        orderItem.setOrderSn(orderTo.getOrderSn());
        //秒杀总共价格
        orderItem.setRealAmount(totalPrice);
        //秒杀商品购买的数量
        orderItem.setSkuQuantity(orderTo.getNum());
        //保存商品的spu信息
        //远程调用商品服务来查询商品信息
        R spuInfo = productFeignService.getSpuInfoBySkuId(orderTo.getSkuId());
        SpuInfoVo spuInfoData = spuInfo.getData("data", new TypeReference<SpuInfoVo>() {
        });
        orderItem.setSpuId(spuInfoData.getId());
        orderItem.setSpuName(spuInfoData.getSpuName());
        orderItem.setSpuBrand(spuInfoData.getBrandName());
        orderItem.setCategoryId(spuInfoData.getCatalogId());
        //保存sku的信息
        R skuInfoBySkuId = productFeignService.getSkuInfoBySkuId(orderTo.getSkuId());
        SkuInfoTo skuInfoTo = skuInfoBySkuId.getData("data",new TypeReference<SkuInfoTo>(){
        });
        orderItem.setSkuId(skuInfoTo.getSkuId());
        orderItem.setSkuPic(skuInfoTo.getSkuDefaultImg());
        orderItem.setSkuName(skuInfoTo.getSkuName());
        orderItem.setSkuPrice(skuInfoTo.getPrice());
        //赠送积分
        orderItem.setGiftIntegration(0);
        //赠送成长值
        orderItem.setGiftGrowth(0);
        //保存订单项数据
        orderItemService.save(orderItem);
    }
package com.saodai.common.to.mq;
import lombok.Data;
import java.math.BigDecimal;
/**
 *秒杀订单类(秒杀商品成功后发给rabbit队列的封装类)
 **/
@Data
public class SeckillOrderTo {
    /**
     * 订单号
     */
    private String orderSn;
    /**
     * 活动场次id
     */
    private Long promotionSessionId;
    /**
     * 商品id
     */
    private Long skuId;
    /**
     * 秒杀价格
     */
    private BigDecimal seckillPrice;
    /**
     * 购买数量
     */
    private Integer num;
    /**
     * 会员ID
     */
    private Long memberId;
}
    /**
     * 根据skuid查找sku对象
     */
    @RequestMapping("/getSkuInfoBySkuId/{skuId}")
    //@RequiresPermissions("product:skuinfo:info")
    public R getSkuInfoBySkuId(@PathVariable("skuId") Long skuId){
        SkuInfoEntity skuInfo = skuInfoService.getById(skuId);
        return R.ok().setData(skuInfo);
    }
相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
6月前
|
NoSQL 前端开发 Redis
《我们一起进大厂》系列-秒杀系统设计
《我们一起进大厂》系列-秒杀系统设计
137 2
|
5月前
|
XML 前端开发 JavaScript
视频弹幕设计网站02-----数据库设计与后端配置
视频弹幕设计网站02-----数据库设计与后端配置
|
5月前
|
前端开发 数据库 索引
前后端分离------后端创建笔记(05)用户列表查询接口(下)
前后端分离------后端创建笔记(05)用户列表查询接口(下)
|
4月前
|
前端开发 API
支付系统27-------梳理一下支付按钮,前端的代码
支付系统27-------梳理一下支付按钮,前端的代码
|
6月前
|
小程序
外卖小程序-购物车模块表结构设计和后端代码
外卖小程序-购物车模块表结构设计和后端代码
58 0
|
6月前
|
存储 数据库 UED
秒杀系统数据库设计核心要点详解
秒杀系统数据库设计核心要点详解
228 1
|
6月前
|
缓存 前端开发 JavaScript
若依框架中的权限控制逻辑 ---- 菜单
若依框架中的权限控制逻辑 ---- 菜单
601 0
|
缓存 运维 NoSQL
秒杀服务-----功能实现逻辑1
秒杀服务-----功能实现逻辑
128 0
|
缓存 NoSQL Redis
订单服务-----功能实现逻辑1
订单服务-----功能实现逻辑
89 0
|
JavaScript Java 数据库
订单服务-----功能实现逻辑2
订单服务-----功能实现逻辑2
201 0