如何设计抢红包系统
一、问题描述
1.1 业务背景
抢红包是高并发场景的经典案例,广泛应用于:
- 微信红包:春节期间每秒抢红包请求达到百万级
- 支付宝红包:集五福活动
- 电商红包雨:双十一抢红包
核心特点:
- 瞬间高并发(百万QPS)
- 金额随机分配
- 防止超抢、重复抢
- 数据强一致性(钱不能多发也不能少发)
1.2 核心功能
基础功能:
- 发红包:指定总金额和红包个数
- 抢红包:用户抢到随机金额
- 拆红包:查看抢到的金额
- 红包详情:查看红包领取记录
进阶功能:
- 红包过期:24小时未抢完退回
- 防重复抢:同一用户只能抢一次
- 防刷:IP限流、设备指纹
- 红包雨:批量发红包
1.3 技术挑战
高并发抢:
微信红包峰值数据:
- 2014年春晚:1600万次/分
- 2015年春晚:10.1亿次红包
- 2016年春晚:420万次/秒(峰值)防超抢:
红包总数:100个
并发请求:10000个
如何保证只有100人抢到?金额分配公平性:
100元分10个红包
如何保证每人机会均等?
不能前面的人抢光,后面的人没得抢数据一致性:
Redis记录抢到的金额
MySQL记录交易流水
如何保证一致性?1.4 面试考察点
- 红包算法:如何分配金额才公平
- 高并发处理:Redis + Lua原子操作
- 防超抢:库存控制
- 数据一致性:Redis和MySQL一致性
- 防作弊:防重复、防刷
二、红包算法
2.1 二倍均值法(微信红包算法)
原理: 每次抢到的金额在 [0.01, 剩余平均值 × 2] 之间随机。
数学推导:
剩余金额:M
剩余个数:N
剩余平均值:M / N
当前红包金额:random(0.01, M / N × 2)
示例:100元分10个红包
第1个:random(0.01, 100/10×2) = random(0.01, 20) = 15.5元
第2个:random(0.01, 84.5/9×2) = random(0.01, 18.78) = 8.2元
...公平性证明:
每人期望:E = M / N
第1个人期望:
E1 = (0.01 + 2M/N) / 2 ≈ M/N ✅
第2个人期望:
E2 = E[(M - E1) / (N - 1)] = E[M/N] = M/N ✅
数学归纳法证明:每个人期望相同优点:
- ✅ 公平:每人期望相同
- ✅ 随机:金额有大有小,有趣
- ✅ 简单:实现容易
实现:
go
package redenvelope
import (
"fmt"
"math/rand"
"time"
)
// DoubleAverageAlgorithm 二倍均值法
func DoubleAverageAlgorithm(totalAmount int64, totalCount int) []int64 {
rand.Seed(time.Now().UnixNano())
var amounts []int64
remaining := totalAmount * 100 // 转为分
for i := 0; i < totalCount; i++ {
if i == totalCount-1 {
// 最后一个红包:剩余全部金额
amounts = append(amounts, remaining)
} else {
// 剩余平均值
avg := remaining / int64(totalCount-i)
// 随机范围:[1分, 平均值×2]
max := avg * 2
if max > remaining {
max = remaining
}
// 保证至少1分
if max < 1 {
max = 1
}
// 随机金额
amount := rand.Int63n(max-1) + 1
amounts = append(amounts, amount)
remaining -= amount
}
}
return amounts
}
// 示例
func main() {
amounts := DoubleAverageAlgorithm(100, 10) // 100元分10个
fmt.Println("红包金额分配:")
sum := int64(0)
for i, amount := range amounts {
fmt.Printf("第%d个:%.2f元\n", i+1, float64(amount)/100.0)
sum += amount
}
fmt.Printf("总计:%.2f元\n", float64(sum)/100.0)
}
// 输出示例:
// 第1个:15.50元
// 第2个:8.21元
// 第3个:12.33元
// ...
// 总计:100.00元java
public class RedEnvelopeAlgorithm {
/**
* 二倍均值法
*/
public static List<Long> doubleAverage(long totalAmount, int totalCount) {
List<Long> amounts = new ArrayList<>();
long remaining = totalAmount * 100; // 转为分
Random random = new Random();
for (int i = 0; i < totalCount; i++) {
if (i == totalCount - 1) {
// 最后一个:剩余全部
amounts.add(remaining);
} else {
// 剩余平均值
long avg = remaining / (totalCount - i);
// 随机范围:[1分, 平均值×2]
long max = avg * 2;
if (max > remaining) {
max = remaining;
}
if (max < 1) {
max = 1;
}
// 随机金额
long amount = random.nextLong(max - 1) + 1;
amounts.add(amount);
remaining -= amount;
}
}
return amounts;
}
}2.2 其他算法
线段切割法:
0 ------- 100元 -------
在线段上随机N-1个点,切割成N段
每段长度即为红包金额缺点:可能出现0元红包
固定金额法(支付宝):
固定金额:0.01, 0.5, 1, 2, 5, 10元
随机选择优点:简单、高效 缺点:不够随机
三、系统设计
3.1 架构图
mermaid
graph TB
subgraph 客户端
A[用户App]
end
subgraph 接入层
B[Nginx + Lua限流]
end
subgraph 业务层
C[发红包服务]
D[抢红包服务]
end
subgraph 缓存层
E[Redis]
E --> E1[红包池<br/>Hash存储金额]
E --> E2[抢红包记录<br/>Set防重复]
E --> E3[库存计数<br/>String原子递减]
end
subgraph 存储层
F[MySQL]
F --> F1[红包表]
F --> F2[领取记录表]
F --> F3[账户流水表]
end
subgraph 消息队列
G[Kafka]
H[异步写MySQL]
end
A --> B
B --> C
B --> D
C --> E
D --> E
D --> G
G --> H
H --> F3.2 数据库设计
红包表
sql
CREATE TABLE red_envelope (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
envelope_no VARCHAR(32) UNIQUE NOT NULL COMMENT '红包编号',
user_id BIGINT NOT NULL COMMENT '发红包用户ID',
total_amount BIGINT NOT NULL COMMENT '总金额(分)',
total_count INT NOT NULL COMMENT '红包总数',
remaining_amount BIGINT NOT NULL COMMENT '剩余金额(分)',
remaining_count INT NOT NULL COMMENT '剩余个数',
expire_time DATETIME NOT NULL COMMENT '过期时间',
status TINYINT DEFAULT 1 COMMENT '状态 1进行中 2已抢完 3已过期',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
KEY idx_user (user_id),
KEY idx_status_expire (status, expire_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
COMMENT='红包表';领取记录表
sql
CREATE TABLE envelope_record (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
envelope_no VARCHAR(32) NOT NULL COMMENT '红包编号',
user_id BIGINT NOT NULL COMMENT '抢红包用户ID',
amount BIGINT NOT NULL COMMENT '抢到金额(分)',
grab_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '抢红包时间',
UNIQUE KEY uk_envelope_user (envelope_no, user_id),
KEY idx_user (user_id, grab_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
COMMENT='红包领取记录表';3.3 Redis数据结构
redis
# 1. 红包池(List,存储预生成的金额)
Key: envelope:pool:{envelope_no}
Value: [1550, 821, 1233, ...] (单位:分)
TTL: 24小时
# 示例
RPUSH envelope:pool:ENV001 1550 821 1233 ...
LPOP envelope:pool:ENV001 # 抢红包时弹出
# 2. 红包库存(String,原子递减)
Key: envelope:stock:{envelope_no}
Value: {remaining_count}
TTL: 24小时
# 示例
SET envelope:stock:ENV001 100
DECR envelope:stock:ENV001 # 抢红包时递减
# 3. 抢红包记录(Set,防重复抢)
Key: envelope:grabbed:{envelope_no}
Value: {user_id1, user_id2, ...}
TTL: 24小时
# 示例
SADD envelope:grabbed:ENV001 10001
SISMEMBER envelope:grabbed:ENV001 10001 # 检查是否已抢
# 4. 红包详情(Hash)
Key: envelope:info:{envelope_no}
Fields: total_amount, total_count, expire_time
TTL: 24小时
# 5. 用户今日抢红包次数(String,防刷)
Key: user:grab:count:{user_id}:{date}
Value: {count}
TTL: 24小时3.4 核心流程
发红包流程
mermaid
sequenceDiagram
participant U as 用户
participant S as 发红包服务
participant R as Redis
participant M as MySQL
U->>S: 发红包(100元, 10个)
S->>S: 二倍均值法生成10个金额
S->>M: 创建红包记录
S->>R: 存储红包池(List)
S->>R: 设置库存(String)
S->>R: 设置红包详情(Hash)
S-->>U: 返回红包编号抢红包流程(关键)
mermaid
sequenceDiagram
participant U as 用户
participant S as 抢红包服务
participant R as Redis
participant K as Kafka
participant M as MySQL
U->>S: 抢红包(envelope_no, user_id)
S->>R: Lua脚本原子操作
Note over R: 1. 检查是否已抢<br/>2. 检查库存<br/>3. 弹出金额<br/>4. 记录用户已抢
R-->>S: 返回金额
alt 抢到红包
S->>K: 异步写MySQL
K->>M: 写入领取记录
S-->>U: 返回金额
else 未抢到
S-->>U: 红包已抢完/已抢过
end四、核心实现
4.1 Lua脚本(原子操作)
抢红包Lua脚本:
lua
-- KEYS[1]: envelope:pool:{envelope_no}
-- KEYS[2]: envelope:stock:{envelope_no}
-- KEYS[3]: envelope:grabbed:{envelope_no}
-- ARGV[1]: user_id
local function grab_envelope()
-- 1. 检查是否已抢过
local grabbed = redis.call('SISMEMBER', KEYS[3], ARGV[1])
if grabbed == 1 then
return {-1, 0} -- 已抢过
end
-- 2. 检查库存
local stock = redis.call('GET', KEYS[2])
if not stock or tonumber(stock) <= 0 then
return {-2, 0} -- 已抢完
end
-- 3. 弹出金额
local amount = redis.call('LPOP', KEYS[1])
if not amount then
return {-2, 0} -- 红包池为空
end
-- 4. 库存-1
redis.call('DECR', KEYS[2])
-- 5. 记录已抢
redis.call('SADD', KEYS[3], ARGV[1])
return {0, tonumber(amount)} -- 成功
end
return grab_envelope()4.2 Go实现
点击查看完整实现
go
package redenvelope
import (
"context"
"fmt"
"time"
"github.com/go-redis/redis/v8"
"gorm.io/gorm"
)
type RedEnvelopeService struct {
db *gorm.DB
redis *redis.Client
kafkaClient *KafkaClient
}
type RedEnvelope struct {
ID int64 `gorm:"primary_key"`
EnvelopeNo string `gorm:"column:envelope_no"`
UserID int64 `gorm:"column:user_id"`
TotalAmount int64 `gorm:"column:total_amount"`
TotalCount int `gorm:"column:total_count"`
RemainingAmount int64 `gorm:"column:remaining_amount"`
RemainingCount int `gorm:"column:remaining_count"`
ExpireTime time.Time `gorm:"column:expire_time"`
Status int8 `gorm:"column:status"`
CreateTime time.Time `gorm:"column:create_time"`
}
type EnvelopeRecord struct {
ID int64 `gorm:"primary_key"`
EnvelopeNo string `gorm:"column:envelope_no"`
UserID int64 `gorm:"column:user_id"`
Amount int64 `gorm:"column:amount"`
GrabTime time.Time `gorm:"column:grab_time"`
}
// Send 发红包
func (s *RedEnvelopeService) Send(ctx context.Context, userID int64, totalAmount int64, totalCount int) (string, error) {
// 1. 生成红包编号
envelopeNo := generateEnvelopeNo()
// 2. 二倍均值法生成金额
amounts := DoubleAverageAlgorithm(totalAmount, totalCount)
// 3. 创建红包记录
envelope := &RedEnvelope{
EnvelopeNo: envelopeNo,
UserID: userID,
TotalAmount: totalAmount * 100,
TotalCount: totalCount,
RemainingAmount: totalAmount * 100,
RemainingCount: totalCount,
ExpireTime: time.Now().Add(24 * time.Hour),
Status: 1,
CreateTime: time.Now(),
}
err := s.db.Create(envelope).Error
if err != nil {
return "", err
}
// 4. 写入Redis
err = s.initRedisData(ctx, envelopeNo, amounts, totalCount)
if err != nil {
return "", err
}
return envelopeNo, nil
}
// initRedisData 初始化Redis数据
func (s *RedEnvelopeService) initRedisData(ctx context.Context, envelopeNo string, amounts []int64, totalCount int) error {
pipeline := s.redis.Pipeline()
// 1. 红包池(List)
poolKey := fmt.Sprintf("envelope:pool:%s", envelopeNo)
for _, amount := range amounts {
pipeline.RPush(ctx, poolKey, amount)
}
pipeline.Expire(ctx, poolKey, 24*time.Hour)
// 2. 库存(String)
stockKey := fmt.Sprintf("envelope:stock:%s", envelopeNo)
pipeline.Set(ctx, stockKey, totalCount, 24*time.Hour)
// 3. 已抢记录(Set)
grabbedKey := fmt.Sprintf("envelope:grabbed:%s", envelopeNo)
pipeline.Expire(ctx, grabbedKey, 24*time.Hour)
_, err := pipeline.Exec(ctx)
return err
}
// Grab 抢红包
func (s *RedEnvelopeService) Grab(ctx context.Context, envelopeNo string, userID int64) (int64, error) {
// 1. 防刷检查
if !s.checkRateLimit(ctx, userID) {
return 0, fmt.Errorf("抢红包过于频繁")
}
// 2. 执行Lua脚本(原子操作)
amount, err := s.grabByLua(ctx, envelopeNo, userID)
if err != nil {
return 0, err
}
if amount <= 0 {
if amount == -1 {
return 0, fmt.Errorf("已抢过该红包")
}
return 0, fmt.Errorf("红包已抢完")
}
// 3. 异步写MySQL
record := &EnvelopeRecord{
EnvelopeNo: envelopeNo,
UserID: userID,
Amount: amount,
GrabTime: time.Now(),
}
s.kafkaClient.SendEnvelopeRecord(record)
return amount, nil
}
// grabByLua 通过Lua脚本抢红包
func (s *RedEnvelopeService) grabByLua(ctx context.Context, envelopeNo string, userID int64) (int64, error) {
script := `
local grabbed = redis.call('SISMEMBER', KEYS[3], ARGV[1])
if grabbed == 1 then
return {-1, 0}
end
local stock = redis.call('GET', KEYS[2])
if not stock or tonumber(stock) <= 0 then
return {-2, 0}
end
local amount = redis.call('LPOP', KEYS[1])
if not amount then
return {-2, 0}
end
redis.call('DECR', KEYS[2])
redis.call('SADD', KEYS[3], ARGV[1])
return {0, tonumber(amount)}
`
poolKey := fmt.Sprintf("envelope:pool:%s", envelopeNo)
stockKey := fmt.Sprintf("envelope:stock:%s", envelopeNo)
grabbedKey := fmt.Sprintf("envelope:grabbed:%s", envelopeNo)
result, err := s.redis.Eval(ctx, script, []string{poolKey, stockKey, grabbedKey}, userID).Result()
if err != nil {
return 0, err
}
arr := result.([]interface{})
code := arr[0].(int64)
amount := arr[1].(int64)
if code == -1 {
return -1, nil // 已抢过
}
if code == -2 {
return -2, nil // 已抢完
}
return amount, nil
}
// checkRateLimit 防刷检查
func (s *RedEnvelopeService) checkRateLimit(ctx context.Context, userID int64) bool {
date := time.Now().Format("20060102")
key := fmt.Sprintf("user:grab:count:%d:%s", userID, date)
count, _ := s.redis.Incr(ctx, key).Result()
s.redis.Expire(ctx, key, 24*time.Hour)
// 每天最多抢100个红包
return count <= 100
}
// ConsumeEnvelopeRecord 消费红包记录(Kafka Consumer)
func (s *RedEnvelopeService) ConsumeEnvelopeRecord(record *EnvelopeRecord) error {
// 幂等性检查
var exist EnvelopeRecord
err := s.db.Where("envelope_no = ? AND user_id = ?", record.EnvelopeNo, record.UserID).First(&exist).Error
if err == nil {
return nil // 已存在,幂等返回
}
// 插入记录
return s.db.Create(record).Error
}
func generateEnvelopeNo() string {
return fmt.Sprintf("ENV%d", time.Now().UnixNano())
}4.3 Java实现
java
@Service
public class RedEnvelopeService {
@Autowired
private RedEnvelopeMapper envelopeMapper;
@Autowired
private EnvelopeRecordMapper recordMapper;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private KafkaTemplate<String, EnvelopeRecord> kafkaTemplate;
/**
* 发红包
*/
@Transactional
public String send(Long userId, Long totalAmount, Integer totalCount) {
// 1. 生成红包编号
String envelopeNo = "ENV" + System.currentTimeMillis();
// 2. 生成金额
List<Long> amounts = RedEnvelopeAlgorithm.doubleAverage(totalAmount, totalCount);
// 3. 创建红包记录
RedEnvelope envelope = new RedEnvelope();
envelope.setEnvelopeNo(envelopeNo);
envelope.setUserId(userId);
envelope.setTotalAmount(totalAmount * 100);
envelope.setTotalCount(totalCount);
envelope.setRemainingAmount(totalAmount * 100);
envelope.setRemainingCount(totalCount);
envelope.setExpireTime(LocalDateTime.now().plusHours(24));
envelope.setStatus((byte) 1);
envelopeMapper.insert(envelope);
// 4. 写入Redis
initRedisData(envelopeNo, amounts, totalCount);
return envelopeNo;
}
/**
* 抢红包
*/
public Long grab(String envelopeNo, Long userId) {
// 1. 防刷检查
if (!checkRateLimit(userId)) {
throw new BusinessException("抢红包过于频繁");
}
// 2. 执行Lua脚本
Long amount = grabByLua(envelopeNo, userId);
if (amount == null || amount <= 0) {
if (amount != null && amount == -1) {
throw new BusinessException("已抢过该红包");
}
throw new BusinessException("红包已抢完");
}
// 3. 异步写MySQL
EnvelopeRecord record = new EnvelopeRecord();
record.setEnvelopeNo(envelopeNo);
record.setUserId(userId);
record.setAmount(amount);
record.setGrabTime(new Date());
kafkaTemplate.send("envelope_topic", record);
return amount;
}
/**
* Lua脚本抢红包
*/
private Long grabByLua(String envelopeNo, Long userId) {
String script =
"local grabbed = redis.call('SISMEMBER', KEYS[3], ARGV[1]) " +
"if grabbed == 1 then return {-1, 0} end " +
"local stock = redis.call('GET', KEYS[2]) " +
"if not stock or tonumber(stock) <= 0 then return {-2, 0} end " +
"local amount = redis.call('LPOP', KEYS[1]) " +
"if not amount then return {-2, 0} end " +
"redis.call('DECR', KEYS[2]) " +
"redis.call('SADD', KEYS[3], ARGV[1]) " +
"return {0, tonumber(amount)}";
String poolKey = "envelope:pool:" + envelopeNo;
String stockKey = "envelope:stock:" + envelopeNo;
String grabbedKey = "envelope:grabbed:" + envelopeNo;
DefaultRedisScript<List> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(script);
redisScript.setResultType(List.class);
List<Object> result = redisTemplate.execute(
redisScript,
Arrays.asList(poolKey, stockKey, grabbedKey),
userId
);
if (result == null || result.size() < 2) {
return null;
}
Integer code = (Integer) result.get(0);
Long amount = ((Number) result.get(1)).longValue();
return code == 0 ? amount : Long.valueOf(code);
}
/**
* 初始化Redis数据
*/
private void initRedisData(String envelopeNo, List<Long> amounts, Integer totalCount) {
// 红包池
String poolKey = "envelope:pool:" + envelopeNo;
redisTemplate.opsForList().rightPushAll(poolKey, amounts.toArray());
redisTemplate.expire(poolKey, 24, TimeUnit.HOURS);
// 库存
String stockKey = "envelope:stock:" + envelopeNo;
redisTemplate.opsForValue().set(stockKey, totalCount, 24, TimeUnit.HOURS);
}
/**
* 防刷检查
*/
private boolean checkRateLimit(Long userId) {
String date = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
String key = "user:grab:count:" + userId + ":" + date;
Long count = redisTemplate.opsForValue().increment(key, 1);
redisTemplate.expire(key, 24, TimeUnit.HOURS);
return count <= 100; // 每天最多100个
}
}五、性能优化
5.1 优化策略
1. Redis预生成金额
优点:抢时无需计算,直接弹出
go
// 发红包时预生成
amounts := DoubleAverageAlgorithm(100, 10)
redis.RPush("envelope:pool:ENV001", amounts...)2. Lua脚本原子操作
保证:检查 + 扣减 + 记录 的原子性
lua
-- 一次性完成所有操作,避免并发问题
redis.call('LPOP', KEYS[1])
redis.call('DECR', KEYS[2])
redis.call('SADD', KEYS[3], ARGV[1])3. 异步写MySQL
Kafka削峰,避免数据库压力
Redis(同步,ms级) → Kafka → MySQL(异步,秒级)4. 限流
Nginx + Lua限流:
lua
-- 限制单用户QPS
local key = "rate:limit:" .. ngx.var.remote_addr
local count = redis.call('INCR', key)
if count == 1 then
redis.call('EXPIRE', key, 1)
end
if count > 10 then
ngx.exit(503)
end5.2 性能数据
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 抢红包响应时间 | 200ms | 10ms | 20x |
| 支持QPS | 5000 | 10万+ | 20x |
| Redis内存 | 1GB | 500MB | 50% |
| MySQL写入延迟 | 同步 | 异步3s | - |
六、数据一致性
6.1 Redis和MySQL一致性
问题:
Redis抢到了,MySQL写入失败 → 钱多发了
MySQL写入了,Redis丢失 → 钱少发了解决方案:
1. 本地消息表(推荐)
go
// 抢红包成功后
// 1. Redis操作成功
amount := redis.LPOP(...)
// 2. 发送Kafka消息
kafka.Send(record)
// 3. Kafka Consumer消费
func ConsumeRecord(record) {
// 幂等性插入MySQL
db.Insert(record)
}2. 定时对账
go
// 每天凌晨对账
func Reconcile() {
// 1. 从Redis统计已抢金额
redisTotal := countRedisGrabbed()
// 2. 从MySQL统计已入账金额
mysqlTotal := countMySQLRecords()
// 3. 差异告警
if redisTotal != mysqlTotal {
alert("金额不一致", redisTotal, mysqlTotal)
}
}3. 退款兜底
go
// 24小时后,未入账的自动退款
func RefundExpired() {
// 查询Redis已抢但MySQL没记录的
// 执行退款
}七、防作弊
7.1 防重复抢
Redis Set:
redis
SISMEMBER envelope:grabbed:{envelope_no} {user_id}数据库唯一键:
sql
UNIQUE KEY uk_envelope_user (envelope_no, user_id)7.2 防刷
IP限流:
go
// 同一IP每秒最多10次
key := fmt.Sprintf("rate:limit:ip:%s", clientIP)
count := redis.Incr(key)
redis.Expire(key, 1)
if count > 10 {
return error("请求过于频繁")
}用户限流:
go
// 同一用户每天最多抢100个红包
key := fmt.Sprintf("user:grab:count:%d:%s", userID, date)
count := redis.Incr(key)
redis.Expire(key, 24*time.Hour)
if count > 100 {
return error("今日抢红包次数已达上限")
}设备指纹:
go
// 记录设备指纹,检测异常设备
deviceID := getDeviceFingerprint(request)
if isAbnormalDevice(deviceID) {
return error("设备异常")
}八、面试要点
8.1 常见追问
Q1: 微信红包和支付宝红包的算法有何不同?
A:
| 维度 | 微信红包 | 支付宝红包 |
|---|---|---|
| 算法 | 二倍均值法 | 固定金额法 |
| 随机性 | 高(金额随机) | 低(固定几个档位) |
| 公平性 | 高(期望相同) | 高 |
| 趣味性 | 高 | 中 |
| 性能 | 中(需预生成) | 高(直接选择) |
Q2: 如何保证不会超抢?
A: 三重保障:
- Redis库存:原子递减
- Lua脚本:原子性检查+扣减
- 数据库唯一键:最后防线
Q3: Redis和MySQL数据如何一致?
A:
- 本地消息表 + Kafka(推荐)
- 定时对账 + 告警
- 退款兜底
Q4: 如何防止高并发击穿Redis?
A:
- 前置限流:Nginx + Lua
- Redis集群:主从+哨兵
- 布隆过滤器:过滤不存在的红包
- 熔断降级:Redis故障时降级
8.2 扩展知识点
相关场景题:
相关技术文档:
九、总结
抢红包系统设计要点:
- 红包算法:二倍均值法(公平+随机)
- 高并发处理:Redis + Lua原子操作
- 防超抢:库存控制 + 唯一键
- 数据一致性:Kafka异步 + 定时对账
- 防作弊:防重复 + 限流 + 设备指纹
核心技术栈:
- Redis:存储红包池、库存、已抢记录
- Lua:原子性抢红包
- Kafka:异步写MySQL
- MySQL:持久化存储
性能指标:
- 响应时间:<10ms
- 支持QPS:10万+
- 数据一致性:99.99%
面试重点:
- 能说清楚二倍均值法的原理和公平性证明
- 能解释Lua脚本如何保证原子性
- 能设计Redis和MySQL的一致性方案
- 能说出防作弊的多重手段
参考资料:
- 微信红包技术分享
- 支付宝红包架构
- 《高并发系统设计40问》
