3 用户系统设计
用户系统设计
4S#
Scenario#
-
注册, 登录, 查询, 用户信息修改
-
支持 100M DAU
-
注册, 登录, 信息修改 QPS 约 100M * 0.1 * 3 / 86400 = 300
- 0.1 = 平均每个用户每天登录 + 注册 + 信息修改
-
查询 QPS 约 100M * 100 * 3 / 86400 = 300k
- 100 = 平均每个用户每天与查询用户信息相关的操作次数 (查看好友, 发消息, 更新消息主页)
-
Service#
-
负责登录注册 (鉴权)
-
负责用户信息存储与查询
-
负责好友关系的存储与查询
Storage#
-
MySQL / PosgreSQL 等 SQL 数据库的性能: 1k
-
MongoDB / Cassandra 等硬盘型 NoSQL 数据库性能: 10k QPS
-
Redis / Memcached 等内存型 NoSQL 数据库性能: 100k ~ 1M
-
以上数据根据机器性能和硬盘数量及硬盘读写速度会有区别
UserService#
读多写少#
读多写少, 需要用 Cache 进行优化. Cache 不一定是指内存, 一般来说: 内存可以做为硬盘的 Cache, 硬盘可以作为网络的 Cache. CPU 也是有 Cache 的.
为了避免缓存与数据库数据不一致的情况, 需要先更新 DB, 再删除 Cache. 下面说几个导致数据不一致的操作:
- 先删除 Cache, 再更新 DB.
线程 A 删除缓存后紧接着线程 B 就读取了这个信息, 发现缓存没有, 就从数据库读取, 读取后将信息又重新写入到缓存中, 这样就导致了缓存与数据库信息不一致.
- 更新 DB, 再更新 Cache.
线程 A 更新完 DB 后, 线程 B 又更新了 DB, 紧接着线程 B 就更新了 Cache, 然后线程 A 才更新 Cache. 这样就导致了 DB 中是线程 B 设置的值, 而 Cache 中是线程 A 设置的值.
那先更新 DB, 再删除 Cache 虽然是业界常用的方法, 会出现什么问题?
- 在多线程, 多进程的情况下, 仍然会出现问题.
class UserService:
def getUser(self, user_id):
key = 'user::%s' % user_id
user = cache.get(user)
if user:
return user
user = database.get(user_id)
cache.set(key, user)
return user
def setUser(self, user):
key = 'user::%s' % user_id
database.set(user)
cache.delete(key)
如果线程 A 执行了 getUser(), 执行到
user = database.get(user_id)
cache.set(key, user)
但是线程 B 执行完毕了 setUser(), 这样 Cache 中会放入旧数据.
- DB set 成功, Cache set 失败
增加 cache 更新重试机制(常用): 如果 cache 服务当前不可用导致缓存删除失败的话,我们就隔一段时间进行重试,重试次数可以自己定。如果多次重试还是失败的话,我们可以把当前更新失败的 key 存入队列中,等缓存服务可用之后,再将缓存中对应的 key 删除即可。
但是这两种情况发生的概率都很低.
读少写多#
读少写多有两种解决方案:
Cache Aside#
服务器分别与 Cache 和 DB 进行沟通, DB 与 Cache 之间不直接沟通.
Cache Through#
Cache 负责 DB 的沟通, 将数据持久化. 服务器只跟 Cache 沟通.
Cache Through 的缺点在于: Redis 只支持 k-v 存储结构, 无法适应复杂的应用场景. 所以业界通常使用 Cache Aside 的方式较多 (小厂), 大厂可能去搭建自己的 Cache Through.
用户登录注册服务#
Session 会话#
用户 Login 之后, 会为他创建一个 session 对象. 并将 session_key 返回给浏览器, 让浏览器存储起来. 浏览器会将该值记录到 Cookie 中. 用户每次向服务器发起的访问, 都会自动带上网站的 Cookie. 此时服务器拿到 Cookie 中的 session_key, 在 Session_table 中检测是否存在, 是否过期. Cookie 是 HTTP 协议中浏览器和服务器的沟通机制, 服务器会把一些用于标记用户身份的信息, 传递给浏览器, 浏览器每次访问任何网页链接的时候, 都会在 HTTP 请求上带上所有该网站相关的 Cookie 信息. Cookie 就是一个 k-v 的表.
Session Table | ||
---|---|---|
session_key | string | 一个 hash 值, 全局唯一, 无规律 |
user_id | Foreign key | 指向 User Table |
expired_at | timestamp | 什么时候过期 |
Session 三问#
- Session 记录过期以后, 服务器会主动删除吗?
不需要, 会被更新的
- 只支持在一台设备登录和支持多台设备同时登录的区别是什么?
如果只允许登录一台设备, 那么登录时需要更新 Session Table 中的 session_key. 如果允许多台设备, 那么登录时就新建或者更新对应设备的一条记录(可以在 Session Table 中新增一个设备字段)
- Session 适合存在什么数据存储系统中
可以放在 Cache 中, 因为它即使没了顶多让用户再登录一次.
Friendship Service#
单项好友关系#
Twitter, Instagram, weibo
- 存在 SQL 型数据库中
Friendship Table | ||
---|---|---|
from_user_id | Foreign key | 用户主体 |
to_user_id | Foreign key | 用户关注了谁 |
查询 x 用户所有关注的对象:
select * from friendship where from_user_id = x;
查询 x 用户所有的粉丝:
select * from friendship where to_user_id = x;
- 存在 NoSQL 数据库中
双向好友关系#
weibo, facebook, whatsApp
- 方案1: 存储为一条数据
smaller_user_id | bigger_user_id |
---|---|
1 | 2 |
1 | 3 |
2 | 3 |
查询 x 用户的所有好友(双向的): select * from friendship where smaller_user_id = x or bigger_user_id = x;
为什么需要区分 smaller/bigger user id
因为这样一个相互的关系存放一条记录即可
缺点
SQL 可以支持这样的方案, 但是 NoSQL 不支持这种方案, 因为很多 NoSQL 都是 k-v 结构的, 只有一个索引, 不能支持两个字段都是索引的情况.
- 方案2: 存储为两条数据
from_user_id | to_user_id |
---|---|
1 | 2 |
2 | 1 |
1 | 3 |
3 | 1 |
2 | 3 |
3 | 2 |
查询 x 用户的所有好友(双向的): select * from friendship where from_user_id = x; 好处在于: SQL 和 NoSQL 都可以按照这种方案.
那种更好呢?#
方案2更好, 方案1查询的时候有 or 操作, 就相当于执行了两遍 select ... 然后将结果并在一起. 而方案2虽然一个相互关系需要两条数据, 但是一个 select 就可以查到结果, 时间开销小. disk is cheap. 空间很便宜, 时间很宝贵.
将关系型数据放入内存型 NoSQL 中如 Redis, 需要在经常查询的字段上建立 key, 比如在 from_user_id 上面建立 key. key: "from_user_id::%s", value: json.
数据库选择原则#
-
大部分情况下 SQL, NoSQL 都可以
-
需要事务则不可以选择 NoSQL.
- 不同表单可以放在不同数据库中
大部分公司将 UserService 放在了 SQL 中, 将 Friendship 放在了 NoSQL 中. 原因是 key-value 查询, 比较简单.
Scale#
扩展练习1: NoSQL 存放单向好友关系#
-
查询某个人的关注列表
-
查询某个人的粉丝列表
-
查询 A 是否关注了 B
需要两张表单, 一张存储粉丝, 一张存储关注.
Redis:
key = user_id
value = set of friend_user_id (粉丝表单中就是粉丝 id, 关注表单中就是关注用户的 id)
使用 SISMEMBER 来查询 A 是否关注了 B
扩展练习2: NoSQL 存储 User#
如果使用不支持 Multi-index 的 NoSQL 存储 User, 如何同时支持按照email, username, phone, id 来检索用户?
同时创建多种表单, key = index(email, username, phone, id), value = user_id. 就是把 Redis 当作一个索引, User 相关的信息还是放在 SQL 中.
扩展练习3: 共同好友#
列出 A 和 B 之间的共同好友
Redis 中 key = user_id, value = set of friend_user_id. 查询 A 和 B 的共同好友就是查询两个 set 集合之后取交集.
扩展练习4: Linkedln 六度关系#
Linkedln 上有一个功能是显示你和某人之间的几度关系 (通过多少个朋友能认识) 请设计这个功能?
可以使用宽度优点搜索吗?
提前算好所有的一度和二度关系并存储到 NoSQL 中
-
一度表: key=user_id, value=set of friend_user_id
-
二度表: key=user_id, value=set of friend_user_id
查询我的所有一度和二度关系得到我的三度关系, 剩下的显示三度+即可.
扩展阅读#
Dynamo DB -- 理解分布式数据库 (NoSQL) 原理
Scaling Memecache at Facebook
Coach Bash Architecture
http://horicky.blogspot.in/2012/07/couchbase-architecture.html
Least Frequently Used Cache (LFU)
作者:Kohn
出处:https://www.cnblogs.com/geraldkohn/p/17091098.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南