当前用户信息该不该缓存?

2014-04-24 16:15:47 +08:00
 gaicitadie
就是当前登录的个人信息,比如在v2ex,你的用户名,头像地址,账户余额(铜币银币之类的个数),最常用的做法,把用户的userid加密后保存在cookies中,每次用户访问网页的时候解密userid,然后

select * from Users where userid=xxxx

从用户表中获取用户的个人信息,用户名、头像、账户余额。。。等等。

这样每次打开一个网页,都要查询一次用户信息,于是为了减少数据库查询,把select出来的用户信息保存在session中,以后每次用户信息从session里面取(session用memcache实现,也就是每次到memcach里面取)。


我现在的疑问是,真的需要缓存用户信息吗?select * from Users where userid=xxxx 语句本身已经很高效,userid是索引字段,现在服务器内存这么大,数据库几乎都可以完全跑在内存中了。我不喜欢用缓存,是因为一旦用了缓存,原本清晰的思路变得复杂,并且控制不好命中率的话,反而会加重系统负担。
9852 次点击
所在节点    程序员
33 条回复
cloudzhou
2014-04-24 18:31:22 +08:00
这个地方要看你对缓存怎么定义,对于使用数据库 select * from xx where id = yy 的情况,其实走的大部分也是数据库的缓存,要理解缓存的重要意义,在于访问时间数量级别的差异:比如寄存器,内存,硬盘,网络访问时间...你可以看到访问时间是数量级别的差距,这就是推崇缓存的意义。

缓存遇到的问题是:
1 粒度
粒度太小,增加了维护上的难度,粒度太大,更新频繁,内存占用大
2 副本
缓存是数据库的一个副本,只要存在副本,就存在不一致性的问题,举个例子,有人手工用 sql 语句更新某个值,而缓存是没办法通知到的。副本状态维护在计算机的各种设计上都是很大的问题,从设计上要减少副本。
3 更新机制
和 2 是相关的,没有一种很好的更新机制能非常好的把更新推送到 cache,代码会比较 dirty。

但是依然推崇缓存:
1 例子:mysql 的协议就比 memcached 复杂很多,相对来说在协议上就体现出延时。
2 大幅的减少 db 的压力,以我经验看,大部分程序员很容易把压力扔向 db,然后由数据库管理员负责,这是不对的。

缓存使用原则:
1 不要手工去维护缓存,特别是业务代码里,这是灾难。
2 orm 里面去维护缓存,做各种更新触发操作,对数据的操作只能通过统一 orm。
senghoo
2014-04-24 18:34:24 +08:00
过早的优化永远是灾难。等到实际上线的时候发现瓶颈再加上缓存也不迟。
zeayes
2014-04-24 20:40:14 +08:00
同意@senghoo
wdlth
2014-04-24 20:53:11 +08:00
很多数据库本身就有缓存,再加一个略显多余。当然有的地方严格要求一致性Hash什么的,可能数据库本身不好解决,要借助其他的东西。
pubby
2014-04-24 22:52:07 +08:00
数据库缓存还依赖更新频率,期望一个频繁更新的表有好的缓存命中率不太现实。
这时候更多取决文件系统的缓存

缓存性能肯定是要比直接从db取要快,尽管不是数量级别上的差异
但是优势是累积的,想象一下一页回帖,50个不同用户需要获取各自的信息,可能还不在同一个表
这时有缓存的话优势就明显多了。
所以单单跟 where pk=? 的比较没意义

缓存不一定是用在db数据上,很多时候还可以避免重复计算,脚本语言有时确实就会有这样的问题,cpu压力可以明显降低。


楼主经常提到某网站访问量不小,没有缓存运行得也很好,这不能说明什么。日PV百万的网站只要能达到每秒20多个请求就行。这种级别只要不是逻辑太复杂啥db都能抗住。
huangzxx
2014-04-24 23:06:43 +08:00
可以把 Users 表再分一下,实时性要求不要的查询才用外部缓存。
likuku
2014-04-25 10:10:04 +08:00
放 memcached 不行么?
likuku
2014-04-25 10:15:17 +08:00
用户少,直接查DB还凑合,用户多,再快的简单查询db也扛不住频繁密集查寻...高压的环境,直接查db都是最迫不得已时才去用的。
soundbbg
2014-04-25 10:46:53 +08:00
需要缓存。这是结论。

