目录
问题:设计用户系统
1.明确设计需求
首先列举一些需要的功能,找出核心功能:
- 登录/注册
- 用户信息修改/查询
- 好友关系
- …
用户系统一般属于读多写少的系统,不会有很多人每天进行登录/注册/修改个人信息的操作,适合使用缓存进行优化。
接下来做一些简单的计算,假设系统每天有1百万活跃用户,平均每个用户每天产生0.1个登录/注册/修改个人信息。
QPS = 1M * 0.1 / 86400 ~ 1, 峰值QPS * 3 ~ 3;
对于查询操作,假设每个用户每天查询100次
QPS = 1M * 100 / 86400 ~ 1.1k, 峰值QPS * 3 ~ 3.3K;
2.设计服务
- Authentication Service: 登录/注册
- User Service:用户信息保存和查询
- Friendship Service:好友关系存储
3.存储结构设计
关于缓存
- 缓存一般保存后面要使用的东西,下次使用时直接拿,不需要重复计算和数据库操作。
- 缓存不一定特指服务器端缓存,浏览器,客户端都可能会有缓存存在。
- 缓存类似哈希表,但是是系统级别的,不像算法,逻辑正确就可以正常执行。系统在运行过程中可能有各种意外情况,如服务器断电等,都会造成意想不到的结果。
数据一致性问题
对于UserService,查询的逻辑比较清晰
class UserService
{
public User GetUser(userId)
{
user = cache.get(key)
if (user != null)
{
return user;
}
user = db.get(userId)
cache.set(key, user)
return user
}
}
由于引入了缓存,那么在数据更新时,不仅要更新数据库,而且要更新缓存,这两个更新操作存在前后的问题:
db.set(user); cache.set(key, user);
db.set(user); cache.delete(key);
cache.set(key, user); db.set(user);
以上三种写法,如果第一步操作执行成功,第二步操作执行失败,都会导致缓存与数据库中数据不一致,造成脏数据。
cache.delete(key); db.set(user);
上面这种方式虽然看起来没有问题,第一步执行成功后,第二步即使操作失败也不会造成数据不一致。但是在多线程多进程的情况下,仍然会存在问题。
比如进程a成功执行第一步操作时,进程b调用GetUser,此时发现缓存中没有对应的key,进程b就会从db中获取数据,并更新到缓存中。这时进程a继续更新数据库,缓存中保存的就变成了旧数据。
比较好的做法应该是下面这种写法
*db.set(key, user); cache.delete(key);
上面的做法在多线程多进程的情况下,也会存在问题。
当进程a在调用GetUser方法时,执行完db.get(userId), 进程b执行更新用户信息的操作,然后再由进程a执行cache.set(key, user),这样缓存中保存的也是旧数据。
看起来两种方式都有问题,但是, 对于用户系统来说,是一个读多写少的系统,这种情况的发生概率是远低于前面那种情况的。而且缓存的写入速度是远超于数据库的写入,很难出现数据库先操作完,缓存还没有更新好的情况。
还有一种搭配解决方案—使用过期机制
我们允许数据库和缓存中存在差异,缓存中的key不要设置成永久有效,比如有效期7天,那么这是即使出现不一致的情况,最多也就存在7天,最终还是会保持一致。
这种缓存的策略就是Cache Aside, 旁路缓存策略。