尽管此刻没有如愿以偿找到合适的ft,但我希望能够充分认识到自己的不足,积蓄力量。相信在不久的将来会有爆发的机会。
---------------------------------------------------------------------------------------------
知识的广度(component, 目的)和深度(sde1: 3年以内,sde2: 4年以上,sde3:team lead,org impact可小可大,深度,3+:org lead, 领域见解)
设计用户系统,实现功能包括注册、登陆、用户信息查询,好友关系存储
Scenario:
注册,登陆,查询(需求量大),用户信息修改
支持100M DAU
注册,登陆,信息修改QPS
100M * 0.1 / 86400 ~100
0.1 = 平均每个用户每天登陆+注册+信息修改
peak = 100 * 3 = 300 -> Mysql
查询的QPS
100M * 100 / 86400 ~100k
100 = 平均每个用户每天查询用户信息相关的操作次数(查看好友,发信息,更新消息主页)
peak = 100k * 3 = 300k ->Redis
Service:
一个authenticationService负责登陆注册
一个userService负责用户信息存储和查询
一个friendshipService负责好友关系存储
Storage:
MySql/PosgreSql等SQL数据库性能: 1k QPS
MongoDB/Cassandra等硬盘型Nosql数据库性能:10k QPS
Redis/Memcached等内存型Nosql数据库性能:100k~1m QPS
用户系统特点:读非常多,写非常少。读多写少,一定用cache进行优化。
Cache: 把之后要查询的东西先存一下。无需重新计算和存取数据库。可以理解为hashmap, key-value结构。提供协议,淘汰机制(LRU/LFU cache)
- Memcached(纯内存不支持数据存储化,断电后数据没有)
- Redis(支持数据持久化)
Cache不一定在内存,没有指定存在怎样的存储介质中。访问远端数据/计算量大的工作file system可以做cache, cpu也有cache。
Cache server cache, frontend/client/browser有客户端cache。
Memcached
cache.set("key","value")
cache.get("key")
cache.set("foo",1,ttl=60) 这个key只能活60s
out of memory key may be evicted by cache
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(key,user)
数据库和cache不一致问题,出现脏数据dirty data
多线程多进程下的数据不一致: process1: setUser, cachedelete() process2: getUser cache.get() //none db.get() cache.set() // 更新老数据 process 1: db.set() // 写入新数据
解决方法:数据库和缓存加锁不可以,因为是两台机器,两套系统,要用分布式锁,会导致存取效率降低,得不偿失。
best practice:
database.set(key,user)
cache.delete(key)
多线程多进程下的数据不一致: process1: setUser, db.set() process2: setUser db.set() // 更新老数据 process 1.2: cache.delete()
process1: setUser, db.set()新数据 cache.delete()失败老数据
原因:读多写少,cache heat rate:98%+
cache ttl 7天,基地概率下出现数据不一致,也就最多不一致7天
写多读少,写时删除cache, 读取时cache miss,无读的优化效果。写改db,多db,分摊写请求
cache aside: db <-> web server<-> cache memcached + mysql
cache through: web server <-> [cache <-> DB] redis只支持单纯的kv存储结构,无法适应复杂的应用场景
Authentication service (session, cookie)
session table(存在cache (+ db))
session_key string //全局唯一,uuid
user_id foreign key // 指向user table
expire_at timestamp //什么时候过期
<device_key>
- login以后创建session对象
- 并把session_key返回给浏览器,让浏览器存储起来
- 浏览器将该值记录在浏览器的cookie中(哈希表)
- 用户每次向服务器发送的访问,都会自动带上该网站所有的cookie
- 此时服务器拿到cookie中的session_key,在session table中检测是否存在,是否过期
friendship service
单向
friendship table
from_user_id foreign key 用户主体
to_user_id foreign key 被关注的人
select * from friendship where from_user_id = x 查关注对象
select * from friendship where to_user_id = x 查粉丝
双向
smaller_user_id bigger_user_id
1 2
存储容量小一半,查询 select * from friendship where smaller_user_id = x or bigger_user_id = x 效率慢
or
from_user_id to_user_id
1 2
2 1
select * from friendship where from_user_id = x查询快,时间要求》空间要求
写入事务操作,同时成功同时失败
Cassandra为例的nosql数据库
第一层:row_key
第二层:column_key
第三层:value
Cassandra 的key = row_key + column_key, 只对应一个value
结构化信息serialize到value存储
row key/hash key/partition key/sharding key: 数据存在哪个机器上的依据,nosql实现分布式多台机器存储;常用:user_id
inset(row_key, column_key, value)
column key: 是排序的,row_key等于x, 进行范围查询query(row_key, column_start, column_end); 可以是复合值,如timestamp + user_id
sql: 以row为单位(取出row作为一条数据),每一列是一种属性
nosql: column是动态的,无限大,以格子为单位,row_key + column_key + value = 一条数据
friendship table在cassandra存储
row key: user_id1 user_id2
column key: friendship_user_id2 friendship_user_id3 friend_user_id1
value: is_mutual_friend, is_blocked, timestamp is_mutual_friend, is_blocked, timestamp
newsfeed在cassandra存储
row_key: owner_id1
column_key: created_at1,tweet_id1 created_at2,tweet_id2 拿tweet_id从cache里读数据
value: tweet_data1 tweet_data2
数据库选择原则1: 大部分情况,sql, nosql都可以
数据库选择原则2: 支持transaction,不能选nosql。在一台机器上操作。
数据库选择原则3: sql-结构化数据,自由创建索引。nosql-分布式,auto-scale自动拓展,replica
数据库选择原则4: 一个网站同时用多种数据库系统。不同的表单放在不同的数据库
user table - sql: 信任度, multi-index
friendship - nosql: 结构简单,key-value的查询/存储需求,nosql效率更高
paper:
练习1: nosql存单项好友关系:需要两张表单,一张存粉丝,一张存关注。
redis:
key = user_id
value = set of friend_user_id(粉丝表就是粉丝ID,关注表就是关注id) 相当于hashset
查a是否关注了b: redis的sismember查询b是否在value(a关注的人)中
Cassandra:
row_key = user_id
column_key = friend_user_id(粉丝表就是粉丝ID,关注表就是关注id)
value=其他想同时存储的东西,如关注时间
查a是否关注了b: 在关注表中查询row_key = A, column_key = B的数据是否存在
练习2: nosql 存储user。如果使用不支持multi-index的nosql存储user。如何同时支持按照email, username, phone, id来检索用户。
redis: key = user_id, value = 用户信息
Cassandra: row_key = user_id, column_key = 任何信息,value = 其他用户信息
创建表单用作index,去查到user_id
redis: key = email/phone/username, value = user_id
Cassandra: row_key = email/phone/username, column_key = 任何信息,value = 其他用户信息
练习3: A和B之间的共同好友
- A的好友
- B的好友
- 求交集
读多写少,使用缓存存储用户的好友列表,而不是两次数据库查询
练习4: 好友几度关系BFS
细化需求:
- 查询你和不超过10个人之间的关系
- 用户数量集>100M
- 平均好友数1000个
- 期望的db query次数为常数级(<20次query)
- >3度没有现实意义 3+
双向BFS,
a查好友,一度
b查好友,一度
查并集,二度
offline提前算好所有的一度和二度并存储在nosql,
A 一度 - 1次db
二度 - 1次db 交集3度
B 一度 - 1次db