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 之间不直接沟通.

Web Server
Cache
DB

Cache Through#

Cache 负责 DB 的沟通, 将数据持久化. 服务器只跟 Cache 沟通.

Web Server
Cache
DB

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 存放单向好友关系#

  1. 查询某个人的关注列表

  2. 查询某个人的粉丝列表

  3. 查询 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) 原理

http://bit.ly/1mDs0Yh

Scaling Memecache at Facebook

http://bit.ly/1UlpbGE

Coach Bash Architecture

http://horicky.blogspot.in/2012/07/couchbase-architecture.html

Least Frequently Used Cache (LFU)

http://dhruvbird.com/lfu.pdf

作者:Kohn

出处:https://www.cnblogs.com/geraldkohn/p/17091098.html

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   kohn  阅读(35)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
more_horiz
keyboard_arrow_up dark_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示