如果你的网站用户量小的可怜,倒是不必,但加了还是比不加好。

这里要理解缓存和数据库的作用是同等重要的,也是需要强调的。

首先需要理解缓存,缓存你可以简单的理解成一个哈希表,通过key就可以取得值。而这个key-value对在内存中,内存的访问极快。如果数据库有索引,那也是相当的快,但索引需要生成文件,如果对大量的key进行索引会导致索引文件数据膨胀,甚至有可能会超过数据文件本身。查询虽然性能消耗的小,但大量的查询性能还是会消耗,相比之下,能节约数据库性能就节约,因为我们程序员是干嘛的?就是为了不必重复劳动。

既然lz对缓存作用有怀疑,觉得sql的查询性能已经很高了,那么我们可以仔细分析,有这么一种情况:

1个用户被100个用户关注。

OK,这样我们如果不使用缓存,加载一个用户的粉丝页面。

1. 首先通过数据库查询到用户,例如from user where id=xxx。
2. 然后通过数据库查到粉丝列表,例如from fans where follow_id=xxx。
3. 显示用户的页面,这里有101个用户。循环查询了from user where id=xxx 101次。

这个时候你会发现,即便是同一个用户,那么访问一次页面需要101个查询,虽然粉丝已经很少了。即便性能很快,还是需要消耗sql资源。如果一个页面每天被访问1000万次,你就自己能够得到结论。

如果使用缓存,会有下面的情况:

1. 首先通过数据库查询到用户,from user where id=xxx,放入缓存。
2. 通过数据库查询到粉丝列表,from fans where follow_id=xxx列表可以放入缓存。
3. 显示用户的页面,假设没有任何缓存的情况下,依旧循环查询了from user where id=xxx 100次。

这个时候再访问同一个页面的时候,假设访问1000万次。

1. 首先通过缓存看用户是否在缓存里,存在,命中返回。
2. 看用户的粉丝列表是否在缓存里,存在,命中返回。
3. 显示用户的页面,100个粉丝的用户都在缓存里,存在,命中返回。

无需数据库查询。

至于当用户的粉丝变动了,如何更新缓存呢,一个简单的方法就是删除缓存。假设用户的粉丝取消关注了这个用户。

1. 删除用户的粉丝列表的缓存。

那么刷新页面的时候就会下面的情况:

1. 首先通过缓存看用户是否在缓存里,存在,命中返回。
2. 看用户的粉丝列表是否在缓存里,不存在,查询数据库from fans where follow_id=xxx,并放入缓存。
3. 显示用户页面,99个粉丝都在缓存里,存在,命中返回。

即便访问依旧是1000万次,只有取消关注的那第一次返回查询了一个数据库,并且是lz说的高性能查询。那么数据库服务器依旧没有什么压力。

至于缓存性能?相当的快,在内存中,可以忽略不计(当然这里也是根据量级来判断,但即便千万用户也可以忽略不计)。

上面的情况只是一个业务里相当小的情况,有比如你当前打开的v2ex这个页面,加载了多少用户、话题和评论?你可以计算一下缓存的优势。

----

当然,继续深入的话,我们推荐缓存什么最好?(正确的缓存不会造成不一致)首先提出不好的:

1. 不要针对页面缓存,这才会造成什么不一致的情况。
2. 不要针对大块数据进行缓存,下面再说。

(其实有时候是可以针对一些业务页面进行缓存的,例如上个月到几年前的每个月的财务结算报表)

要缓存的,最好是最原始的数据。根据上面的情况,例如:

1. 一个用户的数据库数据的缓存。可以直接映射成一个用户的实例。
2. 一个用户的粉丝列表的id缓存,必须强调,这里不是粉丝列表的实例的缓存。

假设通过mc.get(user_key),那么得到的最好是用户数据库里的原数据。如果mc.get(user_fans_key),那么得到的就是用户粉丝的**id**缓存。这里详细说明一下,不要针对大块数据进行缓存的原因。

