V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
gaicitadie
V2EX  ›  程序员

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

  •  
  •   gaicitadie · 2014-04-24 16:15:47 +08:00 · 9790 次点击
    这是一个创建于 3626 天前的主题,其中的信息可能已经有所发展或是发生改变。
    就是当前登录的个人信息,比如在v2ex,你的用户名,头像地址,账户余额(铜币银币之类的个数),最常用的做法,把用户的userid加密后保存在cookies中,每次用户访问网页的时候解密userid,然后

    select * from Users where userid=xxxx

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

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


    我现在的疑问是,真的需要缓存用户信息吗?select * from Users where userid=xxxx 语句本身已经很高效,userid是索引字段,现在服务器内存这么大,数据库几乎都可以完全跑在内存中了。我不喜欢用缓存,是因为一旦用了缓存,原本清晰的思路变得复杂,并且控制不好命中率的话,反而会加重系统负担。
    33 条回复    2019-02-20 16:05:23 +08:00
    nigelvon
        1
    nigelvon  
       2014-04-24 16:33:24 +08:00
    单从你说的这个需求来说会有思路变复杂,和命中率的问题么?
    vietor
        2
    vietor  
       2014-04-24 16:35:17 +08:00
    得看并发是多少
    gaicitadie
        3
    gaicitadie  
    OP
       2014-04-24 16:42:53 +08:00
    @vietor discuz搭建的论坛,有的访问量挺大,貌似discuz没有缓存用户信息,直接每个页面都 select * from Users where userid=xxxx
    heiher
        4
    heiher  
       2014-04-24 16:43:00 +08:00
    关键是瓶颈在哪里
    anheiyouxia
        5
    anheiyouxia  
       2014-04-24 16:45:44 +08:00 via Android
    我觉得应该看情况,并发大的时候应该缓存最经常用的数据。如果本来流浪不大,这类基本信息可以缓存的,当然也可以不缓存。
    说一个以前公司遇到的:
    以前我工作的一家公司做的夜游,也是有类似情况,一开始设计的时候认为效率第一,就在用户登录的时候将用户大多数的常用信息以及为数不少的关联信息都加载到内存了。一开始没有什么问题,直到后来合服,一开始合服几次没问题,最后大服合并后,内存不足了。
    gaicitadie
        6
    gaicitadie  
    OP
       2014-04-24 16:48:27 +08:00
    我个人的习惯是缓存能不用则不用,只有比较复杂的查询计算的出来的结果,并且实时性要求不高的才使用缓存,比如v2ex右侧的“今日热议主题”、“最热节点”这些东西,计算出来这些东西需要的时间多一些,并且实时性要求不高,仅仅为了展示,不会利用缓存里面的东西进行业务逻辑计算。

    能直接select ... where id=xxx 的绝对不缓存
    gaicitadie
        7
    gaicitadie  
    OP
       2014-04-24 16:52:15 +08:00
    感觉对缓存的迷信,就像前几年迷信windows系统下的各种优化大师,一个操作系统同时开N个优化大师,结果本来很快的系统变得巨卡。
    alex321
        8
    alex321  
       2014-04-24 16:59:21 +08:00
    仅缓存必须缓存的东西。比如频繁调用的,而且更新频率相对低的。
    用户名这些信息第一次查询了,无非就是头像、个性签名、昵称、性别、积分、各种统计的总数什么的,直接丢到 session 去,怕 session 过大的话 base64 下,注销就销毁。
    anheiyouxia
        9
    anheiyouxia  
       2014-04-24 17:01:49 +08:00 via Android
    @anheiyouxia 靠,没打完就被发出来了。
    然后我们一开始排查了日志并没有发现问题出现在哪里,只看到内存一直飙升,直到崩溃。最后忘记怎么发现这个问题了,当时都惊讶了,怎么把所有数据都加进去了。然后修改程序,把绝大多数的缓存都去掉了。后来发现去掉太多,数据量读取很频繁,玩家等待数据加载的过程多了几秒。几经修改,才将数据优化到比较稳定的状态。
    gaicitadie
        10
    gaicitadie  
    OP
       2014-04-24 17:06:04 +08:00
    @anheiyouxia 发现瓶颈以后再根据实际情况加缓存比较靠谱
    gaicitadie
        11
    gaicitadie  
    OP
       2014-04-24 17:08:20 +08:00
    @alex321 取session也需要耗费资源,如果是php默认的文件存储,还不一定有查询数据库效率高。如果session放在memcache,能比数据库的 select ... where id=xxx 高多少呢?一直认为 mysql 根据 id 查询单条记录的效率很高很高
    xiawinter
        12
    xiawinter  
       2014-04-24 17:10:23 +08:00
    @gaicitadie 这个太夸张了。 对 v2ex 来说, thread 也是 select * from thread where id = 109856, 难道这个 thread 不缓存么?

    我们缓存的对象是共用的,变化少的, 不容易计算出来的。

    至于楼主说的情况,也需要看情况处理, 如果这一串信息很多, 而且可能在不同的表里,那么是需要缓存的, 如果他必须一次性取出来的话。

    显然 账户余额和头像信息是不应当放在一个表里的, (如果是一张表的话, 那么我觉得这个设计本身有问题, 数据量大,存在更新平静, 不断刷 user 表), 那么为了获取用户信息,可能要查询很多表, 这就有效率问题。
    waitgroup
        13
    waitgroup  
       2014-04-24 17:14:14 +08:00
    @alex321 base64比单纯的序列化占用大, 而且你这个涉及到当前用户A的操作改变了当前在线的B的属性, 你还得去找到B的session更新, 只会比用cache更复杂.


    @gaicitadie pk查询确实高效, 但是频繁查询要保证高效也得依赖querycache, 而mysql的querycache很废, 同样的一个查询涉及到读取columns不同甚至大小写不同都会导致cache命中失败, 这样还不如放弃querycache直接用第三方cache.
    Mutoo
        14
    Mutoo  
       2014-04-24 17:16:55 +08:00
    "最常用的做法,把用户的userid加密后保存在cookies中,每次用户访问网页的时候解密userid"

    貌似不对哦。常用做法是给用户一个令牌(token)并绑定该用户,然后下次用户回来的时候,用这个令牌去查询用户。一段时间后令牌过期。
    waitgroup
        15
    waitgroup  
       2014-04-24 17:18:51 +08:00
    @Mutoo 你说的是传统session的实现, 他说的是拿secure cookie当session介质, 不冲突.
    gaicitadie
        16
    gaicitadie  
    OP
       2014-04-24 17:23:44 +08:00
    @xiawinter v2ex的每个帖子都是缓存的吗,这个不清楚,不过有用discuz搭建的论坛比v2ex的访问量更大却没开缓存的。

    discuz每次查询的账户信息,摘取的其中一个代码片段:m.uid AS discuz_uid, m.username AS discuz_user, m.password AS discuz_pw, m.secques AS discuz_secques,
    m.adminid, m.groupid, m.groupexpiry, m.extgroupids, m.email, m.timeoffset, m.tpp, m.ppp, m.posts, m.digestposts,
    m.oltime, m.pageviews, m.credits, m.extcredits1, m.extcredits2, m.extcredits3, m.extcredits4, m.extcredits5,
    m.extcredits6, m.extcredits7, m.extcredits8, m.timeformat, m.dateformat, m.pmsound, m.sigstatus, m.invisible,
    m.lastvisit, m.lastactivity, m.lastpost, m.newpm, m.accessmasks, m.editormode, m.customshow, m.customaddfeed
    威望、积分、出现ucenter以前的头像。。。很多信息都是放在一个用户表的。

    账户余额如果只在少数的页面显示,就没必要跟头像放在一个表了,但如果每个页面都显示呢,就没必要分两个表了
    alex321
        17
    alex321  
       2014-04-24 17:31:02 +08:00
    @waitgroup
    @gaicitadie
    session 相对零碎,放在内存中的呢,用户按照某个规则分为数个 block,便于索引和操作。
    用户注销时,将用户资料 update 进库,并销毁 session。
    base64 的是个性签名和头像地址,用于对付各种莫名其妙的非主流签名和需要分服务器分区块存储的零碎用户头像地址信息。
    针对性别、数量什么的序列化到一个 info 中,取出时在前端解析。
    akira
        18
    akira  
       2014-04-24 17:40:39 +08:00
    不要为了做缓存而做缓存
    gaicitadie
        19
    gaicitadie  
    OP
       2014-04-24 17:42:28 +08:00
    @akira 握手,感觉项目一开始就强调缓存,有“医之好治不病以为功”的味道
    cocalrush
        20
    cocalrush  
       2014-04-24 17:47:49 +08:00
    这个问题正好是我遇到的问题,现在我负责我们系统的单点登录。单点登录和主系统是分开的,用户登录时单点登录验证成功后,在本地写下cookie信息。主系统拿到cookie中的token 再从单点登录的webservce去获取用户信息放在session里面.....感觉这样效率不高,而且好像还不是很安全呀。
    cloudzhou
        21
    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
        22
    senghoo  
       2014-04-24 18:34:24 +08:00   ❤️ 1
    过早的优化永远是灾难。等到实际上线的时候发现瓶颈再加上缓存也不迟。
    zeayes
        23
    zeayes  
       2014-04-24 20:40:14 +08:00
    同意@senghoo
    wdlth
        24
    wdlth  
       2014-04-24 20:53:11 +08:00
    很多数据库本身就有缓存,再加一个略显多余。当然有的地方严格要求一致性Hash什么的,可能数据库本身不好解决,要借助其他的东西。
    pubby
        25
    pubby  
       2014-04-24 22:52:07 +08:00
    数据库缓存还依赖更新频率,期望一个频繁更新的表有好的缓存命中率不太现实。
    这时候更多取决文件系统的缓存

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

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


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

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

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

    首先需要理解缓存,缓存你可以简单的理解成一个哈希表,通过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
        30
    gaicitadie  
    OP
       2014-04-25 11:15:31 +08:00
    @soundbbg 非常感谢你的回复,收藏了
    lyragosa
        31
    lyragosa  
       2014-04-25 12:37:07 +08:00
    @soundbbg 很有道理,我现在的产品就是这样处理缓存的。
    不过我的用户量小(每天也就几百IP),所以就是简单暴力的“出现了缓存被修改就直接删掉缓存”
    没有做进一步的排序啊什么的处理。
    wdlth
        32
    wdlth  
       2014-04-26 00:20:56 +08:00
    以前mixi.jp就是因为Memcached的一个Bug造成大规模宕机,技术都是双刃剑,享受快感也要承担风险。
    PoetAndPoem
        33
    PoetAndPoem  
       2019-02-20 16:05:23 +08:00
    @cloudzhou orm 上的更新操作,只能通过 orm 这句话意思可以理解为:对 orm 上的更新操作,不能用原生 sql 进行查询?
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   1829 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 16:28 · PVG 00:28 · LAX 09:28 · JFK 12:28
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.