这是我参与「第五届青训营 」笔记创作活动的第7天
内容来自字节内部课:【实践课】手把手教你做系统设计,手把手教你做系统设计之秒杀系统.pptx
一、本堂课重点内容:
- 系统设计方法论
- 电商秒杀业务介绍
- 课程实践
- 课程总结
二、详细知识点介绍:
1. 引言
系统设计的问题:
- 为什么要做系统设计
- 系统设计的定义是什么
- 怎么做系统设计,如何落地一个系统
- 系统功能实现后如何分析瓶颈并优化
- 如何验证系统的可用性和稳定性
系统设计的定义:
- 系统:关联的个体,规则运作,组成工作的个体
- 设计:设想和计划,目的,过程安排
定义:为了达成某种目的,通过个体组成整体的过程
如何做系统设计(4s分析法):
- 场景分析( Scenario ):什么系统,需要哪些功能,多大的并发量
- 存储设计( Storage )数据如何组织,Sql存储,NoSql存储
- 服务设计( Service ):业务功能实现和逻辑整合
- 可扩展性( Scale ):解决设计缺陷,提高鲁棒性、扩展性
如何发现系统的瓶颈:
- 火焰图分析
- 链路追踪
- 性能测试
如何保证可用性和稳定性:
- 链路梳理:
- 核心链路
- 流量漏斗(eg.购物车10个商品只下单两个)
- 强弱依赖(弱依赖可降级,强依赖不可)
- 可观测性(观测指标):
- 链路追踪(全链路微服务追踪)
- 核心监控
- 业务报警
- 全链路测试:
- 压力测试
- 负载测试
- 容量测试(eg.双十一)
- 稳定性控制:
- 系统限流
- 业务兜底
- 熔断降级
- 容灾演练:
- 混沌工程(故障注入,演练)
- 应急手册
- 容灾预案(机房故障等)
2. 电商和秒杀
电商要素:
- 人-消费者侧:消费者,用户,流量来源
- 货-供给侧:商品,商家,供应链
- 场-交易环境:线下商场,线上电商
电商介绍:
商品:具有交易价值和属性的信息载体
- SPU: Standard Product Unit (标准化管理)
- SKU: Stock Keeping Unit (库存保存单元,具有交易属性)
秒杀业务的特点:瞬时流量高;读多写少;实时性要求高
秒杀的挑战:资源成本(资源有限);反欺诈;高性能;防止超卖;流量管控(对无用流量拦截、过滤);扩展性;鲁棒性(系统稳定性、可用性)
3. 设计秒杀系统
场景(Scenario)
- 功能:秒杀活动发布;秒杀商品详情;秒杀下单
- 并发:万人参与秒杀;QPS 1w+;TPS 1k+
存储(Storage) 三级存储:MySQL->Redis->LocalCache(比Redis性能更高)
服务(Service)
- 子服务:用户服务;风控服务;活动服务;订单服务
- 基础组件:ID生成器(生成订单ID——分布式ID生成);缓存组建;MQ组件(保护系统——削峰);限流组件(保护系统)
扩展:
- 流量隔离(与常规流量合理)
- CDN(静态资源缓存,提高效率)
- 缓存优化
- 流量管控(拦截、过滤)
- 数据库扩展
- 服务水平扩展(负载均衡、反向代理)
- MQ扩展(主从架构、多主多从架构)
- Redis扩展
- 服务垂直扩展
系统架构图
4. 实践
秒杀流程图:
重要代码部分:
src/main/java/com/camp/promotion/service/impl/HPromotionServiceImpl.java
public List<CreatePromoProductModel> CreatePromoActivity(CreateActivityModel createActivityModel)
public HPromoProductModel getPromotionProductDetail(Long promoId, Long skuId, Long spuId)
src/main/java/com/camp/promotion/lock/RedisDistributedLock.java
public void unlock() {
// 为了保证可靠性采用lua脚本
// 在父类中定义了close方法——自动解锁
Long result;
...
src/main/java/com/camp/promotion/controller/CategoryController.java
public ResponseData<?> testLock() {
// 把锁的创建放在try后,不用写finally了,自动调用close方法
try (RedisDistributedLock lock = new RedisDistributedLock("test_key", 10000)) {
if (lock.tryLock()) {
return ResponseData.Success("ok");
}
} catch (Exception e) {
throw new RuntimeException("lock fail");
}
...
src/main/java/com/camp/promotion/mq/OrderProducer.java
public boolean send(HOrder order) {
byte[] body = JSON.toJSONBytes(order); // 想提升性能可用protobuff(序列化方式)
Message message = new Message(topicName, "create_order" + order.getId(), body);
src/main/java/com/camp/promotion/service/IdGenerateService.java
public synchronized long generateId() {
...
// ID生成器,雪花算法,性能很高
/*
* (currentMilliseconds - EPOCH) << TIMESTAMP_LEFT_SHIFT_BITS) 时间毫秒数往右移动22位,避开workId和sequence,放到高位的41位
* (getWorkerId() << WORKER_ID_LEFT_SHIFT_BITS) 将workId左移到随后的10位上
* 最后是sequence占据低位的12位
*
* 最后用或运算将三个部分组合到一个long中,返回最终的结果
*/
return ((currentMilliseconds - EPOCH) << TIMESTAMP_LEFT_SHIFT_BITS) | (getWorkerId() << WORKER_ID_LEFT_SHIFT_BITS) | sequence;
}
src/main/java/com/camp/promotion/service/CacheService.java
public HPromoProductModel getVal(String key) {
...
} else {
// 如果qps非常高,这一步会成为瓶颈,那就没必要做了
String stockKey = Constant.generatePromoStockKey(promoId, skuId, spuId);
// 从redius中再获取,因为库存是实时的数据
Integer stock = (Integer) redisService.getVal(stockKey);
if (stock != null) {
model.getSkuModel().setPromoStock(stock);
}
}
return model;
}
src/main/java/com/camp/promotion/mq/OrderPullConsumer.java
public void doConsumer() throws InterruptedException {
while (isRunning.get()) {
...
executorService.execute(() -> {
try {
// 线程池里做异步消费
processOrder(messageExts);
} catch (Exception e) {
log.error("process order fail, e = ", e);
}
});
...
}
}
@Transactional(rollbackFor = Exception.class)
public void processOrder(List<MessageExt> messageExts) {
...
// 真正从数据库中扣减
int updateStockRes = this.hPromoProductService.decreaseStock(orders.get(0).getActivityId(), orders.get(0).getSkuId(), decreaseCount);
if (updateStockRes < 1) {
log.error("process order fail, size = {}, data = {}", orders.size(), JSON.toJSONString(orders));
this.deadQueue.addAll(orders);
throw new BizException(ResponseEnum.CONSUMER_FAIL);
}
// 批量插入(优化了性能)
int insertRes = this.hOrderService.batchInsert(orders);
if (insertRes < orders.size()) {
log.error("process order fail, size = {}, data = {}", orders.size(), JSON.toJSONString(orders));
this.deadQueue.addAll(orders);
throw new BizException(ResponseEnum.CONSUMER_FAIL);
}
}
服务器操作:
看redis是不是单线程的:
ps -ef | grep redis
top -HP 1076955
jmeter:简单压测工具
思考题:
- 发起订单请求100个,数据库中库存剩1,redis中为0,为什么?
- 我们在业务中用MQ做了缓存,MQ中用户消费成功,但订单服务还没有这个消息,若此时用户立马去查看或取消该订单(在业务逻辑中还没有处理),我们如何解决该问题?
总结:
- 服务无状态:当前应用服务不存储状态、数据
- 批量写入:上面的项目中消费者采用了
- 最终一致性:解释了用redis等技术产生了数据副本,但仍有最终一致性