谈缓存和Redis

    自从上次分享《Redis到底该如何利用?》已经有1年多了,这1年经历了不少。从码了我们网站的第一行开始到现在,我们的缓存模块也不断在升级,这之中确实略有心得,最近也有朋友探讨缓存,觉得可以总结并分享一下拙见,期待能有更深入的研究。

缓存是什么?

    我时常在群里或者在社区里看到有人对缓存有诸多疑问,搞不清缓存的用途,分不清.NET Redis各驱动、中间件的区别和选择。缓存其实并不是什么看起来很深奥或者很难驾驭的东西,它一般是用来保存一些常用的数据到内存,以加快数据读取,减少直接访问DB流量以降低DB压力。

    比较常用的场景比如:

        静态的维表类数据,比如地址库,单位之类。
        用户Session
        一些实时性高,访问频率高的计算数据,比如用户访问次数,文章阅读量,用户黑名单之类。

    传统的架构里,缓存纯粹是DB数据的一份Copy,就像上面所说是为了程序能更快的读取数据的。既然是Copy,其实就不必关心丢失,甚至微小的误差。一定是最先保证DB,然后才是考虑缓存。另外现在分布式大行其道,集群比比皆是,缓存的应用就分成了多级,从单机内存到集中式缓存到最后穿透到DB。

    但是现在很多大型互联网架构里缓存是有不一样的应用的,比如新浪微博,他们使用Redis并不是简单的缓存,而是直接作为第一层的Storage,然后再异步写回DB。可以参考《新浪微博关系服务与Redis的故事》。

    最近遇到一次很有意思的讨论,说到用户黑名单功能的设计。有朋友DB依赖性超强,上来就是用户表里加字段呀?读取太慢?加索引啊之类之类。我觉得这个挺有意思的,以前我也是想当然的这样想。为什么?一开始做项目都是设计数据库开始,建模就是ER图,上来就是DB 三范式。以至于其实现在我都很难改变这样的思维。导致学习OO,DDD之类建模时,思想始终绕不过DB First的思维。如果绕开DB,思考缓存去设计这样的功能,可行性和性能都能提高不少。

    (缓存穿透:一般的缓存系统,都是按照key去缓存查询,如果不存在对应的value,就应该去后端系统查找(比如DB)。如果key对应的value是一定不存在的(数据库里面没有此值,也无法更新缓存,但DB也要被执行),并且对该key并发请求量很大,就会对后端系统造成很大的压力。这就叫做缓存穿透。

       解决方案:对查询结果为空的情况也进行缓存,缓存时间设置短一点,或者该key对应的数据insert了之后清理缓存。

.NET下的缓存应用

    针对单机应用,内存缓存(System.Runtime.Caching)就足够,集群环境应该上集中式缓存,比较常用的是memcached和Redis,这两者的区别倒是可以好好说道说道。

    memcached更加的像内存缓存,功能单一,只能做普通的缓存操作(Put/Get/Remove...)

    Redis功能更加丰富一些,也支持更多的数据结构,更多的计算命令,因此例如Session等缓存模块更加的适合memcached,而带实时计算性质的更加适合Redis。不过同时用上两种服务,也只有大公司能干了,一般人像我,还是比较喜欢Redis,毕竟功能丰富。

    关于Redis的驱动,我也经常看到SeviceStack.Redis/StackExchange.Redis搞得大家不知道取舍。

    两个我都用,因为ServiceStack本来是开源免费后来为了支撑发展吧,人家顺便就在V4之后开始加入限制,开始收钱了。不过V3依然免费,使用的时候需要注意所有的依赖都要用V3以下哦。V3版本很遗憾,很多功能并不能很好的支持,比如Pub/Sub.

    StackExchange.Redis源自鼎鼎大名的StackOverFlow,他们有网站的收入,自然热衷开源免费。不过质量还是非常靠谱的,新功能支持的很好。

    以上在GitHub上一搜便有。

    另外一个开源项目CacheManager.NET最近也是很火,可参考GitHub相关开源代码很多人搞不懂它是什么样的定位,它实际上是一个中间件,本身并不直接提供与缓存(Redis\mem)的对接API,当前的版本它是使用了StackExchange.Redis来作为驱动的,博客园里已经有了很详细的介绍,如《.Net缓存管理框架CacheManager》。它致力于屏蔽各种缓存服务的复杂度,提供简单一致的API,让开发者能够用一套代码,只要稍加配置就能使用MemroyCache/集中式缓存(redis/mem)。最强大的是它提供了多层缓存的方案(基于Redis Pub/Sub),只要简单的配置就达到了多层之间的缓存同步。(内部的原理是,通过Redis Pub/Sub,每当缓存变动就通知sub们自动remove掉响应的缓存)。我们公司最近的一次更新也切换到了CacheManager.NET,不得不说它真的很好用。

    “集中式缓存"与"分布式缓存"的区别其实就在于“集中”与"非集中"的概念,其对象可能是服务器、内存条、硬盘等。

        比如:----1.服务器版本:

                ----.----缓存集中在一台服务器上,为集中式缓存。

                ----.----缓存分散在不同的服务器上,为分布式缓存。

                ----2.内存条版本:

                ----.----缓存集中在一台服务器的一条内存条上,为集中式缓存。

                ----.----缓存分散在一台服务器的不同内存条上,为分布式缓存。

                ----3.硬盘版本:

                ----.----缓存集中在一台服务器的一个硬盘上,为集中式缓存。

                ----.----缓存分散在一台服务器的不同硬盘上,为分布式缓存。


合理设计缓存

1. 合理设计Key

    缓存最重要的特点的是其Key-Value形式,即使Redis的多样数据结构也是。Key-Value是保证其快速的根本原因,所以合理的Key,会让搜索更方便。

    这也会让一份数据根据场景被设计成多份不同的Key-Value,例如:我之前的文章中提到的模糊匹配功能,就会把name设计进key,而如果是简单的根据userid取用户信息,则会把userid设计进key。从这里也可以看出缓存并不介意保存很多一样的数据。

2. 合理的使用缓存失效时间

    上面提到缓存是可以丢失的,的确如果是内存缓存,它会随着应用的进程的终止而释放。除了这样的释放,缓存还可以被设置过期时间。为什么要如此设计呢?试想机器内存一定不会比硬盘大呀,空间有效,珍贵的资源自然是要保存尽可能常用的数据(热数据)。

    所以合理的设计失效时间会保持数据始终是最活跃的那一部分。当然失效时间也会引起,缓存雪崩等一系列问题,这里有一篇深入的文章值得去看看《Web开发基本准则-55实录-缓存策略

    缓存雪崩:当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如DB)带来很大压力。

       解决方案:

       1:在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
       2:不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
       3:做二级缓存,A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期(此点为补充)
 

posted on 2018-01-01 00:35  SuperSnowYao  阅读(1530)  评论(0编辑  收藏  举报

导航