-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Expand file tree
/
Copy pathmianshiguan-youhuiquan.md
More file actions
488 lines (311 loc) · 14.1 KB
/
mianshiguan-youhuiquan.md
File metadata and controls
488 lines (311 loc) · 14.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
---
title: 淘宝面试官:优惠券系统该如何设计?
shortTitle: 如何设计优惠券系统?
description: 大厂的优惠券系统是如何设计的?
category:
- 求职面试
tag:
- 面试题&八股文
head:
- - meta
- name: keywords
content: Java,java,面试题,八股文,优惠券
---
## 1 Scenario 场景
电商大厂常见促销手段:
* 优惠券
* 拼团
* 砍价
* 老带新
### **1.1 优惠券的种类**
* 满减券
* 直减券
* 折扣券
### **1.2 优惠券系统的核心流程**
#### **1.2.1 发券**
发券的方式:同步发送 or 异步发送
#### **1.2.2 领券**
* 谁能领?
所有用户 or 指定的用户
* 领取上限
一个优惠券最多能领取多少张?
* 领取方式
用户主动领取 or 自动发放被动领取
#### **1.2.3 用券**
* 作用范围
商品、商户、类目
* 计算方式
是否互斥、是否达到门槛等
### **1.3 需求拆解**
#### **1.3.1 商家侧**
* 创建优惠券
* 发送优惠券
#### **1.3.2 用户侧**
* 领取优惠券
* 下单
* 使用优惠券
* 支付
## **2 Service 服务**
### **2.1 服务结构设计**

### **2.2 优惠券系统设计技术难点**
* 券的分布式事务,使用券的过程会出现的分布式问题分析?
* 如何防止超发?
* 如何大批量给用户发券?
* 如何限制券的使用条件?
* 如何防止用户重复领券?
## 3 Storage存储
### **3.1 表单设计**
#### **券批次(券模板),coupon\_batch**
指一批优惠券的抽象、模板,包含优惠券的大部分属性。
如商家创建了一批优惠券,共1000张,使用时间为2022-11-11 00:00:00 ~ 2022-11-11 23:59:59,规定只有数码类目商品才能使用,满100减50。
#### **券**
发放到用户的一个实体,已与用户绑定。
如将某批次的优惠券中的一张发送给某个用户,此时优惠券属于用户。
#### **规则**
优惠券的使用有规则和条件限制,比如满100减50券,需要达到门槛金额100元才能使用。

**券批次表 coupon\_batch**

**规则表 rule:**

**规则内容:**
```
{
threshold: 5.01 // 使用门槛
amount: 5 // 优惠金额
use_range: 3 // 使用范围,0—全场,1—商家,2—类别,3—商品
commodity_id: 10 // 商品 id
receive_count: 1 // 每个用户可以领取的数量
is_mutex: true // 是否互斥,true 表示互斥,false 表示不互斥
receive_started_at: 2020-11-1 00:08:00 // 领取开始时间
receive_ended_at: 2020-11-6 00:08:00 // 领取结束时间
use_started_at: 2020-11-1 00:00:00 // 使用开始时间
use_ended_at: 2020-11-11 11:59:59 // 使用结束时间
}
```
**优惠券表 coupon:**
```
create table t_coupon
(
coupon_id int null comment '券ID,主键',
user_id int null comment '用户ID',
batch_id int null comment '批次ID',
status int null comment '0-未使用、1-已使用、2-已过期、3-冻结',
order_id varchar(255) null comment '对应订单ID',
received_time datetime null comment '领取时间',
validat_time datetime null comment '有效日期',
used_time datetime null comment '使用时间'
);
```
### **3.2 建券**
**1、新建规则**
```
INSERT INTO rule (name, type, rule_content)
VALUES(“满减规则”, 0, '{
threshold: 100
amount: 10
......
}');
```
**2、新建优惠券批次**
```
INSERT INTO coupon\_batch (coupon\_name, rule\_id, total\_count )
VALUES(“劳斯莱斯5元代金券”, 1010, 10000);
```
### **3.3 发券**


##### **如何给大量用户发券?**
异步发送!
##### **触达系统**
* 短信、邮件
可通过调用第三方接口的方式实现
* 站内信
通过数据库插入记录来实现
**信息表 message**
```
create table t_message
(
id int null comment '信息ID',
send_id int null comment '发送者id',
rec_id int null comment '接受者id',
content vachar(255) comment '站内信内容',
is_read int null comment '是否已读',
send_time datetime comment '发送时间'
)
comment '信息表';
```
先考虑用户量很少的情况,商家要给所有人发站内信,则先遍历用户表,再按照用户表中的所有用户依次将站内信插入到 message 表中。这样,如果有100个用户,则群发一条站内信要执行100个插入操作。
#### **系统用户数增加到w级**
发一条站内信,就得重复插入上万条数据。而且这上万条数据的 content 一样!假设一条站内信占100K,发一次站内信就要消耗十几M。对此,可将原来的表拆成两个表:
**信息表 message**

**信息内容表 message\_content**

#### **发一封站内信的步骤**
1. 往 message\_content 插入站内信的内容
2. 在 message 表中,给所有用户插入一条记录,标识有一封站内信
#### **千w级用户数**
这就有【非活跃用户】的问题,假设注册用户一千万,根据二八原则,其中活跃用户占20%。若采用上面拆成两个表的情况,发一封“站内信”,得执行一千万个插入操作。可能剩下80%用户基本都不会再登录,其实只需对其中20%用户插入数据。
**信息表 message:**
```
create table t_message
(
id int null comment '信息 ID',
# send_id int null comment '发送者 id', 去除该字段
rec_id int null comment '接受者 id',
message_id int null comment '外键,信息内容',
is_read int null comment '是否已读'
)
comment '信息表';
create table t_message_content
(
id int null comment '信息内容id',
send_id int null comment '发送者id',
content varchar(255) null comment '内容',
send_time datetime null comment '发送时间'
);
```
#### **用户侧操作**
登录后,首先查询 message\_content 中的那些没有在 message 中有记录的数据,表示是未读的站内信。在查阅站内信的内容时,再将相关的记录插入 message。
#### **系统侧操作**
发站内信时:
* 只在 message\_content 插入站内信的主体内容
* message 不插入记录
#### **给 10W 用户发券**

