从零开始搭建一个电商项目-秒杀(八)

  1. 前言

    电商项目最能体现出技术要求的就是秒杀功能了,可以全方位的体现出后端的技术栈。如分布式锁、消息队列、分布式事务、缓存、限流等。秒杀也是电商项目中的一大特色,本电商项目中首先在秒杀的前几天,通过定时任务拉取需要秒杀的商品,将商品信息保存到缓存中。然后前端用户可以获取即将要秒杀的产品,在秒杀开始时进行秒杀。

  2. 定时拉取

    通过定时任务定期获取需要秒杀的商品信息,秒杀的商品信息在平台端维护。如下图,添加秒杀场次。直接使用了生成器的生成的修改接口,前端页面可以优化启用状态为开关样式,此后不在累赘,本项目只是学习技术,这些细节的东西,由于没有具体的业务需求并且只是本人学习的产物,并没有商业化一般的美观。
    image.png
    新增场次的关联商品
    image.png
    定时拉取3天后的秒杀数据,第一步首先保存要秒杀的商品信息到Redis,Key为开始时间和结束时间,Value为秒杀Id-SkuId。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private void saveSessionInfos(List<SeckillSessionWithSkusVo> sessions) {

sessions.forEach(session -> {

//获取当前活动的开始和结束时间的时间戳
long startTime = session.getStartTime().getTime();
long endTime = session.getEndTime().getTime();

//存入到Redis中的key
String key = SESSION__CACHE_PREFIX + startTime + "_" + endTime;

//判断Redis中是否有该信息,如果没有才进行添加
Boolean hasKey = redisTemplate.hasKey(key);
//缓存活动信息
if (hasKey != null && !hasKey) {
//获取到活动中所有商品的skuId
List<String> skuIds = session.getRelationSkus().stream()
.map(item -> item.getPromotionSessionId() + "-" + item.getSkuId().toString()).collect(Collectors.toList());
redisTemplate.opsForList().leftPushAll(key, skuIds);
}
});

}

Redis中保存的数据结构:
image.png
第二步,需要将秒杀的商品信息保存到Redis中。
1.生成随机码。为了防止在秒杀开始的时候,一些技术人员可以通过一些渠道知道即将秒杀的URL,那样的话就可以写程序去自动秒杀。所以需要在秒杀的时候生成一个随机码,随机码在开始秒杀的时候才会获取到。
2.缓存商品信息,并将随机码一并保存到Redis。其中Key为Id-SkuId,也就是上一步保存的秒杀信息的Value,这样可以更快的拿到需要秒杀的商品信息。Value就是商品信息。
3.保存库存到Redis中。使用分布式锁,设置信号量,即保存商品的库存。其中Kesy为秒杀需要的随机码,Value为库存数量。秒杀服务一般会部署多个服务,所以要以分布式锁的方式设置库存数据。一般来说还需要在保存秒杀信息以及保存秒杀的商品信息的外层添加分布式锁即可,我这里没有加,需要根据相应的情境去考虑需不需要分布式锁的参与。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
private void saveSessionSkuInfo(List<SeckillSessionWithSkusVo> sessions) {

sessions.forEach(session -> {
//准备hash操作,绑定hash
BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
session.getRelationSkus().forEach(seckillSkuVo -> {
//生成随机码
String token = UUID.randomUUID().toString().replace("-", "");
String redisKey = seckillSkuVo.getPromotionSessionId().toString() + "-" + seckillSkuVo.getSkuId().toString();
Boolean has;
if ((has = operations.hasKey(redisKey)) != null && !has) {

//缓存我们商品信息
SeckillSkuRedisTo redisTo = new SeckillSkuRedisTo();
Long skuId = seckillSkuVo.getSkuId();
//1、先查询sku的基本信息,调用远程服务
R info = productFeignService.getSkuInfo(skuId);
if (R.isOk(info)) {
SkuInfoVo skuInfo = JSON.parseObject(JSON.toJSONString(info.get("skuInfo")), new TypeReference<SkuInfoVo>() {
});
redisTo.setSkuInfo(skuInfo);
}

//2、sku的秒杀信息
BeanUtils.copyProperties(seckillSkuVo, redisTo);

//3、设置当前商品的秒杀时间信息
redisTo.setStartTime(session.getStartTime().getTime());
redisTo.setEndTime(session.getEndTime().getTime());

//4、设置商品的随机码(防止恶意攻击)
redisTo.setRandomCode(token);

//序列化json格式存入Redis中
String seckillValue = JSON.toJSONString(redisTo);
operations.put(seckillSkuVo.getPromotionSessionId().toString() + "-" + seckillSkuVo.getSkuId().toString(), seckillValue);

//如果当前这个场次的商品库存信息已经上架就不需要上架
//5、使用库存作为分布式Redisson信号量(限流)
// 使用库存作为分布式信号量
RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
// 商品可以秒杀的数量作为信号量
semaphore.trySetPermits(seckillSkuVo.getSeckillCount());
}
});
});
}

