系统设计案例实战
2022-12-22 15:42:35 0 举报
AI智能生成
系统设计案例实战
作者其他创作
大纲/内容
聊天系统设计
Scenario 场景
基本功能
• * 用户登录注册
• * 通讯录
• 两个用户互相发消息
• 群聊
其他功能
• 限制多机登陆
• 或者支持多机登陆
• * 用户在线状态
• WhatsApp, Messenger 等 APP 有此功能
• 或者支持多机登陆
• * 用户在线状态
• WhatsApp, Messenger 等 APP 有此功能
• 微信
• 10.8亿 月活跃用户 • 日发送量 450 亿 • ——数据来自 2019 年微信公开课PRO
• QPS: • Average QPS = 45B / 86400 ~ 520k
• Peak QPS = 520k * 3 ~ 1.5m
• 存储: • 假设每条记录约30bytes的话,大概需要 1.3T 的存储
Service 服务
Message Service 负责信息相关的存取
Realtime Service 负责信息的实时推送
Storage 存储
Message Table
数据量很大,不需要修改,一条聊天信息就像一条log一样
sharding key (row key)是什么
• 存储结构:
• row_key = thread_id
• column_key = created_at 因为要按照时间倒序
• value = 其他信息
Thread Table
• Thread Table 存储公有的 Thread 信息
• 如果使用 SQL 需要同时 index by
• thread_id 用于查询某个对话的信息
• participant_hash_code 用户查询某些用户之间是否已经有 thread
如果使用 NoSQL 该如何存储?
如果使用 NoSQL 存储 Thread Table 并同时支持按照 thread_id 和
participant_hash_code 进行查询,我们需要两张表:
• 表1:Thread Table
• row_key = thread_id
• column_key = null
• value = 其他的基本信息
• 表2:ParticipantHashCode Table
• row_key = participant_hash_code
• column_key = null
• value = thread_id
User Thread Table
UserThread Table 存储私有的 Thread 信息
用什么做 sharding key(row_key)
存储结构:
• row_key = user_id
• column_key = updated_at 按照更新时间倒序
• value = 其他信息
当用户 A 给用户 B 发消息的时候,可能并不知道他们之间的
thread_id 是什么,如何在服务器上查询?
在 Thread Table 中增加一个
participants_hash_code
该值由所有参与者的user_id排序之后hash得到,即:
partcipants_hash_code = any_hashfunc(sorted(participants_user_ids))
不直接使用排序之后的 user_ids 是因为如果是群聊的话,会太长
如果只需要考虑两人对话的话,可以自定义一个格式:private::user1::user2
采用uuid之类的hash方式则不需要考虑hash collision的问题
一个可行解的流程
• 用户 A 发送一条消息给 用户 B
• 服务器收到消息,查询是否有 A 和 B 的对话记录(Thread),如果没有则创建对应的 Thread
• 根据 Thread id 创建 Message
• B 每隔 10s 访问一次服务器获取最新的信息(Poll) • B 收到信息
Scale 升级
每隔10秒钟收一次消息太慢了,聊天体验很差,不实时
使用手机的推送系统
• A 发送消息到 Web Server
• Web Server 创建信息存入数据库之后通知 APNS
• APNS 告诉 B 有新消息了
• B 去 Web Server 抓取下新的消息(可选) • 如果消息比较短的话,也可以直接通过 APNS 传递
• 无需 B 再次访问 Web Server 获取
无法支持 Web 端
GCM / APNS 都是依赖于手机操作系统的
无法支持 Web 端或者桌面端的信息推送
如 Web 微信,Facebook Messenger
Socket
• 用户A打开App后,问 Web Server 要一个 Push Service 的连接地址
• A通过 socket 与push server保持连接 • 用户B发消息给A,消息先被发送到服务器 • 服务器把消息存储之后,告诉 Push Server 让他通知 A
• A 收到及时的消息提醒
如何支持群聊?
假如一个群有500人(1m用户也同样道理)
• 如果不做任何优化,需要给这 500 人一个个发消息
• 但实际上 500 人里只有很少的一些人在线(比如10人)
• 但Message Service仍然会尝试给他们发消息
• Message Service (web server) 无法知道用户和Push Server的socket连接是否已经断开
• 至于 Push Server 自己才知道
• 消息到了Push Server 才发现490个人根本没连上 • Message Service 与 Push Server 之间白浪费490次消息传递
• 解决
• 增加一个Channel Service(频道服务) • 为每个聊天的Thread增加一个Channel信息
• 对于较大群,在线用户先需要订阅到对应的 Channel 上
• 用户上线时,Web Server (message service) 找到用户所属的频道(群),并通知 Channel Service 完成订阅
• Channel就知道哪些频道里有哪些用户还活着
• 用户如果断线了,Push Service 会知道用户掉线了,通知 Channel Service 从所属的频道里移除
• Message Service 收到用户发的信息之后
• 找到对应的channel
• 把发消息的请求发送给 Channel Service
• 原来发500条消息变成发1条消息
• Channel Service 找到当前在线的用户 • 然后发给 Push Service 把消息 Push 出去
• 增加一个Channel Service(频道服务) • 为每个聊天的Thread增加一个Channel信息
• 对于较大群,在线用户先需要订阅到对应的 Channel 上
• 用户上线时,Web Server (message service) 找到用户所属的频道(群),并通知 Channel Service 完成订阅
• Channel就知道哪些频道里有哪些用户还活着
• 用户如果断线了,Push Service 会知道用户掉线了,通知 Channel Service 从所属的频道里移除
• Message Service 收到用户发的信息之后
• 找到对应的channel
• 把发消息的请求发送给 Channel Service
• 原来发500条消息变成发1条消息
• Channel Service 找到当前在线的用户 • 然后发给 Push Service 把消息 Push 出去
Q: Channel Service 中的数据是什么结构?
A: key-value 的结构。key 为 channel name,可以是一个字符串比如 “#personal::user_1”。value是一个
set 代表哪些人订阅到了这个 channel 下。
Q: Channel Service 用什么数据存储?
A: 根据上面所提到的 key-value 结构以及 value 需要是一个 set,Redis 是一个很好的选择。
Q: 如何知道一个用户该订阅到哪些 Channels?
A: 首先用户需要订阅自己的 personal channel,如 #personal::user_1,与该用户有关的私聊信息都在这 个 channel 里发送。小于一定人数的群聊可以依然通过 personal channel 推送,超过一定人数的群聊,
可以采用 lazy subscribe 的方式,在用户打开 APP 且群处于比较靠前的位置的时候才订阅,用户没有主
动订阅的群聊靠 Poll 的模式获取最新消息。
Q: 用户关闭 APP 以后还能收到提醒么?
A: 如果真的关闭了 APP 是不行的。所以很多 APP 会常驻后台,保证至少 Poll 模式还能工作即可。
A: key-value 的结构。key 为 channel name,可以是一个字符串比如 “#personal::user_1”。value是一个
set 代表哪些人订阅到了这个 channel 下。
Q: Channel Service 用什么数据存储?
A: 根据上面所提到的 key-value 结构以及 value 需要是一个 set,Redis 是一个很好的选择。
Q: 如何知道一个用户该订阅到哪些 Channels?
A: 首先用户需要订阅自己的 personal channel,如 #personal::user_1,与该用户有关的私聊信息都在这 个 channel 里发送。小于一定人数的群聊可以依然通过 personal channel 推送,超过一定人数的群聊,
可以采用 lazy subscribe 的方式,在用户打开 APP 且群处于比较靠前的位置的时候才订阅,用户没有主
动订阅的群聊靠 Poll 的模式获取最新消息。
Q: 用户关闭 APP 以后还能收到提醒么?
A: 如果真的关闭了 APP 是不行的。所以很多 APP 会常驻后台,保证至少 Poll 模式还能工作即可。
拓展问题1:多机登录
如何限制多台客户端同时登录一个账户?
如从手机登陆时,查询是否已经有其他手机处于登陆状态 • 如果没有,则创建新的 session
• 如果有,将对应的 session 设为 expire 或者删除,并发送 push notification 让已经登录的手机 logout
• 如果 Push Notification 失败也没有关系
• 该手机会在下次访问任何API的时候发现自己已经logout了并跳转至登入界面
如何支持用户在线状
态显示?
是否可以用 Push Server 中的 socket 连接情况?
缺陷1:如果用户的网络不稳定,会导致连接时断时连
缺陷2:如果在 Push Service 中使用数据库来存储在线信息,Push Service 的结构会变得复杂,通用性
会变差,依赖会增多。
使用数据库存储 online status 的信息
使用 Web Server 直接访问数据库的获取该信息
OnlineStatus Table 中存储什么信息?
存如下一些信息足够: <user, last_updated_at, client_info>
是 Pull 还是 Push?
是 Pull,每隔3-5s pull 一次(heartbeat)
原因:
1. Pull 更简单,依赖更少(不依赖于 Push Service),代码量更少
2. 在告诉服务器我在线的时候,还可以顺带更新所有好友的在线状
况,用于客户端显示
a. 更新好友在线状态如果用 Push 的方式来做存在很多问题,比如用
户如果掉线了,还需要由 Push Server 通知 Web Server 来更新在
线状态,然后再通过 Web Server 通知所有的好友他掉线了。
总而言之就是 Pull 更简单,Push 更复杂,对实时性要求不高的时候,
用 Pull 更好。
视频流系统设计
Scenario 场景
功能点
用户可以上传视频
用户可以观看和分享视频 生成缩略图
用户可以点赞、评论
用户可以观看和分享视频 生成缩略图
用户可以点赞、评论
系统的高可用
观看视频的流畅度
视频的实时推荐
观看视频的流畅度
视频的实时推荐
容量评估
根据数据,我们有150M的日活跃用户(DAU),如果用户平均每天观看30个视频,则每秒视频的观看 数为:
150M * 30 / 86400 秒 = 52083 视频/秒,即每天视频观看数为45亿
根据经验值上传视频数总是小于观看视频数,因此假设每上传一个视频,我们就会观看500部视频, 则每秒视频的上传数为:
52083 / 500 = 104 视频/秒
假设平均每个视频时长为5分钟,则每秒上传的视频时长为
104 * 5 = 520 分钟/秒,即每分钟上传视频时长520小时
存储空间估算:假设在平均的情况下,一分钟视频需要50MB的存储空间(视频需要以多种格式存 储),则一秒钟内上传的视频所需的总存储量为:
520 * 50 = 26000 MB/s = 26 GB/s
带宽估算:每秒上传520分钟的视频,并假设每次上传的视频占用的带宽为 166KB/s,那么:
520 * 60 * 166 = 5179200 KB/s = 5 G/s
Service 服务
用户服务 UserService
上传服务 UploadService
视频切分
在编码方式上传中,在前端我们 只要先获取文件的二进制内容, 然后对其内容进行拆分,最后将 每个切片上传到服务端即可。
在JavaScript中,文件FIle对象 是Blob对象的子类,Blob对象 包含一个重要的方法slice,通 过这个方法,我们就可以对二进 制文件进行拆分。
断点续传
• 视频切分后,在客户端根据哈希算法生成所 有视频切片的 chunk_id
• 客户端发起上传请求,将所有 chunk_id 带 到服务端
• 服务端生成 video_id & 保存目录并返回给 客户端 • 客户端开始上传 chunk
• 客户端发起上传请求,将所有 chunk_id 带 到服务端
• 服务端生成 video_id & 保存目录并返回给 客户端 • 客户端开始上传 chunk
当上传完一个 chunk 之后,服务端告诉客户端需 要传下一个 chunk
转码服务 EncodeService
格式转码
用户可能上传各种格式的视频,需要转换成 Youtube 兼 容的格式在网页端进行播放。
清晰度转码
用户根据网络环境的不同,会有不同清晰度视频的需求。
缩略图服务 ThumbService
视频服务 VideoService
Storage 存储
视频表 Video Table
视频分片表 Chunk Table
缩略图表 Thumbnail Table
用户表 User Table
用户视频关系表 User Video Table
Scale 升级
如何用 CDN 优化读取
独立出一个文件系统来存储文件,将文件与 webserver 独立。
如何对数据库做 sharding
查询用户数据
用户视频关系表以用户ID做Sharding
查询视频数据
视频表、Chunk表以视频ID做Sharding
Uber打车系统设计
Scenario 场景
第一阶段:
• Driver report locations
• Rider request Uber, match a driver with rider
第二阶段:
• Driver deny / accept a request
• Driver cancel a matched request
• Rider cancel a request
• Driver pick up a rider / start a trip
• Driver drop off a rider / end a trip
第三阶段 *
• Uber Pool
• Uber Eat
设计得多牛
2018年每天有 2M 司机载客
假设同时在线的司机平均约为 600k(猜的)
• Average Driver QPS = 600k / 4 = 150k
• Driver report locations by every 4 seconds
• Peak Driver QPS = 150k * 2 = 300 k
• Uber 官方自己的说法:2015 新年夜的 Peak QPS 是 170K,当时 Uber 约有 1M 的司机
• Read More: http://bit.ly/1FBSgMK
• Rider QPS 可以忽略
• 无需随时汇报位置
• 一定远小于Driver QPS
存储估算
假如每条Location都记录:600 k * 86400 / 4 * 100bytes (每条位置记录)~ 1.3 T / 天
假如只记录当前位置信息:600 k * 100 bytes = 60 M
Service 服务
Uber 主要干的事情就两件
记录车的位置 GeoService
匹配打车请求 DispatchService
Storage 存储
Trip Table
Location Table
Driver Table
如何存储和查询地理位置信息?
Geohash
SQL 数据库
• 首先需要对 geohash 建索引
• CREATE INDEX on geohash;
• 使用 Like Query
• SELECT * FROM location WHERE geohash LIKE` 9q9hv%`;
NoSQL - Cassandra
• 将 geohash 设为 column key
• 使用 range query (9q9hv0, 9q9hvz)
NoSQL - Redis
• Driver 的位置分级存储
• 如 Driver 的位置如果是 9q9hvt,则存储在 9q9hvt, 9q9hv, 9q9h 这 3 个 key 中
• 6位 geohash 的精度已经在一公里以内,对于 Uber 这类应用足够了
• 4位 geohash 的精度在20公里以上了,再大就没意义了,你不会打20公里以外的车
• key = 9q9hvt, value = set of drivers in this location
• 如 Driver 的位置如果是 9q9hvt,则存储在 9q9hvt, 9q9hv, 9q9h 这 3 个 key 中
• 6位 geohash 的精度已经在一公里以内,对于 Uber 这类应用足够了
• 4位 geohash 的精度在20公里以上了,再大就没意义了,你不会打20公里以外的车
• key = 9q9hvt, value = set of drivers in this location
打车用户的角度
用户发出打车请求,查询给定位置周围的司机
(lat,lng) → geohash → [driver1, driver2, …]
• 先查6位的 geohash༌找0.6公里以内的
• 如果没有༌再查5位的 geohash༌找2.4公里以内的
• 如果没有༌再查4位的 geohash༌找20公里以内的
匹配司机成功,用户查询司机所在位置
driver1 → (lat, lng)
司机的角度
司机汇报自己的位置
计算当前位置 lat, lng的geohash
• geohash4, geohash5, geohash6
• 查询自己原来所在的位置
• geohash4’,geohash5’, geohash6’
• 在Driver Table中更新自己的最后活跃时间
司机接受打车请求
• 修改 Trip 状态
• 用户发出请求时就已经在 Trip Table 中创建一次旅程༌并Match上最近的司机
• 在Driver Table 中标记自己的状态进入不可用状态
• 用户发出请求时就已经在 Trip Table 中创建一次旅程༌并Match上最近的司机
• 在Driver Table 中标记自己的状态进入不可用状态
司机完成接送༌结束一次Trip
• 在 Trip Table 中修改旅程状态
• 在Driver Table 中标记自己的状态进入可用状态
• 在Driver Table 中标记自己的状态进入可用状态
可行解 Work Solution
1. 乘客发出打车请求,服务器创建一次Trip
• 将 trip_id 返回给用户 • 乘客每隔几秒询问一次服务器是否匹配成功
2. 服务器找到匹配的司机,写入Trip,状态为等待司机回应 • 同时修改 Driver Table 中的司机状态为不可用,并存入对应的 trip_id
3. 司机汇报自己的位置
• 顺便在 Driver Table 中发现有分配给自己的 trip_id
• 去 Trip Table 查询对应的 Trip,返回给司机
4. 司机接受打车请求 • 修改 Driver Table, Trip 中的状态信息
• 乘客发现自己匹配成功,获得司机信息
5. 司机拒绝打车请求 • 修改 Driver Table,Trip 中的状态信息,标记该司机已经拒绝了该trip
• 重新匹配一个司机,重复第2步
Scale 升级
Redis server is
down
DB Sharding
按照前4位 Geohash
数据怎么查询就怎么拆分
查询是按照 4-6位的 geohash
那么拆分就可以按照 4位的 geohash 来 sharding
Uber 的做法:按城市 Sharding
Redis Master Slave
让更强大的 NoSQL 数据
库来处理
库来处理
既然一定要用多台机器的话,我们可以用 1000 台 Cassandra / Riak
这样的 NoSQL 数据库,平均每台分摊 300 的 QPS 就不大了
这类数据库会帮你更好的处理 Replica 和挂掉之后恢复的问题
分布式计算MapReduce
Scenario 场景
Service 服务
Storage 存储
Scale 升级
推特搜索系统设计
Scenario 场景
Service 服务
Storage 存储
Scale 升级
爬虫系统设计
Scenario 场景
抓取互联网上的“所有”网页内容信息
并存储下来供索引器(Indexer)建立倒排索引(Inverted Index)
爬取目标
10天之内抓取下 1B 网页 (1k webpages per sec)
需要 10T 的存储 (10k per webpage)
Service 服务
Web Crawler
解析抓取到的网页,并将新的
URL存入到对应域名下面的待
抓取 URL 列表中
从队列中获取需要抓
取的 URL 并抓取
取的 URL 并抓取
Message Queue
(Store crawler tasks
Distributed File System
存网页
Database
Scheduler
获得可以抓取的 Domain 和待
抓取的 URL List
从 URL List 中获得 URL(每次1
个或者若干个,根据 Robots 协 议而定),扔进抓取队列
Storage 存储
Scale 升级
在中国抓取美国的网页会比较慢
在不同的地区部署 Crawler 系统
每个地区只抓取所在地区被分配的 domain 下的网页
可以通过 domain 的 whois 信息来确定网站所属地区
如何处理网页的更新和失效?
很多时候网页会不断更新,爬虫也需要不断的抓取同一个网页
有的时候可能网站挂了一小段时间,此时网页正好无法被抓取
增加对每个 URL 的信息记录
记录下这个 URL 下一次需要被重新抓取的时间
可以通过 Expenential Backoff 的方式计算
如何处理网页更新
• URL 抓取成功以后,默认 1 小时以后重新抓取
• 如果 1 小时以后抓取到的网页没有变化,则设为 2 小时以后再次抓取
• 2小时以后还是没有变化,则设为 4 小时以后重新抓取,以此类推 • 如果 1 小时以后抓取到的网页发生变化了,则设为 30 分钟以后再次抓取
• 30分钟以后又变化了,设为 15 分钟以后重新抓取。
• 设置抓取时间的上下边界,如至少 30 天要抓取一次,至多 5 分钟抓取一次
如何处理网页失效
• URL 抓取失效以后,默认 1 小时以后重新抓取
• 如果 1 小时以后依然抓不到,则设置为 2 小时 • 其他步骤类似网页更新的情况
搜索建议系统设计
Scenario 场景
输入与输出
用户输入一段想要搜索的内容的前缀,返回可能匹配的 Top 10 Suggest Queries
尽量返回被其他人搜索得较多的 Queries
推算 QPS
DAU = 500M
假设每天每人搜索 6 次,每次平均输入 10 个字符
用户每输入一个字符,都会访问一次 Suggestion API 来获得 Top 10 Queries
Average QPS = 500M * 6 * 10 / 86400 = 30B / 86400 ~ 340k
Peak QPS ~ Average QPS * 3 ~ 1M
Service 服务
QueryService
提供 Top 10 Queries 的查询
CollectionService
记录用户的 Queries 提供给 QueryService
Storage 存储
Key-value Storage
CollectionService 负责统计每个 Query 的搜索次数
key=query, value=被搜索次数
定期遍历所有 Queries ,将每个 Query 通过打擂台的方式加入到其所有 Prefix 的 Top 10 Queries 中
• 如 apple 这个词如果被搜索了 1b 次 • 那 apple 需要分别被加入到 a, ap, app, appl, apple 这5个 prefix 的 Top 10 Queries 中
• 如果某个 prefix 已经存在了被搜索次数更多的其他 10 个 Queries,就无需再加入了
• 如 apple 这个词如果被搜索了 1b 次 • 那 apple 需要分别被加入到 a, ap, app, appl, apple 这5个 prefix 的 Top 10 Queries 中
• 如果某个 prefix 已经存在了被搜索次数更多的其他 10 个 Queries,就无需再加入了
最后相当于我们需要存储下这样一些 key-value: • a: [“amazon”, “aws”, “air china”, “apple”, “airbnb”, “adidas”, ...]
• ap: [“apex”, “apple china”, “apple id”, … ]
• …
• ap: [“apex”, “apple china”, “apple id”, … ]
• …
Scale 升级
如何优化 CollectionService
并不是每条 Query 都会成为 Top 10
会浪费很多存储在记录永远不会成为 Top 10 的 Queries 上
会浪费很多存储在记录永远不会成为 Top 10 的 Queries 上
不记录所有的 Queries,以 1/10000 的概率来记录
即 if get_random(10000) == 0 则对应的 Query 计数+1
否则就直接扔掉
因为我们不关心具体的 Query 次数,只需要一个相对的排名
该是 Top 10 的还是 Top 10
即 if get_random(10000) == 0 则对应的 Query 计数+1
否则就直接扔掉
因为我们不关心具体的 Query 次数,只需要一个相对的排名
该是 Top 10 的还是 Top 10
优化 Prefix → Top 10 的构建速度
循环遍历每一个 Query 然后打擂台的方式非常慢
假设计算资源足够,有什么办法可以优化效率?
Map Reduce
Map: <apple: 1b> → <a: apple,1b> <ap: apple, 1b> …
Reduce: 遍历同一个 Prefix 下的 Queries, 筛选 Top 10
用户输入速度很快怎么办
没有必要都获取 a, ap, app, appl, apple 的 top 10 queries
在 frontend 设置一个 delay
当用户停止输入超过 200ms 时,才发送请求
如何优化响应速度
要应对 1M+ 的 QPS
我们还需要进一步极尽所能的优化这个系统的响应速度
Backend Cache: 每次更新 prefix -> top 10 的数据时都主动更新 Backend Cache
避免更新带来的 Cache miss
避免更新带来的 Cache miss
Frontend-cache & Prefetch: 即在前端进行缓存
Pre-fetch 即预加载一些用户可能搜索的 prefix 和 top 10
Pre-fetch 即预加载一些用户可能搜索的 prefix 和 top 10
如何获得实时热门 Queries
每天重新计算一次 Prefix → Top 10 则可能会有一些延迟
且一些短期内的热门的搜索应该获得更高的权重
构建一套一样的系统,只是查询的内容是最近 2 小时内的热门搜索
这套系统每 2 小时更新一次数据
用户的请求需要汇总普通搜索结果和热门搜索结果
汇总时可以用一些算法来提高热门搜索的权重(2小时内被搜索的次
数肯定会员小于历史被搜索次数)
网页搜索结果的展示也有类似的架构
数据库索引与事务
Scenario 场景
Service 服务
Storage 存储
Scale 升级
评论系统设计
Scenario 场景
具体描述
Service 服务
具体描述
Storage 存储
具体描述
Scale 升级
新鲜事系统
Scenario 场景
并发用户 Concurrent User
日活跃 * 每个用户平均请求次数 / 一天多少秒 = 150M * 60 / 86400~ 100k
峰值 Peak = Average Concurrent User * 3 ~ 300k
快速增长的产品 Fast Growing
MAX peak users in 3 months = Peak users * 2
读频率 Read QPS (Queries Per Second)
300k
写频率 Write QPS
5k
Service 服务
User Service
SQL
FriendShip Service
SQL/NOSQL
Tweet Service
NOSQL
Media Service
File System
图片
Storage 存储
Pull Model
算法
在用户查看News Feed时,获取每个好友的前100条Tweets,合并出前100条News Feed
K路归并算法 Merge K Sorted Arrays
复杂度分析
News Feed => 假如有N个关注对象,则为N次DB Reads的时间 + N路归并时间(可忽略)
Post a tweet => 1次DB Write的时间
缺陷
getNewsFeed(request)
getNewsFeed(request)
• followings = DB.getFollowings(user=request.user)
• news_feed = empty • for follow in followings:
• tweets = DB.getTweets(follow.to_user, 100)
• news_feed.merge(tweets)
• sort(news_feed)
• return news_feed[:100] # 返回前100条
• followings = DB.getFollowings(user=request.user)
• news_feed = empty • for follow in followings:
• tweets = DB.getTweets(follow.to_user, 100)
• news_feed.merge(tweets)
• sort(news_feed)
• return news_feed[:100] # 返回前100条
N次DB Reads非常慢 且发生在用户获得News Feed的请求过程中
postTweet(request, tweet)
• DB.insertTweet(request.user, tweet)
• return success
• return success
Push Model
算法
为每个用户建一个List存储他的News Feed信息
用户发一个Tweet之后,将该推文逐个推送到每个用户的News Feed List中
关键词:Fanout
用户需要查看News Feed时,只需要从该News Feed List中读取最新的100条即可
复杂度分析
News Feed => 1次DB Read
Post a tweet => N个粉丝,需要N次DB Writes
好处是可以用异步任务在后台执行,无需用户等待
缺陷
getNewsFeed(request)
• getNewsFeed(request)
• return DB.getNewsFeed(request.user)
• return DB.getNewsFeed(request.user)
postTweet(request, tweet_info)
• tweet = DB.insertTweet(request.user, tweet_info)
• AsyncService.fanoutTweet(request.user, tweet)
• return success
• AsyncService.fanoutTweet(request.user, tweet)
• return success
异步执行
AsyncService::fanoutTweet(user, tweet)
• followers = DB.getFollowers(user)
• for follower in followers:
• DB.insertNewsFeed(tweet, follower)
• for follower in followers:
• DB.insertNewsFeed(tweet, follower)
followers的数 目可能很大
Pull VS Push
Facebook – Pull
Instagram – Push + Pull
Twitter – Pull
朋友圈
Scale 升级
解决Pull的缺陷
最慢的部分发生在用户读请求时(需要耗费用户等待时间)
在 DB 访问之前加入Cache
Cache 每个用户的 Timeline
N次DB请求 → N次Cache请求 (N是你关注的好友个数)
Trade off: Cache所有的?Cache最近的1000条?
Cache 每个用户的 News Feed
没有Cache News Feed的用户:归并N个用户最近的100条Tweets,然后取出结果的前100条
有Cache News Feed的用户༚ 归并N个用户的在某个时间戳之后的所有Tweets
解决Push的缺陷
浪费更多的存储空间 Disk
与Pull模型将News Feed存在内存(Memory)中相比
Push模型将News Feed存在硬盘(Disk)里完全不是个事儿
Disk is cheap
不活跃用户 Inactive Users
粉丝排序 Rank followers by weight (for example, last login time)
粉丝数目 followers >> 关注数目 following
Push 的挑战
Fanout 的过程可能需要几个小时!
尝试在现有的模型下做最小的改动来优化
比如多加几台用于做 Push 任务的机器,Problem Solved!
对长期的增长进行估计,并评估是否值得转换整个模型
Push 结合 Pull 的优化方案
• 普通的用户仍然 Push
• 将 Lady Gaga 这类的用户,标记为明星用户
• 对于明星用户,不 Push 到用户的 News Feed 中
• 当用户需要的时候,来明星用户的 Timeline 里取,并合并到 News Feed 里
• 将 Lady Gaga 这类的用户,标记为明星用户
• 对于明星用户,不 Push 到用户的 News Feed 中
• 当用户需要的时候,来明星用户的 Timeline 里取,并合并到 News Feed 里
如何定义明星
是不是明星不能在线动态计算,要离线计算
为 User 增加一个 is_superstar 的属性
一个用户被标记为 superstar 之后,就不能再被取消标记
Pull vs Push
什么时候用 Push?
• 资源少
• 想偷懒,少写代码
• 实时性要求不高
• 用户发帖比较少
• 双向好友关系,没有明星问题(比如朋友圈)
• 想偷懒,少写代码
• 实时性要求不高
• 用户发帖比较少
• 双向好友关系,没有明星问题(比如朋友圈)
什么时候用 Pull ?
• 资源充足
• 实时性要求高
• 用户发帖很多
• 单向好友关系,有明星问题
• 实时性要求高
• 用户发帖很多
• 单向好友关系,有明星问题
拓展问题
如何实现 follow & unfollow ?
Follow 一个用户之后,异步地将他的 Timeline 合并到你的 News Feed 中
Merge timeline into news feed asynchronously.
Unfollow 一个用户之后,异步地将他发的 Tweets 从你的 News Feed 中移除
Pick out tweets from news feed asynchronously.
为什么需要异步 Async?
因为这个过程一点都不快呀
异步的好处?
用户迅速得到反馈,似乎马上就 follow / unfollow 成功了
异步的坏处?
Unfollow 之后刷新 News Feed,发现好像他的信息还在
不过最终还是会被删掉的
如何存储 likes?
当有人点赞的时候:
UPDATE like_table SET num_of_likes = num_of_likes + 1 where tweet_id = xxx
当有人取消赞的时候:
UPDATE like_table SET num_of_likes = num_of_likes - 1 where tweet_id = xxx
想要获得一个 Tweet 的点赞数时,因为 num_of_likes 就存在 tweet 里,故无需额外的 SQL Queries
秒杀系统与订票系统设计
Scenario 场景
QPS 分析
平日每秒 1000 人访问该页面。
秒杀时每秒数10万人访问该页面。
QPS 增加 100 倍以上。
商品购买和下单流程
秒杀系统需要解决问题
瞬时大流量高并发
服务器、数据库等能承载的 QPS 有限,如数据库一般是单机 1000 QPS。需要根据业务预估并发量。
有限库存,不能超卖
库存是有限的,需要精准地保证,就是卖掉了 N 个商品。不能超卖,当然也不能少卖了。
黄牛恶意请求
使用脚本模拟用户购买,模拟出十几万个请求去抢购。
固定时间开启
时间到了才能购买,提前一秒都不可以(以商家「京东」「淘宝」的时间为准)。
严格限购
一个用户,只能购买 1 个或 N 个。
需求拆解
商家侧(京东自营、淘宝天猫店家)
新建秒杀活动
配置秒杀活动
用户侧
商品秒杀页面(前端或客户端)
购买
下单
付款
Service 服务
秒杀服务 Seckill Service
商品信息和 库存服务 Commodity Info & Stock Service
订单服务 Order Service
支付服务 Payment Service
Storage 存储
数据库设计
数据流
秒杀操作 减库存
减库存
使用 UPDATE 语句自带的行锁 超卖问题解决了,其他问题呢?
1. 大量请求都访问 MySQL,导致 MySQL 崩溃。
SELECT stock FROM `stock_info` WHERE commodity_id = 189 AND seckill_id = 28;
1. 查询库存余量
UPDATE `stock_info` SET stock = stock - 1 WHERE commodity_id = 189 AND seckill_id = 28 AND stock > 0;
2. 扣减库存 对于抢购活动来说,可能几十万人抢 100 台 iPhone,实际大部分请求都是无效的,不需要下沉到 MySQL。
1. 大量请求都访问 MySQL,导致 MySQL 崩溃。
SELECT stock FROM `stock_info` WHERE commodity_id = 189 AND seckill_id = 28;
1. 查询库存余量
UPDATE `stock_info` SET stock = stock - 1 WHERE commodity_id = 189 AND seckill_id = 28 AND stock > 0;
2. 扣减库存 对于抢购活动来说,可能几十万人抢 100 台 iPhone,实际大部分请求都是无效的,不需要下沉到 MySQL。
库存预热
秒杀的本质,就是对库存的抢夺。
每个秒杀的用户来都去数据库查询库存校验库存,然后扣减库存,导致数据库崩溃。
MySQL 数据库单点能支撑 1000 QPS,但是 Redis 单点能支撑 10万 QPS,
可以考 虑将库存信息加载到 Redis 中。 直接通过 Redis 来判断并扣减库存。
每个秒杀的用户来都去数据库查询库存校验库存,然后扣减库存,导致数据库崩溃。
MySQL 数据库单点能支撑 1000 QPS,但是 Redis 单点能支撑 10万 QPS,
可以考 虑将库存信息加载到 Redis 中。 直接通过 Redis 来判断并扣减库存。
通过 Redis 扣减库存
如果秒杀数量是1万台,或者10万台呢?
因为 Redis 和 MySQL 处理能力的巨大差异。实际下沉到 MySQL 的量还是巨大,MySQL 无法承受。
解决思路
可不可以在通过 Redis 扣库存后,到 MySQL 的请求慢一 点?
解决方案
通过消息队列(Message Queue,MQ)进行削峰(Peak Clipping)操作。
通过消息队列异步地创建订单
子主题
如何限购
Redis 数据校验
使用 Redis 提供的集合数据结构,将扣减 Redis 库存的用户 ID 写入。
流程图
Scale 升级
CDN
限流
前端限流
服务熔断
验证码机制
恶意防刷
黑名单IP地址
黑名单用户ID
用户系统设计
Scenario 场景
注册、登录、查询、用户信息修改
哪个需求量最大?
支持 100M DAU
注册,登录,信息修改 QPS 约
• 100M * 0.1 / 86400 ~ 100
• 0.1 = 平均每个用户每天登录+注册+信息修改
• Peak = 100 * 3 = 300
• 0.1 = 平均每个用户每天登录+注册+信息修改
• Peak = 100 * 3 = 300
查询的QPS 约
• 100 M * 100 / 86400 ~ 100k
• 100 = 平均每个用户每天与查询用户信息相关的操作次数(查看好友,发信息,更新消息主页)
• Peak = 100k * 3 = 300 k
• 100 = 平均每个用户每天与查询用户信息相关的操作次数(查看好友,发信息,更新消息主页)
• Peak = 100k * 3 = 300 k
Service 服务
一个 AuthenticationService 负责登录注册
Session 会话
• 用户 Login 以后,为他创建一个 session 对象
• 并把 session_key 返回给浏览器,让浏览器存储起来
• 浏览器将该值记录在浏览器的 cookie 中
• 用户每次向服务器发送的访问,都会自动带上该网站所有的 cookie
• 此时服务器拿到 cookie 中的 session_key,在 Session Table 中检测是否存在,是否过期
• Cookie:HTTP 协议中浏览器和服务器的沟通机制,服务器把一些用于标记用户身份的信息,传递给 浏览器,浏览器每次访问任何网页链接的时候,都会在 HTTP 请求中带上所有的该网站相关的 Cookie 信息。Cookie 可以理解为一个 Client 端的 hash table。
• 并把 session_key 返回给浏览器,让浏览器存储起来
• 浏览器将该值记录在浏览器的 cookie 中
• 用户每次向服务器发送的访问,都会自动带上该网站所有的 cookie
• 此时服务器拿到 cookie 中的 session_key,在 Session Table 中检测是否存在,是否过期
• Cookie:HTTP 协议中浏览器和服务器的沟通机制,服务器把一些用于标记用户身份的信息,传递给 浏览器,浏览器每次访问任何网页链接的时候,都会在 HTTP 请求中带上所有的该网站相关的 Cookie 信息。Cookie 可以理解为一个 Client 端的 hash table。
一个 UserService 负责用户信息存储与查询
一个 FriendshipService 负责好友关系存储
单向好友关系
• 例子:Twitter、Instagram、微博
• 存在 SQL 数据库时 • 查询x所有的关注对象:
• select * from friendship where from_user_id=x
• 查询x所有的粉丝:
• select * from friendship where to_user_id=x
• 存在 SQL 数据库时 • 查询x所有的关注对象:
• select * from friendship where from_user_id=x
• 查询x所有的粉丝:
• select * from friendship where to_user_id=x
双向好友关系
• 例子:微信,Facebook,WhatsApp
• 方案1:存储为一条数据
• select * from friendship where smaller_user_id = x or bigger_user_id=x
• 问:为什么需要区分 smaller / bigger? • SQL 可以按照这种方案 • NoSQL 很多不支持 Multi-index 不能使用这种方案
• 方案1:存储为一条数据
• select * from friendship where smaller_user_id = x or bigger_user_id=x
• 问:为什么需要区分 smaller / bigger? • SQL 可以按照这种方案 • NoSQL 很多不支持 Multi-index 不能使用这种方案
• 方案2:存储为两条数据
• select * from friendship where from_user_id=x
• NoSQL 和 SQL 都可以按照这种方案
• select * from friendship where from_user_id=x
• NoSQL 和 SQL 都可以按照这种方案
Cassandra
• Cassandra 是一个三层结构的 NoSQL 数据库
• http://www.lintcode.com/problem/mini-cassandra/
• 第一层:row_key
• 第二层:column_key
• 第三层:value
• Cassandra 的 Key = row_key + column_key
• 同一个 row_key + column_key 只对应一个 value
• http://www.lintcode.com/problem/mini-cassandra/
• 第一层:row_key
• 第二层:column_key
• 第三层:value
• Cassandra 的 Key = row_key + column_key
• 同一个 row_key + column_key 只对应一个 value
Row Key
又称为 Hash Key, Partition Key Cassandra
会根据这个 key 算一个 hash 值
然后决定整条数据存储在哪儿 无法进行 Range Query 常用:user_id
会根据这个 key 算一个 hash 值
然后决定整条数据存储在哪儿 无法进行 Range Query 常用:user_id
Column Key
insert(row_key, column_key, value) 任何一条数据,
都包含上面三个部分 你可以指定 column_key 按照什么排序
Cassandra 支持这样的“范围查询”: query(row_key, column_start, column_end) 可以是复合值,如 timestamp + user_id
都包含上面三个部分 你可以指定 column_key 按照什么排序
Cassandra 支持这样的“范围查询”: query(row_key, column_start, column_end) 可以是复合值,如 timestamp + user_id
SQL vs NoSQL
Friendship Table适合什么数据库?
SQL 和 NoSQL 的选择标准是什么?
大部分的情况,用SQL也好,用NoSQL也好,都是可以的
需要支持 Transaction 的话不能选 NoSQL
你想在什么地方偷懒很大程度决定了选什么数据库
SQL:结构化数据,自由创建索引
NoSQL:分布式,Auto-scale,Replica
SQL:结构化数据,自由创建索引
NoSQL:分布式,Auto-scale,Replica
一般一个网站会同时用多种数据库系统
不同的表单放在不同的数据库里
不同的表单放在不同的数据库里
User Table 存在哪儿?
大部分公司选择了 SQL 原因:信任度,Multi-Index
Friendship 存在哪儿?
大部分公司选择了 NoSQL 原因:数据结构简单,都是 key-value 的查询/存储需求 NoSQL效率更高
Storage 存储
Cache
用户系统特点: 读非常多,写非常少
一个读多写少的系统,一定要使用 Cache 进行优化
Best practice: database.set(key, user); cache.delete(key)
巧妙利用 cache 的 ttl(time to live / timeout) 机制
任何一个 cache 中的 key 都不要永久有效,设置一个短暂的有效时间,如 7 天 那么即便在极低概率下出现了数据不一致,也就最多不一致 7 天
即,我们允许数据库和缓存有“短时间”内的不一致,但最终会一致。
扩展练习1: NOSQL存储单向好友关系
Redis
key=user_id
value= set of friend_user_id (粉丝表就是粉丝ID, 关注表就是关注的用户ID)
查看A是否关注了B,使用Redis的SISMEMBER 操作查询A的关注用户里有没有B
Cassandra
row_key=user_id
cloumn_key=friendship_user_id(粉丝表就是粉丝ID, 关注表就是关注的用户ID)
查看A是否关注了B,在关注表中查询row_key=A, cloumn_key=B的数据是否存在
- 扩展练习2: NOSQL存储User
如果使用不支持Muti-Index的NOSQL存储User, 如何同时支持按emial、username、phone等检索用户
User相关的所有数据存储在UserTable中
Redis
key=user_id value=用户信息
Cassandra
row_key=user_id
cloumn_key=任何你想放的信息
value=用户信息
其他再同时创建多张表单, 用作index
Redis
key=emial、username、phone等, value=user_id
Cassandra
row_key=key=emial、username、phone等
cloumn_key=任何你想放的数据
value=其他用户信息
扩展练习3-共同好友
列出A和B之间的共同好友
获取A的好友列表
获取B的好友列表
求交集
优化:
使用缓存储用户的好友列表->两次数据库查询变成两次缓存查询
扩展练习4-六度好友关系
切入点: 大于3度是没有现实意义的,对于超出3度的,直接显示3度+
方法
提前算好所有的一度关系和二度关系在NQSQL中
一度表:
key=user_id,value=所有的一度关系(直接好友)
二度表:
key=user_id,value=所有的二度度关系(间接好友)
一度关系和二度关系并集: set1
对于指定不超过10个需要查询六度关系的用户列表查询他们的一度关系、得到set2(10次query)
在set1和set2求交集,根据交集结果推导10人和我的关系距离
Scale 升级
短网址系统设计
Scenario 场景
长网站->短网站
根据LongURL生成一个ShortURL
http://www.jiuzhang.com=>http://bit.ly/1UIoQB6
根据 Short URL 还原 Long URL,并跳转
http://bit.ly/1UIoQB6=>http://www.jiuzhang.com
场景
Long Url 和 Short Url 之间必须是一一对应的关系么?
Short Url 长时间没人用需要释放么?
不同的要求设计出来的系统完全不一样!
QPS
询问面试官微博日活跃用户: 约100M
推算产生一条Tiny URL的QPS
• 假设每个用户平均每天发 0.1 条带 URL 的微博
• Average Write QPS = 100M * 0.1 / 86400 ~ 100
• Peak Write QPS = 100 * 2 = 200
推算点击一条Tiny URL的QPS
• 假设每个用户平均点1个Tiny URL
• Average Read QPS = 100M * 1 / 86400 ~ 1k
• Peak Read QPS = 2k
推算每天产生的新的 URL 所占存储
• 100M * 0.1 ~ 10M 条 • 每一条 URL 长度平均 100 算,一共1G
• 1T 的硬盘可以用 3 年
Service 服务
URL Service
函数设计
• UrlService.encode(long_url)
• UrlService.decode(short_url)
API设计
• GET /<short_url>
• return a Http redirect response
• POST /data/shorten/
• Data = {url: http://xxxx }
• Return short url
Storage 存储
SQL vs NoSQL
算法
如何将 Long Url 转换为一个 6位的 Short Url?
随机生成ShortURL + 数据库去重
随机一个 6 位的 ShortURL,如果没有被用过,就绑定到该 LongURL
• 优点:实现简单
• 缺点:生成短网址的速度随着短网址越来越多变得越来越慢
需要根据 Long 查询 Short,也需要根据 Short 查询 Long。
如果选择用 SQL 型数据库,表结构如下:
并且需要对 shortKey 和 longURL 分别建索引(index)。
并且需要对 shortKey 和 longURL 分别建索引(index)。
也可以选用 NoSQL 数据库,但是需要建立两张表(大多数 NoSQL 不支持多重索引 Multi-index) 以 Cassandra 为例子
第一张表:根据 Long 查询 Short
row_key=longURL, column_key=ShortURL, value=null or timestamp
第二张表:根据 Short 查询 Long
row_key=shortURL, column_key=LongURL, value=null or timestamp
进制转换 Base62
• Base62
• 将 6 位的short url看做一个62进制数(0-9, a-z, A-Z)
• 每个short url 对应到一个整数
• 该整数对应数据库表的Primary Key —— Sequential ID
• 6 位可以表示的不同 URL 有多少?
• 5 位 = 625
= 0.9B = 9 亿 • 6 位 = 626
= 57 B = 570 亿 • 7 位 = 627
= 3.5 T = 35000 亿
• 将 6 位的short url看做一个62进制数(0-9, a-z, A-Z)
• 每个short url 对应到一个整数
• 该整数对应数据库表的Primary Key —— Sequential ID
• 6 位可以表示的不同 URL 有多少?
• 5 位 = 625
= 0.9B = 9 亿 • 6 位 = 626
= 57 B = 570 亿 • 7 位 = 627
= 3.5 T = 35000 亿
优点:效率高
• 缺点:依赖于全局的自增ID
需要根据 Long 查询 Short,也需要根据 Short 查询 Long。
因为需要用到自增ID(Sequential ID),因此只能选择使用 SQL 型数据库。 表单结构如下,shortURL 可以不存储在表单里,因为可以根据 id 来进行换算
Scale 升级
如何提速
• 利用缓存提速(Cache Aside) • 缓存里需要存两类数据:
• long to short(生成新 short url 时需要)
• short to long(查询 short url 时需要)
利用地理位置信息提速
优化服务器访问速度
• 不同的地区,使用不同的 Web 服务器 • 通过DNS解析不同地区的用户到不同的服务器
• 优化数据访问速度
• 使用Centralized MySQL+Distributed Memcached
• 一个MySQL配多个Memcached,Memcached跨地区分布
Sharding
按数据分片
如果用 ID / ShortUrl 做Sharding Key
• 做 long to short 查询的时候,只能广播给 N 台数据库查询
• 为什么会需要查 long to short?创建的时候避免重复创建 • 如果不需要避免重复创建,则这样可行
如果用 Long Url 做Sharding Key
• 做 short to long 查询的时候,只能广播给 N 台数据库查询
Sharding Key
如果一个 Long 可以对应多个 Short
• 使用 Cache 缓存所有的 Long to Short
• 在为一个 Long Url 创建 Short Url 的时候,如果 cache miss 则去直接创建新的 short url 即可
如果一个 Long 只能对应一个 Short:
如果使用随机生成 Short Url 的算法
• 两张表单,一张存储 Long to Short,一张存储 Short to Long
• 每个映射关系存两份,则可以同时支持 long to short 和 short to long 的查询
如果使用 base62 的进制转换法
• 这里有一个很严重的问题是,多台机器之间如何维护一个全局自增的 ID?
• 一般关系型数据库只支持在一台机器上实现这台机器上全局自增的 ID
• 一般关系型数据库只支持在一台机器上实现这台机器上全局自增的 ID
基于 base62 的方法下的 Sharding key 策略
• 使用 Hash(long_url) % 62 作为 Sharding key
• 并将 Hash(long_url) % 62 直接放到 short url 里 • 如果原来的 short key 是 AB1234 的话,现在的 short key 是 • hash(long_url) % 62 + AB1234
• 如果 hash(long_url) % 62 = 0 那就是 0AB1234
• 这样我们就可以同时通过 short_url 和 long_url 得到 Sharding Key
• 缺点:数据库的机器数目不能超过 62
按地域分片
• 中国的用户访问时,会被DNS分配中国的服务器 • 中国的用户访问的网站一般都是中国的网站
• 所以我们可以按照网站的地域信息进行 Sharding
• 如何获得网站的地域信息?只需要将用户比较常访问的网站弄一张表就好了
• 中国的用户访问美国的网站怎么办? • 那就让中国的服务器访问美国的数据好了,反正也不会慢多少
• 中国访问中国是主流需求,优化系统就是要优化主要的需求
优惠券系统设计
Scenario 场景
发券
发券的方式:同步发送 or 异步 发送
领券
谁能领:所有用户 or 指定的用户
领取上限:一个优惠券最多能领取 多少张?
领取方式:用户主动领取 or 自动 发放被动领取
领取上限:一个优惠券最多能领取 多少张?
领取方式:用户主动领取 or 自动 发放被动领取
用券
作用范围:商品、商户、类目
计算方式:是否互斥、是否达到 门槛等
计算方式:是否互斥、是否达到 门槛等
商家侧
创建优惠券
发送优惠券
用户侧
领取优惠券
下单
使用优惠券
支付
Service 服务
支付服务 Payment Service
订单服务 Order Service
优惠券服务 Coupon Service
建券
1. 新建规则
INSERT INTO rule (name, type, rule_content) VALUES(“满减规则”, 0, '{ threshold: 100 amount: 10 ...... }');
. 新建优惠券批次
INSERT INTO coupon_batch (coupon_name, rule_id, total_count ) VALUES(“劳斯莱斯5元代金券”, 1010, 10000);
发券
触达系统
短信
邮件
站内信
我们先考虑用户量很少的情况,如果商家要给所有人发站内信,则先遍历用户表,再按照用户表 中的所有用户依次将站内信插入到 message 表中。这样,如果有100个用户,则群发一条站内信 要执行100个插入操作。
如果系统的用户量达到上万呢?
这样发一条站内信VX:,s就tu要dy重32复2其插他入均上为万翻条录数倒据卖。而且这上万条数据的content是一样的,假设一条站内信占100个字节,发一次站内信就要消耗十几MB。因此我们可以将原来的表拆成两个表。
信息表 message Table
信息内容表message_contentTable
系统
发站内信的时候,只在message_content插入站内信的主体内容,message里不插入记录
用户
用户登录后,首先查询message_content中的那些没有在message中有记录的数据,表示是未读的站内信。在查阅站内信的内容时,再将相关的记录插入到message中。
触达服务 Touch Service
难点
券的分布式事务,使用券的过程会出现的分布式问题分析
如何大批量给用户发券
1. 运营提供满足条件的用户文件,上传到发券管理后台并选择要发送的 优惠券
2. 管理服务器根据用户ID和券批次ID生成消息,发送到消息中间件中
3. 优惠券服务器消费消息
INSERT INTO coupon(user_id,coupon_id,batch_id)VALUES(1001,66889,1111);
UPDATEcoupon_batchSETtotal_count=total_count-1,assign_count=assign_count+1
WHEREbatch_id=1111 AND total_count>0;
WHEREbatch_id=1111 AND total_count>0;
如何防止超发
如何限制券的使用条件
1. 返回可用券
2. 选择可用券,并返回结果
SELECTbatch_idFROMconpou
WHEREuser_id=1001ANDstatus=0;
SELECTrule_idFROMcoupon_batch WHEREbatch_id=1111;
WHEREuser_id=1001ANDstatus=0;
SELECTrule_idFROMcoupon_batch WHEREbatch_id=1111;
SELECTname,type,rule_contentFROMrule WHERErule_id=1010;
如何防止用户重复领券
1. 在领券前先查缓存
语法:SISMEMBER KEY VALUE 作用:判断成员元素是否是集合的成员。 实例:SISMEMBER batch_id:1111:user_id 1001
2. 领券
3. 领券后更新缓存
语法:SADD KEY VALUE1......VALUEN 作用:将一个或多个成员元素加入到集合中,已经存在于集合的成员元素将被忽略。 实例:SADD batch_id:1111:user_id 1001
Storage 存储
表单设计
券批次表 coupon_batch Table
规则表 rule Table
'{ 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 Table
Scale 升级
快过期券提醒
新 增通 知 表
通知信息表 Notify_msg Table
1.在创建优惠券的时候就将需要提醒 的记录插入提醒表中notify_msg
2.把用户ID+批次ID+通知日期 作为 一个唯一索引,防止同一个批次有重 复的记录通知,保证每天只会被通知 一次
3.建立notify_time,通知时间索引,每 日的通知扫描通过该索引列查询,通过 索引列来提高查询的效率
4.通知完成后该表中的数据变 失去了意义,通过定时任务将 该数据删除
数据库层面优化
券批次表
batch_id
优惠券表
coupon_id
user_id
batch_id
order_id
发券接口限流保护
前端限流
点击一次后,按钮 短时间内置灰
后端限流
部分请求直接 跳转到「繁忙页」
数据库拆分与一致性算法
Scenario 场景
当访问量越来越大以后,如何让你的系统 Scale?
数据拆分 Sharding
纵向拆分 Vertical Sharding
User Table 放一台数据库
Friendship Table 放一台数据库
Message Table 放一台数据库
横向拆分 Horizontal Sharding
不一致 hash
一致性 Hash 算法
无论是 SQL 还是 NoSQL 都可以用这个方法进行 Sharding
大部分 NoSQL 都帮你实现好了这个算法,帮你自动 Sharding
很多 SQL 数据库也逐渐加入 Auto-scaling 的机制了,也开始帮你做
自动的 Sharding
一致性哈希算法 Consistent Hashing
• 将取模的底数从 360 拓展到 2^64
• 将 0~2^64-1 看做一个很大的圆环(Ring)
• 将数据和机器都通过 hash function 换算 到环上的一个点
• 数据取 key / row_key 作为 hash key
• 机器取MAC地址,或者机器固定名字如
database01,或者固定的 IP 地址
• 每个数据放在哪台机器上,取决于在
Consistent Hash Ring 上顺时针碰到的
下一个机器节点
虚拟节点 Virtual Node
引入分身的概念(Virtual nodes)
• 一个实体机器(Real node) 对应若干虚拟机器(Virtual Node) • 通常是 1000 个 • 分身的KEY可以用实体机器的KEY+后缀的形式
• 如 database01-0001
• 好处是直接按格式去掉后缀就可以得到实体机器
• 用一个数据结构存储这些 virtual nodes
• 支持快㏿的查询比某个数大的最小数
• 即顺时针碰到的下一个 virtual nodes
• 哪种数据结构可以支持?
User Table 如何 Sharding
• User Table Sharding 之后,多台数据库无法维护一个全局的自增ID怎么办? • 手动创建一个 UUID 来作为用户的 user_id
• 创建用户时还没有用户的 user_id,如何知道该去哪个数据库创建呢?
• Web Server 负责创建用户的 user_id,如用 UUID 来作为 user_id
• 创建之后根据 consistent_hash(user_id) 的结果来获得所在的实体数据库信息
• 更进一步的问题:如果 User Table 没有 sharding 之前已经采用了自增ID该怎么办? • UUID 通常是一个字符串,自增 id 是一个整数,并不兼容
• 单独用一个 UserIdService 负责创建用户的 ID,每次有新用户需要创建时,问这个 Service 要一个新的
ID。这个 Service 是全局共享的,专门负责创建 UserId。负责记录当前 UserId 的最大值是多少了,然后每
次 +1 即可。这个 Service 会负责加锁来保证数据操作的原子性(Atomic)。
• 因为创建用户不是一个很大的 QPS,因此这个做法问题不大
Friendship Table 如何 Sharding
Session Table 如何 Sharding
News Feed / Timeline 按照什么
Sharding?
Sharding?
数据复制 Replica
Backup
• 一般是周期性的,比如每天晚上进行一次备份
• 当数据丢失的时候,通常只能恢复到之前的某个时间点
• Backup 不用作在线的数据服务,不分摊读
• 当数据丢失的时候,通常只能恢复到之前的某个时间点
• Backup 不用作在线的数据服务,不分摊读
Replica
• 是实时的, 在数据写入的时候,就会以复制品的形式存为多份
• 当数据丢失的时候,可以马上通过其他的复制品恢复
• Replica 用作在线的数据服务,分摊读
Service 服务
Storage 存储
Scale 升级
分布式文件系统GFS
Scenario 场景
Service 服务
Storage 存储
Scale 升级
文档协调编辑系统设计
Scenario 场景
Service 服务
Storage 存储
Scale 升级
分布式数据库BigTable
Scenario 场景
Service 服务
Storage 存储
Scale 升级
0 条评论
下一页