> For the complete documentation index, see [llms.txt](https://lewiszlw.gitbook.io/notebooks/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://lewiszlw.gitbook.io/notebooks/wheels/red-envelope/fang-an-she-ji.md).

# 方案设计

## 具体设计

### 缓存

选用Redis

**缓存穿透解决方案：**

当查询时缓存未命中，从数据库load数据到缓存，如果红包不存在，则load一条数据（key为红包id，value标记红包不存在）到缓存中。

**缓存击穿解决方案：**

加互斥锁，利用redisTemplate.opsForValue().setIfAbsent()方法

循环：

1. 查缓存
2. 缓存为空，则尝试加锁，否则跳出循环
3. 尝试加锁失败，则跳到第1步继续循环，尝试加锁成功，则从DB load数据到缓存

// TODO

1.存储格式优化，改用hash格式

**缓存雪崩解决方案：**

### 消息队列

mq选型：RocketMq

具体方案：

先从缓存中查红包剩余数量，如果数量为0则直接返回，如果不为0，则使用mq发送一条红包金额扣减消息，然后返回秒杀中，客户端轮询秒杀结果。数据库层订阅扣减消息，进行红包金额扣减，扣减成功后刷新缓存。

### 前端/客户端

点击`抢`后，弹出加载动画，此时后端一般返回正在抢中的状态，然后前端轮询抢红包状态，直到查到抢红包结果确定，结束动画，展示结果。

### 扣减红包金额

金额全部使用int型，单位为分。

不可重复读：从查询红包余额时加排它锁，防止查询余额后，有其他线程更新了该余额，导致不可重复读。

解决办法：

1. 排他锁（for update）：mybatis实现排他锁不够优雅，抛弃
2. 分布式锁

### 分布式锁：

使用stringRedisTemplate.opsForValue().setIfAbsent()方法实现

锁设置超时时间，如果线程卡住了，锁过期后其他线程可以抢占锁。

// TODO

当单次处理时间过长时，锁过期，存在锁泄漏情况。

解决方案：做好监控。

### 接口限流

使用guava的RateLimter配合aop实现

### 数据库设计

数据库：red-envelope

数据库表：

1.envelope\_detail（红包详情）

```sql
CREATE TABLE `envelope_detail` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键',
  `user` varchar(25) DEFAULT NULL COMMENT '发红包者',
  `type` varchar(10) NOT NULL DEFAULT 'UNKONWN' COMMENT '红包类型',
  `amount` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '红包总额',
  `size` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '红包份数',
  `remain_money` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '红包余额',
  `remain_size` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '红包余下份数',
  `created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```

2.envelope\_grabber（抢到红包的人和金额等）

```sql
CREATE TABLE `envelope_grabber` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键',
  `envelope_id` int(11) unsigned NOT NULL COMMENT '红包id',
  `grabber` varchar(25) NOT NULL DEFAULT '' COMMENT '抢到用户',
  `money` int(11) unsigned NOT NULL COMMENT '抢到金额',
  `created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_envelope_id_grabber` (`envelope_id`,`grabber`)
) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8mb4;
```

### 常见问题

1.如何解决抢超问题（）

* 在sql加上判断防止数据边为负数
* 数据库加唯一索引防止用户重复抢红包
* redis预减库存减少数据库访问　内存标记减少redis访问　请求先入队列缓冲，异步下单，增强用户体验

### 对账

### 压力测试

Jmeter/Siege

## 可优化点

1. 数据库分库分表
2. redis集群宕机解决方案
3. 应用监控、基础设施监控
4. 防范脚本抢红包：负载均衡设备中做一些配置，判断如果同一个用户在1分钟之内多次发送请求来进行抢红包，就认为是恶意脚本代抢
5. 利用负载均衡层来过滤无效请求（红包已抢完）
6. 红包过期，多余金额退换
7. set化：红包系统根据红包 ID，按一定的规则（如按 ID 尾号取模等），垂直上下切分。切分后，一个垂直链条上的逻辑 Server 服务器、DB 统称为一个 SET。各个 SET 之间相互独立，互相解耦。并且同一个红包 ID 的所有请求，包括发红包、抢红包、拆红包、查详情详情等，垂直 stick 到同一个 SET 内处理，高度内聚。通过这样的方式，系统将所有红包请求这个巨大的洪流分散为多股小流，互不影响，分而治之
8. 大key优化

## 参考资料

<https://www.zhihu.com/question/22625187/answer/85530416> <https://rocketmq.apache.org/docs/quick-start/> <https://www.bilibili.com/video/av19582887/> <https://www.infoq.cn/article/2017hongbao-weixin> <https://www.ibm.com/developerworks/cn/web/wa-design-small-and-good-kill-system/index.html> <https://zhuanlan.zhihu.com/p/25368538> <https://www.zhihu.com/question/54895548> <https://www.infoq.cn/article/2017hongbao-weixin>