Redis中保存的商品信息的数据结构:
image.png
Redis中保存的库存的数据结构
image.png

  1. 获取信息

    一般的,用户需要在商城的页面上能看见即将要秒杀的商品信息。访问商城主页,即可看到需要秒杀的商品信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public List<SeckillSkuRedisTo> getCurrentSeckillSkus() {
long nowTime = new Date().getTime();
Set<String> keys = redisTemplate.keys(SESSION__CACHE_PREFIX + "*");
if (keys == null) {
return null;
}

for (String key : keys) {
String[] s = key.replace(SESSION__CACHE_PREFIX, "").split("_");
long startTime = Long.parseLong(s[0]);
long endTime = Long.parseLong(s[1]);

if (startTime <= nowTime && endTime >= nowTime) {
List<String> skuKeys = redisTemplate.opsForList().range(key, -100, 100);
BoundHashOperations<String, String, String> operations = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
if (skuKeys != null && skuKeys.size() > 0) {
List<String> skus = operations.multiGet(skuKeys);
if (skus != null) {
return skus.stream().map(item -> JSON.parseObject(item, new TypeReference<SeckillSkuRedisTo>() {
})).collect(Collectors.toList());
}
}
break;
}
}
return null;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public SeckillSkuRedisTo getSkuSeckilInfo(Long skuId) {
BoundHashOperations<String, String, String> operations = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
Set<String> keys = operations.keys();
if (keys == null) {
return null;
}
String pattern = "\\d-" + skuId;
long nowTime = new Date().getTime();
for (String key : keys) {
if (key.matches(pattern)) {
Object o = operations.get(key);
SeckillSkuRedisTo seckillSkuRedisTo = JSON.parseObject((String) o, SeckillSkuRedisTo.class);
if (seckillSkuRedisTo != null) {
Long endTime = seckillSkuRedisTo.getEndTime();
Long startTime = seckillSkuRedisTo.getStartTime();
if (startTime <= nowTime && endTime >= nowTime) {

} else {
seckillSkuRedisTo.setRandomCode(null);
}
return seckillSkuRedisTo;
}

break;
}
}
return null;
}

image.png

  1. 开始秒杀

    秒杀信息都准备好之后,就要开始秒杀了。首先前端需要传秒杀Id-SkuId、秒杀时需要的随机码以及要秒杀的数量。首先通过秒杀的Id来获取要秒杀的商品信息,判断时间是否在秒杀区间中,判断随机码是否正确,判断秒杀的数量是否登录秒杀信息中每个用户规定的数量。并且判断改用户是否已经秒杀过了,若秒杀过则拒绝。然后符合条件的用户,通过分布式信号量锁获取秒杀的库存,然后发送MQ消息给订单服务让其下单,同时返回秒杀成功给用户。这里的一系列操作都在缓存中进行,没有使用到数据库,所以用户秒杀很快。秒杀下单完成之后,用户进行支付。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public String kill(String killId, String key, Integer num) {
BoundHashOperations<String, String, String> operations = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
UserLoginResp userLoginResp = LoginInterceptor.threadLocal.get();
String seckillInfo = operations.get(killId);
if (StringUtils.isNotBlank(seckillInfo)) {
SeckillSkuRedisTo seckillSkuRedisTo = JSON.parseObject(seckillInfo, SeckillSkuRedisTo.class);
long startTime = seckillSkuRedisTo.getStartTime();
long endTime = seckillSkuRedisTo.getEndTime();
long nowTime = new Date().getTime();
//时间有效
if (startTime <= nowTime && endTime >= nowTime) {
String randomCode = seckillSkuRedisTo.getRandomCode();
Long promotionSessionId = seckillSkuRedisTo.getPromotionSessionId();
Long skuId = seckillSkuRedisTo.getSkuId();
String reqKey = promotionSessionId + "-" + skuId;
Integer seckillCount = seckillSkuRedisTo.getSeckillCount();
//随机码效验
if (randomCode.equals(key) && reqKey.equals(killId) && num <= seckillCount) {
//判断用户是否已经秒杀
Long id = userLoginResp.getId();
Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(id + "-" + reqKey, num.toString(), endTime - nowTime, TimeUnit.MILLISECONDS);
if (aBoolean == null || aBoolean) {
//分布式事务获取个数
RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + key);

boolean b = semaphore.tryAcquire(num);
if (b) {
String timeId = IdWorker.getTimeId();
SeckillOrderTo seckillOrderTo = new SeckillOrderTo();
seckillOrderTo.setOrderSn(timeId);
seckillOrderTo.setMemberId(id);
seckillOrderTo.setNum(num);
seckillOrderTo.setPromotionSessionId(promotionSessionId);
seckillOrderTo.setSeckillPrice(seckillSkuRedisTo.getSeckillPrice());
seckillOrderTo.setSkuId(skuId);
rabbitMessagingTemplate.convertAndSend(OrderMqConstant.MODULE + OrderMqConstant.EXCHANGE,
OrderMqConstant.MODULE + OrderMqConstant.LINE + OrderMqConstant.SECKILL,
seckillOrderTo);
return timeId;
}

}
}
}

}

return null;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component
@Slf4j
@RabbitListener(queues = OrderMqConstant.SECKILL_KEYWORDS + OrderMqConstant.queue)
public class SeckillMegListener {

@Autowired
private OrderService orderService;


@RabbitHandler
public void seckill(SeckillOrderTo seckillOrderTo, Message message, Channel channel) throws IOException {
log.info("----收到秒杀下单消息 " + seckillOrderTo.toString());

orderService.seckill(seckillOrderTo);
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);

}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void seckill(SeckillOrderTo seckillOrderTo) {

OrderEntity orderEntity = new OrderEntity();
orderEntity.setOrderSn(seckillOrderTo.getOrderSn());
orderEntity.setMemberId(seckillOrderTo.getMemberId());
orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
BigDecimal seckillPrice = seckillOrderTo.getSeckillPrice();
Integer num = seckillOrderTo.getNum();
BigDecimal amount = seckillPrice.multiply(BigDecimal.valueOf(num));
orderEntity.setPayAmount(amount);
this.save(orderEntity);
OrderItemEntity orderItemEntity = new OrderItemEntity();
orderItemEntity.setSkuId(seckillOrderTo.getSkuId());
orderItemEntity.setRealAmount(amount);
orderItemEntity.setOrderSn(seckillOrderTo.getOrderSn());
orderItemEntity.setSkuQuantity(num);
orderItemEntity.setSkuPrice(seckillPrice);
orderItemService.save(orderItemEntity);
}
  1. 总结

    上面就行整个秒杀流程。首先我们需要将需要秒杀的商品信息通过定时任务拉取到缓存中,然后用户就可以查看到可以秒杀的商品信息。这里为了防止内部人员或者技术人员通过链接秒杀,可以创建一个随机码,只有在秒杀开始的时候才能通过这个随机码来进行秒杀。之后用户进行秒杀,判断时间、随机码、秒杀数量等条件是否符合,然后使用消息队列减缓高峰带来的压力,之后订单服务收到消息,进行下单,用户支付订单即可。
    秒杀流程也存在一些问题,若发送下单消息给订单服务,此时订单服务都挂了,那么用户就无法进行支付,压根就没有订单产生。所以需要根据需求的具体情况,以及场景的情况进行相应的改进。

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!

请我喝杯咖啡吧~

支付宝
微信