有什么问题?重复消费,导致超发!
1. 运营提供满足条件的用户文件,上传到发券管理后台并选择要发送的优惠券
2. 管理服务器根据【用户ID】、【券批次ID】生成消息,发送到MQ
3. 优惠券服务器消费消息
```
# 记住使用事务哦!
INSERT INTO coupon (user_id, coupon_id,batch_id)
VALUES(1001, 66889, 1111);
UPDATE coupon_batch SET total_count = total_count - 1,
assign_count = assign_count + 1
WHERE batch_id = 1111 AND total_count > 0;
```
### **3.4 领券**
#### 步骤
1. 校验优惠券余量
```
SELECT total_count FROM coupon_batch
WHERE batch_id = 1111;
```
2. 新增优惠券用户表,扣减余量
```
# 注意事务!
INSERT INTO coupon (user_id, coupon_id,batch_id)
VALUES(1001, 66889, 1111);
UPDATE coupon_batch SET total_count = total_count - 1,
assign_count = assign_count + 1
WHERE batch_id = 1111 AND total_count > 0;
```
用户领券过程中,其实也会出现类似秒杀场景。秒杀场景下会有哪些问题,如何解决?

#### **用户重复领取或多领**
Redis 数据校验!
1. 领券前,先查缓存
```
# 判断成员元素是否是集合的成员
```
```
SISMEMBER KEY VALUE
SISMEMBER batch_id:1111:user_id 1001
```
2. 领券
3. 领券后,更新缓存
```
# 将一或多个成员元素加入到集合中,已经存在于集合的成员元素将被忽略
SADD KEY VALUE1......VALUEN
SADD batch_id:1111:user_id 1001
```
## **3.5 用券**
何时校验优惠券使用规则?
1. 确认订单(√)
2. 提交订单
3. 立即付款
确认订单页,对优惠券进行校验:
* 判断是否过期
* 判断适用范围
* 判断是否达到门槛
* 判断是否互斥
### **返回可用券**

```
SELECT batch_id FROM coupon WHERE user_id = 1001 AND status = 0;
SELECT rule_id FROM coupon_batch WHERE batch_id = 1111;
SELECT name, type, rule_content FROM rule WHERE rule_id = 1010;
```
### 选择可用券,并返回结果

### **同时操作多个服务,如何保证一致性?**

### **表设计**
**优惠券操作记录表 Coupon\_opt\_record**
```
create table t_coupon_opt_record
(
user_id int null comment '用户id',
coupon_id int null comment '优惠券id',
operating int null comment '操作,0-锁定、1-核销、2-解锁',
operated_at datetime null comment '操作时间'
);
```
TCC,Try-Confirm-Cancel,目前分布式事务主流解决方案。
1. 阶段一:Try
对资源进行冻结,预留业务资源
创建订单时,将优惠券状态改为 “冻结”
2. 阶段二:Confirm
确认执行业务操作,做真正提交,将第一步Try中冻结的资源,真正扣减
订单支付成功,将优惠券状态改为 “已使用”
3. 阶段三:Cancel
取消执行业务操作,取消Try阶段预留的业务资源
支付失败/超时或订单关闭情况,将优惠券状态改为 “未使用”

## 4 Scale 扩展
### **4.1 快过期券提醒**
#### **定时扫券表**
缺点:扫描数据量太大,随着历史数据越来越多,会影响线上主业务,最终导致慢SQL。
#### **延时消息**
缺点:有些券的有效时间太长了(30天)以上,有可能造成大量 MQ 积压
#### **新增通知表**
优点:扫描的数据量小,效率高。删除无用的已通知的数据记录
##### **通知信息表(notify\_msg)设计**
```
create table t_notify_msg
(
id bigint auto_increment comment '自增主键',
coupon_id bigint null comment '券id',
user_id bigint null comment '用户id',
notify_day varchar(255) null comment '需要执行通知的日期',
notify_type int null comment '通知类型,1-过期提醒',
notif_time timestamp null comment '通知的时间,在该时间戳所在天内通知',
status int null comment '通知状态,0-初始状态、1-成功、2-失败',
constraint t_notify_msg_id_uindex
unique (id)
);
alter table t_notify_msg
add primary key (id);
```
**过期券提**
1. 在创建优惠券的时候就将需要提醒的记录插入提醒表中notify\_msg
2. 把用户ID+批次ID+通知日期作为唯一索引,防止同一个批次有重复的记录通知,保证每天只会被通知一次
3. 建立notify\_time,通知时间索引,每日的通知扫描通过该索引列查询,通过索引列来提高查询效率
4. 通知完成后该表中的数据变失去了意义,通过定时任务将该数据删除
### **4.2 数据库层面优化 - 索引**


### **4.3 发券接口,限流保护**
#### **前端限流**
点击一次后,按钮短时间内置灰

#### **后端限流**
部分请求直接跳转到【繁忙页】
>参考链接:[https://mp.weixin.qq.com/s/r9lgiOwV5cw8XmfCBUTcUA](https://mp.weixin.qq.com/s/r9lgiOwV5cw8XmfCBUTcUA),出处:JavaEdge,整理:沉默王二
----
GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/)
微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。