假设你缓存了大块数据,即拿到用户粉丝的列表不是缓存id,而是缓存对象,那么如果一个用户更新了自己的信息,这个时候并不会删除掉用户的关注列表缓存,对吧,因为粉丝并没有取消关注用户,那么关注列表拿到的用户数就错误了。

另外一个问题就是如果缓存id,即便有100万个粉丝,内存占用也不大,如果100万个实例呢。。。

所以,只针对粉丝的id进行缓存是很重要的,举例说明:

1. 用户有100个粉丝,并且缓存了。
2. 一个粉丝改了自己的名字,这个时候把粉丝的用户缓存失效。
3. 用户查询自己的粉丝时,发现99个用户缓存命中,1个用户缓存失效,查询并放入缓存。

因为在2里粉丝并没有取消关注用户,不会刷新粉丝缓存,如果这个时候缓存的是用户对象,那就出现了不一致。

----

我们可以再发散,为什么现在redis比memcached更热门,因为redis可以直接在内存里排序。假设我有一个需求是最新关注的粉丝在最上面。那么redis可以直接在缓存列表的最前面插入最新粉丝的id,但memcached不行,按上面的方法就是先删除,然后数据库里查询保持顺序,在插入到缓存中。

redis这样的情况可以让类似twitter、新浪微博这样的业务顺利进行,memcached就用起来别扭了。

----

再回到这个页面,我们看看v2ex当前页面可以怎么缓存。

1. 用户信息。
2. lz发的内容的原数据,文字可以放入key-value持久化数据库。
3. lz发的内容的评论id列表。
4. 每个评论的原数据缓存,内容可以放入key-value持久化数据库。
5. 如果有人添加评论,可以过期lz发的内容的评论id列表。
6. 如果有人更新评论,如果是内容,不需要更新缓存,直接更新持久化数据库。
7. 如果有人更新评论如时间之类的,或者删除评论,过期lz内容评论的id列表。

可以看到数据库查询大大的减少,v2ex这个用户量级和访问,至少毫秒内就能展示。

----

我不太同意『缓存』这是一个过度优化的设计,『缓存』这个概念无论是计算机硬件、软件还是web设计都是很重要的。不仅能提升app的使用体验(载入更快了),还减少了sql服务器的压力。

大部分互联网公司都及其依赖缓存,如果缓存服务器挂了,网站必然挂。但之前我也说了,缓存和数据库都是非常重要的,因为在内存中的数据断电就会消失,还是需要持久化的。另外要知道,内存的速度是硬盘访问的数百倍甚至千倍。

正如 @pubby 说的,网站运行了几年,那积累的缓存要是没了,查询流量瞬间就会击垮服务器。

几年前大家做开发的时候,喜欢把数据写入数据库,然后放入memcached,现在更多的开发更喜欢先放入redis,然后写入数据库持久化,当成是log和恢复时使用。当然我觉得可以根据业务来选择不同的方案,不一定redis是最好的(因为它太消耗内存)。

本来想解答lz的没想到说了这么多,只是觉得能反应架构和思维上的区别,当然我是一个垃圾,如果说的有问题还希望大家轻拍。
gaicitadie
2014-04-25 11:15:31 +08:00
@soundbbg 非常感谢你的回复,收藏了
lyragosa
2014-04-25 12:37:07 +08:00
@soundbbg 很有道理,我现在的产品就是这样处理缓存的。
不过我的用户量小(每天也就几百IP),所以就是简单暴力的“出现了缓存被修改就直接删掉缓存”
没有做进一步的排序啊什么的处理。
wdlth
2014-04-26 00:20:56 +08:00
以前mixi.jp就是因为Memcached的一个Bug造成大规模宕机,技术都是双刃剑,享受快感也要承担风险。
PoetAndPoem
2019-02-20 16:05:23 +08:00
@cloudzhou orm 上的更新操作,只能通过 orm 这句话意思可以理解为:对 orm 上的更新操作,不能用原生 sql 进行查询?

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/109856

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX