Java系统设计知识整理《1》

前言

  1. 如何设计一个高并发秒杀系统?(CDN + 限流 + Redis 扣库存 + MQ 削峰)
  2. 分布式 ID 生成方案?(Snowflake、号段模式、UUID 优化)
  3. 如何设计短链系统?(自增 ID + 62 进制 + 布隆过滤器)
  4. 接口幂等性如何保证?(Token 机制、唯一索引、状态机)

1、如何设计一个高并发秒杀系统?

核心挑战是 瞬时高并发(10w+ QPS) + 防超卖 + 系统稳定性 。设计原则:层层拦截、异步解耦、多级缓存

如下所示:

一、整体架构(分层设计)秒杀

rust 复制代码
[用户] 
  ↓
[CDN] → 静态页面缓存(HTML/JS/CSS)
  ↓
[Nginx/OpenResty] → IP/用户限流 + 答题验证
  ↓
[API 网关] → 熔断降级 + 用户鉴权
  ↓
[秒杀服务] → Redis+Lua 扣库存 + 发 MQ
  ↓
[订单服务] ← MQ 消费 → 创建订单 + DB 落库
  ↓
[前端轮询/WebSocket] ← 通知结果

二、关键环节详解

  1. 前端层:静态化 + 按钮控制
  • CDN 缓存:秒杀页面、JS、CSS 全部缓存,减少后端请求;

  • 按钮置灰:秒杀开始前禁用,开始后启用;

  • 合并请求:下单 + 查询库存合并为一个接口。

  1. 接入层:限流 + 防刷

  2. 服务层:原子扣库存

库存预热:秒杀开始前,将库存加载到 Redis:

  • Nginx 限流

    Lua 复制代码
    limit_req_zone $binary_remote_addr zone=ip:10m rate=10r/s;
    limit_req_zone $http_x_user_id zone=user:10m rate=2r/s;
  • 答题/验证码:防止机器人刷单;

  • IP 黑名单:实时封禁高频 IP。

Lua 复制代码
SET stock:1001 100

Lua 脚本保证原子性

Lua 复制代码
-- 扣减库存
if redis.call('GET', KEYS[1]) >= tonumber(ARGV[1]) then
    return redis.call('DECRBY', KEYS[1], ARGV[1])
else
    return -1
end
  • 先扣库存,再发 MQ:避免超卖。
  1. 异步解耦:MQ 削峰
  • 扣库存成功后,发送消息到 RocketMQ/Kafka

  • 后台消费者异步创建订单、扣减 DB 库存;

  • 前端通过轮询 or WebSocket 获取结果

  1. 数据层:多级缓存 + 最终一致性
  • 本地缓存(Caffeine):缓存商品信息;

  • Redis:存储库存、用户资格;

  • MySQL:最终持久化,通过 MQ 保证一致性。

  1. 稳定性保障
  • 熔断降级:库存服务不可用时,返回"稍后再试";

  • 独立部署:秒杀服务与主站物理隔离;

  • 压测预案:提前全链路压测,准备扩容方案。

三、超时处理

  • 延迟队列:订单创建后,加入 Redis ZSet(score=过期时间);

  • 定时任务:每秒扫描 ZSet,释放未支付库存(同样用 Lua 原子操作)。

四、最佳实践

  • 提到 热点 Key 优化:分库存(stack_1,stack_2...) + 本地缓存兜底;

  • 举例:某电商秒杀,QPS 从 2k 提升到 8w,超卖率为 0;

  • 强调:秒杀的核心是"库存扣减原子性 + 请求拦截"

2、分布式 ID 生成方案?

分布式 ID 需满足:全局唯一、趋势递增、高性能、高可用

1、主流方案对比

方案 原理 优点 缺点 适用场景
UUID 128 位随机 全球唯一、无中心 无序、长(36 字符) 日志 ID、非 DB 主键
Snowflake 时间戳 + 机器 ID + 序列号 趋势递增、高性能 依赖时钟、需分配 workerId 订单 ID、消息 ID
号段模式 DB 预取一段 ID 高性能、DB 依赖低 有 DB 依赖 用户 ID、业务 ID
ULID 时间戳 + 随机 有序、兼容 UUID 新方案,生态少 替代 UUID

二、详细方案

  1. Snowflake(雪花算法)
  • 结构(64 位):

    • 1 bit:符号位(固定 0)

    • 41 bit:时间戳(毫秒,可用 69 年)

    • 10 bit:机器 ID(1024 节点)

    • 12 bit:序列号(4096/毫秒)

  • 问题时钟回拨 → ID 重复;

  • 解决方案

    • 等待时钟追上;

    • 抛异常(美团 Leaf);

    • 使用 NTP 服务 保证时钟同步。

  1. 号段模式(Segment)
  • 原理

    • 从 DB 获取一段 ID(如 [1000, 2000));

    • 缓存在内存中分配;

    • 用完再取。

  • 优势

    • 减少 DB 访问(1 次 DB → 1000 个 ID);

    • 性能高(≈ Snowflake)。

  • 优化双 buffer(用到 80% 时异步加载下一段)。

  1. UUID 优化:ULID
  • 结构:48 位时间戳 + 80 位随机;

  • 优点

    • 字符串有序(按时间排序);

    • 兼容 UUID 格式(26 字符);

    • 无中心。

