Skip to main content

5. Design User System

info

设计用户系统,实现功能包括注册、登录、用户信息查询、好友关系的存储

Overview

  • Step1. Scenario 场景
    • 需要设计哪些功能,有哪些场景
    • Ask / Features / QPS / DAU / Interfaces
  • Step2. Service 服务
    • 将大系统拆分为小服务
    • Split / Application / Module
  • Step3. Storage 存储
    • 数据如何存储和访问
    • Schema / Data / SQL / NoSQL / File System
  • Step4. Scale 升级
    • tradeoff 取舍并解决缺陷,处理可能遇到的问题
    • Sharding / Optimize / Specical Case

Step1. Scenario

Assumption and QPS

  • 100M DAU
  • QPS of register+log in+update info
    • Each user: register+log in+update info = 0.1 time/day = 1 time/10days
    • 100M * 10% / 86400s/day = 100/day
    • Peak = 100 * 3 = 300/day
  • QPS of search
    • Each user: 与查询用户信息相关的操作次数(查看好友 发信息 更新消息主页 = 100 times/day
    • 100M * 100 / 86400s/day = 100k/day
    • Peak = 100k * 3 = 300k/day

Read heavy system

Step2. Service

  • AuthenticationService 登录注册服务
  • UserService 用户信息存储与查询服务
  • FriendshipService 好友关系存储服务

Authentication Service

Session 会话

  • Session 记录过期以后,服务器会主动删除么? 不会.
  • 只支持在一台机器登陆和在多台机器同时登陆的区别是什么? 多台机器登录时创建多个 session_key.
  • Session 适合存在什么数据存储系统中? SQL DataBase + Cache 加速访问.

Friend Service

单向好友关系

Twitter, Instagram, weibo

  • 存于 SQL DataBase
-- 查询x所有的关注对象
select * from friendship where from_user_id = x;

-- 查询x所有的粉丝
select * from friendship where to_user_id = x;

双向好友关系

Wechat, Facebook, WhatsApp

  • 方案 1: 存储为一条数据
    • 为什么区分 smaller / bigger?
    • NoSQL 很多不支持 Multi-index 不能使用这种方案
select * from friendship
where small_user_id = x or bigger_user_id = x;
  • 方案 2: 存储为两条记录
    • NoSQL, SQL 都可以按照这种方案
select * from friendship where from_user_id = x;

Step3. Storage

Cache

E.g., UserService 操作

class UserService:

def getUser(self, user_id):
key = "user::%s" % user_id
user = cache.get(key)
if user:
return user
user = database.get(user_id)
cache.set(key, user)
return user

def setUser(self, user):
key = "user::%s" % user_id
cache.delete(key)
database.set(user)

多线程多进程下的数据不一致问题

Solution

class UserService:

def getUser(self, user_id):
key = "user::%s" % user_id
user = cache.get(key)
if user:
return user
user = database.get(user_id)
cache.set(key, user)
return user

def setUser(self, user):
key = "user::%s" % user_id
# change the action order
database.set(user)
cacahe.delete(key)

Cache handles data consistency

  • 巧妙利用 cache 的 ttl(time to live / timeout) 机制。任何一个 cache 中的 key 都不要永久有效,设置一个短暂的有效时间,如 7 天
  • 那么即便在极低概率下出现了数据不一致,也就最多不一致 7 天。即,我们允许数据库和缓存有“短时间”内的不一致,但最终会一致 Eventual consistency

Cache Aside

  • Web Server 分别与 DB 和 Cache 进行沟通
  • DB 和 Cache 直接不直接沟通
  • 业界 E.g., Memcached + MySQL
  • 是业界使用较多的方案,Cache 和 DB 可以自由的搭配组合

Cache Through

  • Web Server 只和 Cache 沟通
  • Cache 负责与 DB 通信,将数据持久化
  • 业界 E.g., Redis (可以理解为 Redis 里包含了 Cache + DB)
  • Cons: Redis 支持单纯的 key-value 存储结构,无法适应复杂的应用场景

Cassandra - 典型 NoSQL 数据结构

Cassandra 是一个三层结构的 NoSQL 数据库

  • 第一层: row_key
    • 又称为 Hash Key, Partition Key
    • Cassandra 会根据这个 key 算一个 hash 值, 然后决定整条数据存储在哪儿
    • 无法进行 Range Query
    • 常用: user_id
  • 第二层: column_key
    • 用于排序, 如支持"范围查询" query(row_key, column_start, column_end)
    • 可以是复合值,如 timestamp + user_id
    • NoSQL 的 column 是动态的,无限大,可以随意添加
  • 第三层: value
    • 一条数据一般以 grid 为单位,row_key + column_key + value = 一条数据

Cassandra - Friendship Table 存储

Cassandara - NewsFeed 存储

DataBase 选择原则

  • 需要支持 Transaction 的话不能选 NoSQL
  • 擅长的方面
    • SQL: 结构化数据,自由创建索引
    • NoSQL: 分布式,Auto-scale,Replica
  • 同时使用多种数据库系统,不同的表单放在不同的数据库里
  • User table 选择 SQL: safety, 信任度, Multi-Index.
  • Friendship table 选择 NoSQL: 数据结构简单, 都是 key-value 的查询/存储需求, NoSQL 效率更高.