1. 说在前头
大家好,我是方圆
。到了这篇就是要写到秒杀的下单动作
了,之后大部分的性能优化都将围绕着它来展开。废话不说了,列下订单模块的功能
- 下单核心功能
- 取消订单
- 根据ID获取秒杀订单明细
- 多条件查询秒杀订单
在这里还是标记一下GitHub代码库,以base-function分支
为准,2,3功能就是CRUD,请将主要精力放在秒杀下单功能
上,我在后文中也会解释一下其中的细节,方便大家参考
另外,在此也留下秒杀订单表的DDL语句,所有相关的SQL语句在项目environment的mysql文件夹下
CREATE TABLE IF NOT EXISTS flash_sale.`flash_order` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '秒杀订单ID',
`item_id` bigint(20) NOT NULL COMMENT '秒杀品ID',
`activity_id` bigint(20) NOT NULL COMMENT '秒杀活动ID',
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
`item_title` varchar(50) NOT NULL COMMENT '秒杀品名称',
`flash_price` bigint(20) NOT NULL COMMENT '秒杀价',
`quantity` int(11) NOT NULL COMMENT '数量',
`total_amount` bigint(20) NOT NULL COMMENT '总价格',
`status` tinyint(2) NOT NULL COMMENT '订单状态 10-已创建 20-已取消',
`modified_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `flash_order_user_id_idx` (`user_id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '秒杀订单表';
2. FlashOrderController
- 仍然是将Controller中的调用入口放在这里,供大家参考,具体源码仍然是以项目代码为准,我们在第3节中解释一下秒杀功能实现的细节
@RestController
public class FlashOrderController {
@Resource
private FlashOrderAppService flashOrderAppService;
/**
* 秒杀商品下单
*
* @param request 秒杀请求
* 用户ID 这里我们并没有创建用户管理系统,所以仅以此来作为用户唯一标识
*/
@ApiOperation(value = "秒杀商品下单")
@PostMapping("/flash-orders/placeOrder")
public <T> SingleResponse<T> placeOrder(@RequestBody FlashOrderPlaceRequest request) {
FlashPlaceOrderCommand command = FlashOrderConvertor.toCommand(request);
AppResult<T> appResult = flashOrderAppService.placeOrder(request.getUserId(), command);
return ResponseConvertor.with(appResult);
}
@ApiOperation(value = "取消秒杀订单")
@PutMapping("/flash-orders/{orderId}/cancelOrder")
@ApiImplicitParam(name = "orderId", value = "秒杀订单ID", dataTypeClass = Long.class)
public <T> SingleResponse<T> cancelOrder(@PathVariable Long orderId) {
AppResult<T> appResult = flashOrderAppService.cancelOrder(orderId);
return ResponseConvertor.with(appResult);
}
@PostMapping("/flash-orders")
@ApiOperation(value = "多条件查询秒杀订单")
public SingleResponse<List<FlashOrderResponse>> getFlashOrders(@RequestBody FlashOrderQueryRequest request) {
FlashOrderQuery flashOrderQuery = FlashOrderConvertor.toQuery(request);
return ResponseConvertor.with(flashOrderAppService.getFlashOrders(flashOrderQuery));
}
}
3. 秒杀的实现细节
- 首先,我在APP层创建了一个
PlaceOrderService
,专门来执行秒杀的具体动作,如下
public interface PlaceOrderService {
/**
* 实际下单执行的动作
*
* @param userId 用户ID
*/
<T> AppResult<T> doPlaceOrder(Long userId, FlashPlaceOrderCommand command);
}
- 我们看具体的实现类,具体的执行步骤我都标记在代码注释中了
@Override
@Transactional
public <T> AppResult<T> doPlaceOrder(Long userId, FlashPlaceOrderCommand command) {
log.info("doPlaceOrder|开始下单|{}, {}", userId, JSON.toJSONString(command));
// 校验秒杀活动、秒杀时间条件
checkPlaceOrderCondition(command.getActivityId(), command.getItemId());
// 扣减库存
decreaseItemStock(command.getItemId(), command.getQuantity());
// 初始化秒杀订单信息
FlashOrder flashOrder = initialFlashOrderInfo(userId, command);
// 秒杀订单入库
flashOrderDomainService.doPlaceOrder(flashOrder);
log.info("doPlaceOrder|下单成功");
return AppResult.success();
}
- 值得关注的是使用了@Transactional注解,让Spring为我们管理这个
事务
,其中有两个修改数据库的动作,扣减库存
和秒杀订单入库
,两条SQL必须都要执行正确才行,哪有说扣减库存成功而下单失败的?
- 这个秒杀动作做了四件事,
校验条件
、扣减库存
、初始化订单信息
和秒杀订单入库
其中的业务逻辑很简单,大家看看源代码就能很好的理解,我最想提的一点是:在秒杀订单入库的时候,采用了雪花算法
生成秒杀订单的ID
- 我为此专门写了一个雪花算法的工具类,如下
@Slf4j
public class SnowflakeIdUtil {
// 下面两个每个5位,加起来就是10位的工作机器id
private static final long WORKER_ID;
private static final long DATACENTER_ID;
// 长度为5位
private static final long WORKER_ID_BITS = 5L;
private static final long DATACENTER_ID_BITS = 5L;
// 12位的序列号
private static long SEQUENCE;
// 序列号id长度
private static final long SEQUENCE_BITS = 12L;
// 序列号最大值
private static final long SEQUENCE_MAX = ~(-1L << SEQUENCE_BITS);
// 工作id需要左移的位数,12位
private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;
// 数据id需要左移位数 12+5=17位
private static final long DATACENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
//时间戳需要左移位数 12+5+5=22位
private static final long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATACENTER_ID_BITS;
// 初始时间戳
private static final long ORIGINAL_TIME_STAMP = 1288834974657L;
// 上次时间戳,初始值为负数
private static long LAST_TIMESTAMP = -1L;
static {
Random random = new Random(1);
// workerId can't be greater than ~(-1L << workerIdBits) or less than 0
WORKER_ID = random.nextInt(32);
// datacenterId can't be greater than ~(-1L << datacenterIdBits) or less than 0
DATACENTER_ID = 1;
SEQUENCE = 1;
log.info("Snowflake starting. timestamp left shift {}, datacenter id bits {}, worker id bits {}, " +
"sequence bits {}, worker id {}",
TIMESTAMP_LEFT_SHIFT, DATACENTER_ID_BITS, WORKER_ID_BITS, SEQUENCE_BITS, WORKER_ID);
}
/**
* ID生成算法
*/
public static synchronized long nextId() {
long currentTimestamp = System.currentTimeMillis();
// 获取当前时间戳如果小于上次时间戳,则表示时间戳获取出现异常
if (currentTimestamp < LAST_TIMESTAMP) {
log.error("clock is moving backwards. Rejecting requests until {}.", LAST_TIMESTAMP);
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds",
LAST_TIMESTAMP - currentTimestamp));
}
// 获取当前时间戳如果等于上次时间戳(同一毫秒内),则在序列号加一;否则序列号赋值为0,从0开始。
if (LAST_TIMESTAMP == currentTimestamp) {
SEQUENCE = (SEQUENCE + 1) & SEQUENCE_MAX;
if (SEQUENCE == 0) {
currentTimestamp = tilNextMillis(LAST_TIMESTAMP);
}
} else {
SEQUENCE = 0;
}
// 将上次时间戳值刷新
LAST_TIMESTAMP = currentTimestamp;
/*
* 返回结果:
* (timestamp - ORIGINAL_TIME_STAMP) << TIMESTAMP_LEFT_SHIFT) 表示将时间戳减去初始时间戳,再左移相应位数
* (DATACENTER_ID << DATACENTER_ID_SHIFT) 表示将数据id左移相应位数
* (WORKER_ID << WORKER_ID_SHIFT) 表示将工作id左移相应位数
* | 是按位或运算符,例如:x | y,只有当x,y都为0的时候结果才为0,其它情况结果都为1。
* 因为个部分只有相应位上的值有意义,其它位上都是0,所以将各部分的值进行 | 运算就能得到最终拼接好的id
*/
return ((currentTimestamp - ORIGINAL_TIME_STAMP) << TIMESTAMP_LEFT_SHIFT)
| (DATACENTER_ID << DATACENTER_ID_SHIFT)
| (WORKER_ID << WORKER_ID_SHIFT)
| SEQUENCE;
}
// 获取时间戳,并与上次时间戳比较
private static long tilNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
}
- 具体雪花算法的生成的ID的原理参考博客:理解分布式id生成算法SnowFlake
- 我在秒杀订单入库之前,使用了这个工具类生成了订单ID,如下
public void save(FlashOrder flashOrder) {
FlashOrderDO flashOrderDO = FlashOrderConvertor.toDataObject(flashOrder);
// 雪花算法生成ID
flashOrderDO.setId(SnowflakeIdUtil.nextId());
flashOrderMapper.insert(flashOrderDO);
}
- 那为什么使用雪花算法?
- 它所有生成的ID按时间趋势递增,索引效率高
- 在分布式系统中,不会产生重复的ID(在工具类里WORKER_ID是用随机数来区分不同的服务器的)
- 不依赖数据库生成,高性能高可用
- 对于秒杀系统,它能够每秒生成数百万的自增ID,解决自增生成ID的问题
4. 基础功能的完结
下单功能中的细节,我觉得值得说的也就如上所述。
这篇博客完稿之后,基础功能部分就已经完结了,接下来的几篇将主要对秒杀系统进行优化,那才是重中之重!
收!