三、工业级方案

  • 美团 Leaf:支持号段模式 + Snowflake;

  • 百度 uid-generator:基于 Snowflake,解决时钟回拨;

  • 滴滴 TinyID:基于号段,提供 HTTP 接口。

四、最佳实践

  • 提到 ZooKeeper 分配 workerId(Snowflake);

  • 举例:用号段模式生成用户 ID,QPS 达 50w+;

  • 强调:Snowflake 适合趋势递增场景,号段模式适合强顺序场景


3、如何设计短链系统?

短链系统目标:**将长 URL 转为短字符串(如 https://t.cn/abc12)。要求:唯一、短、高性能。

如下所示:

一、核心流程

rust 复制代码
[用户] → POST /shorten { "url": "https://..." }
  ↓
[服务] → 1. 生成唯一 ID(自增 or Snowflake)
        2. 转 62 进制 → 短码(如 abc123)
        3. 存 Redis(短码 → 长 URL)
        4. 返回 https://t.cn/abc123
  ↓
[用户] → GET /abc123
  ↓
[服务] → Redis 查询 → 302 重定向

二、关键技术

  1. ID 生成
  • 自增 ID(MySQL):

    • 简单,但单点瓶颈;

    • 可分库分表(user_id % 1024)。

  • Snowflake:无 DB 依赖,趋势递增。

  1. 62 进制转换
  • 字符集:0-9 + a-z + A-Z(62 个);

  • 算法

    java 复制代码
    public static String toBase62(long id) {
        char[] chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();
        StringBuilder sb = new StringBuilder();
        while (id > 0) {
            sb.append(chars[(int) (id % 62)]);
            id /= 62;
        }
        return sb.reverse().toString();
    }
  • 长度:6 位可支持 568 亿(62^6)。

  • Redis Hash

  1. 存储
  • Redis Hash
java 复制代码
HSET short_urls abc123 "https://original.com"
  • 过期时间EXPIRE short_urls 365d(按需)。
  1. 防重复
  • 布隆过滤器

    • 初始化时加载所有短码;

    • 生成前先 check,避免碰撞。

  • 唯一索引:DB 中 short_code 唯一。

三、高可用设计

  • 缓存穿透:布隆过滤器 + 空值缓存;

  • 热点短链:本地缓存(Caffeine)兜底;

  • 扩容:ID 生成器支持分段。

四、最佳实践

  • 提到 自定义进制 (如 64 进制,加 -_);
  • 举例:短链系统支持 10 亿 URL,6 位短码;
  • 强调:62 进制比 36 进制更短,比 16 进制更紧凑

4、接口幂等性如何保证?

幂等性:同一请求执行多次,结果与执行一次相同 。在分布式系统中,因网络超时、重试、MQ 重投,必须保证幂等。

一、四大方案

  1. Token 机制(防重 Token)
  • 下单前,GET /token → 返回唯一 token;

  • 提交订单时,携带 token;

  • 服务端校验 token(Redis 原子操作):

Lua 复制代码
if (redis.setex("token:" + token, 300, "1", SET_IF_NOT_EXIST)) {
    // 处理业务
} else {
    return "重复提交";
}
  1. 唯一索引

适用:表单提交、支付。

原理:DB 唯一约束防止重复插入;

sql 复制代码
CREATE UNIQUE INDEX uk_order_user ON orders(user_id, product_id);
  1. 状态机

原理:业务有明确状态流转,重复请求因状态不符被忽略;

java 复制代码
if (order.getStatus() == PAID) {
    return; // 已支付,忽略重复
}
  1. 去重表

原理:消息带唯一 ID,消费前查去重表;

sql 复制代码
CREATE TABLE dedup (
    msg_id VARCHAR(64) PRIMARY KEY,
    create_time DATETIME
);
  • 适用:MQ 消费、回调接口。

二、MQ 场景幂等

  • Kafka/RocketMQ:消息可能重复投递;

  • 必须业务层幂等,不能依赖 offset。

三、最佳实践

  • 提到 Redis 原子操作SET token EX 300 NX

  • 举例:支付系统用订单号 + 唯一索引,0 重复扣款;

  • 强调:幂等性是分布式系统的基石,必须前置设计

相关推荐
q***06292 小时前
环境安装与配置:全面了解 Go 语言的安装与设置
开发语言·后端·golang
程序猿七度2 小时前
【Excel导入】读取WPS格式嵌入单元格内的图片
java·开发语言·wps
IoT智慧学堂2 小时前
C语言流程控制:if判断语句全解析
c语言·开发语言
楼田莉子2 小时前
C++/Linux小项目:自主shell命令解释器
linux·服务器·开发语言·c++·后端·学习
用户298698530142 小时前
Java: 为PDF批量添加图片水印实用指南
java·后端·api
EXtreme352 小时前
C语言指针深度剖析(2):从“数组名陷阱”到“二级指针操控”的进阶指南
c语言·开发语言·算法
q***31832 小时前
微服务生态组件之Spring Cloud LoadBalancer详解和源码分析
java·spring cloud·微服务
大头an2 小时前
Spring事务在微服务架构中的实践与挑战
java
BugShare2 小时前
嘿嘿,一个简单ElasticSearch小实现
java·大数据·spring boot·elasticsearch