# 方案设计

## 具体设计

### 缓存

选用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>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://lewiszlw.gitbook.io/notebooks/wheels/red-envelope/fang-an-she-ji.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
