缓存专题--服务端缓存

作者: 大唐雷恋 | 来源:发表于2019-03-26 20:27 被阅读13次

    疑问:为何要使用服务端缓存?

    1.对热点数据进行缓存,可以加快响应速度

    2.高并发,大流量这种问题怎么解决?加机器抗,用缓存抗,优化算法。。。

    3.降低对数据服务器的压力

    (老鹰抓小鸡),一句话----不惜一切代价将流量拦截,拦截,再拦截。

    服务端缓存的分类:

    数据库缓存

    SQL查询流程:

    命中条件

    缓存存在一个hash表中,通过查询SQL,查询数据库,客户端协议等作为key,在判断命中前,mysql不会解析SQL,而是使用SQL去查询缓存,SQL上的任何字符的不同,如空格,注释,都会导致缓存不命中。如果查询有不确定的数据like now(),current_date(),那么查询完成后结果者不会被缓存,包含不确定的数的是不会放置到缓存中。

    数据库缓存各种相关参数:https://www.cnblogs.com/yesuuu/p/6114600.html

    mysql缓存原理以及碎片问题:https://blog.csdn.net/qzqanzc/article/details/80418125

    mysql一级缓存,二级缓存:https://www.cnblogs.com/maoyizhimi/p/7778504.html

    一个实际的例子:(如何开启mysql缓存可以自行查资料,仅仅是实验为说明原理,知道了这个后就当没有mysql缓存这个事儿。)

    例子:show variables like '%query_cache%';

    可以通过设置query_cache_type->OFF来关闭缓存,但这就将查询缓冲永久地关闭了。在MySQL 5.0中提供了一种可以临时关闭查询缓冲的方法:

    (1) SELECT SQL_NO_CACHE field1, field2 FROM TABLE1

    以上的SQL语句由于使用了SQL_NO_CACHE,因此,不管这条SQL语句是否被执行过,服务器都不会在缓冲区中查找,每次都会执行它。

    我们还可以将my.ini中的query_cache_type设成2,这样只有在使用了SQL_CACHE后,才使用查询缓冲。

    (2) SELECT SQL_CALHE * FROM TABLE1

    数据库缓存失效机制:

    自动失效

    在表的结构或数据发生改变时,查询缓存中的数据不再有效。有这些INSERT、UPDATE、 DELETE、TRUNCATE、ALTERTABLE、DROPTABLE或DROPDATABASE会导致缓存数据失效。所以查询缓存适合有大量相同查询的应用,不适合有大量数据更新的应用。

    手动失效

    1.FLUSH QUERY CACHE;

    2.清理查询缓存内存碎片

    3.RESET  QUERY CACHE;

    4.SQL会从查询缓存中移出所有查询

    总结:

    mysql的查询缓存利用率很低,原因是每当有修改表内容操作时,缓存中所有与该表相关的内容全部要被清空。

    mysql缓存还容易造成碎片问题。参考:https://blog.csdn.net/jiakairong/article/details/78958215

    低版本mysql缓存默认是开启的,但从5.6开始默认禁用query_cache_type参数,禁用缓存。

    建议:

    不要使用mysql缓存,高版本的mysql的query_cache_type也是模式关闭的,咱们目前的云平台库中的mysql缓存也是关闭的。使用mysql数据库缓存,加重了数据库服务器的负担,而且利用率不一定高。尤其是对于经常有变更(数据/结构)的情况下,频繁写入的缓存又不能高效利用。专人专职,大概率情况下不要让厨子去写代码~

    缓存服务器缓存

    关于缓存穿透,缓存雪崩,key重建的话题就不聊了。可以参考简书的这篇帖子,写的不错:

    https://www.jianshu.com/p/b126d466f01a

    一、进程内缓存还是进程外缓存

    服务端缓存的分类:进程内缓存和进程外缓存(redis/memcache)

    进程内缓存的定义

    将一些数据缓存在站点,或者服务的进程内,这就是进程内缓存。

    进程内缓存的实现载体,最简单的,可以是一个带锁的Map。又或者,可以使用第三方库,例如leveldb。

    进程内缓存能存储什么?

    redis/memcache等进程外缓存服务能缓存什么,进程内缓存就能缓存什么。

    进程内缓存可以存储json数据,可以存储html页面,可以存储对象。

    进程内缓存的好处:

    1.与没有缓存相比,进程内缓存的好处是,数据读取不再需要访问数据库。

    2.与进程外缓存相比,进程内 缓存省去了网络开销,所以一来节省了内网带宽,而来响应时延会更低。

    进程内缓存的缺点:

    进程外缓存虽然多了一次网络交互,但仍然是统一存储。站点和服务中的多个节点访问统一的缓存服务,数据统一存储,容易保证数据的一致性。而进程内缓存,保存在多个站点和服务的节点内,数据存了多份,一致性比较难保证。

    保证进程内缓存的数据一致性:

    第一种方案:单节点通知其他节点,写请求发生在server1,在修改完自己内存数据与数据库中的数据后,主动通知其他server节点,也修改内存的数据。

    缺点:同一功能的一个集群的多个节点,相互耦合在一起,特别是节点较多时,网状连接关系会极其复杂。

    第二种方案:通过MQ通知其他节点。写请求发生在server1,在修改完自己内存数据与数据库中的数据之后,给MQ发布数据变化通知,其他server节点订阅MQ消息,也修改内存数据。

    这种方案解决了节点间的耦合,但引入了MQ,使得系统更加复杂。

    前两种方案,节点数量较多,数据冗余份数越多,数据更新的原子性越难保证,一致性也就越难保证。

    第三种方案:干脆放弃“实时一致性”,每个节点启动一个timer,定时从后端拉取最新的数据,更新内存缓存。

    缺点:因为不实时,可能会读到脏数据。

    关于进程内缓存的总结说明:

    不要频繁使用进程内缓存:

    分层架构设计有一条准则:站点层,服务层要做到无数据,无状态,这样才能任意的加节点水平扩展,数据和状态应尽量存储到后端的数据存储服务(数据库或者缓存服务)。

    何时可以使用进程内缓存:

    1.只读数据,可以考虑在进程启动时加载到内存。

    2.一定程度上允许数据不一致的业务。(页面对数据一致性要求较低,可以考虑使用进程内页面缓存)

    终了,一句话总结:还是使用redis和memcache吧

    二、redis还是memcache

    memcache与redis的对比:

    数据结构:vaue是哈希,列表,集合,有序集合这类复杂的数据结构时,会选择redis,因为mc无法满足这些需求。

    持久化:mc无法满足持久化的需求,只能选择redis。

    千万不要把redis当做数据库使用:

    1> redis的定期快照不能保证数据不丢失

    2> redis的AOF(Append Only file 持久化功能)会降低效率,并且不能支持太大的数据量。

    缓存场景下,开启固化功能,有什么利弊?

    优点:redis挂了重启,内存里能够快速恢复热数据,不会瞬时将压力压到数据库上,没有一个cache预热的过程。

    缺点:在 redis挂了的过程中,如果数据库中的数据有修改,可能导致redis重启后,数据库与redis的数据不一致。

    因此,只读场景,或者允许一些不一致的业务场景,可以尝试开启redis的固化功能。

    天然高可用:redis天然支持分布式集群功能,可以实现主动复制,读写分离。而memcache,要想实现高可用,需要进行二次开发。

    思考:(大部分业务场景,缓存真的需要高可用吗)

    1>缓存场景,很多时候,是允许cache miss的

    2>缓存挂了,很多时候后可以通过DB读取数据

    存储内容比较大:memcache的value存储,最大为1M,如果存储的value很大,只能使用redis

    redis中key和value的最大大小限制:https://redis.io/topics/data-types-intro

    redis中key的数量限制:https://redis.io/topics/faq

    (redis官网的一句话,redis不会限制你对缓存量大小的限制,仅仅取决于你机器配置的内存)

    什么时候倾向于memcache?

    纯KV,缓存对象小于1M,key的长度大于250个字符,没有持久化要求,基本不需要分布式集群,高并发(秒杀场景,抗量),单台机器多核利用(redis是单线程的)的情况,使用memcache或许更适合。

    底层实现机制差异:

    内存分配:

    memcache使用预分配内存池的方式管理内存,能够省去内存分配时间。redis则是临时申请空间,可能导致碎片。

    虚拟内存使用:

    memcache把所有的数据存储在物理内存中。redis有自己的VM机制,理论上能够存储比物理内存更多的数据,当数据量超量时,会引发swap,把冷数据刷道磁盘上。

    网络模型:

    memcache使用非阻塞IO复用模型,redis也是使用非阻塞IO复用模型。

    redis还提供了一些非KV存储之外的排序,聚合功能,在执行这些功能时,复杂的CPU计算,会阻塞整个IO调度。

    线程模型:

    memcache使用多线程,主线程监听,worker线程接受请求,执行读写,这个过程中,可能存在锁冲突。redis使用单线程,虽无锁冲突,但难以利用多核的特性提升整体吞吐量。

    三、缓存,淘汰还是修改

    1.KV缓存都缓存了一些什么数据?

    (1)朴素类型的数据,例如:int

    (2)序列化后的对象,例如:User实体,本质是binary

    (3)文本数据,例如:json或者html

    2.淘汰缓存和修改缓存有什么区别?

    (1)淘汰某个key,操作简单,直接将key置为无效,但下一次该key的访问会cache miss

    (2)修改某个key的内容,逻辑相对复杂,但下一次该key的访问仍会cache hit

    可以看到,差异仅仅在于一次cache miss。

    3.缓存中的value数据一般是怎么修改的?

    (1)朴素类型的数据,视情况而定。

    (2)序列化后的对象:一般需要先get数据,反序列化成对象,修改其中的成员,再序列化为binary,再set数据

    (3)json或者html数据:一般也需要先get文本,parse成dom树对象,修改相关元素,序列化为文本,再set数据

    结论:对于对象类型,或者文本类型,修改缓存value的成本较高,一般选择直接淘汰缓存。

    纠结于到底是修改缓存还是淘汰缓存的时候就是需要对比两者成本的时候,修改的成本小就直接修改,修改的成本较高就直接淘汰。

    四、先操作数据库还是先操作缓存?

    https://mp.weixin.qq.com/s?__biz=MjM5ODYxMDA5OQ==&mid=2651961341&idx=1&sn=e27916b8e96bd771c72c055f1f53e5be&chksm=bd2d02218a5a8b37ecffd78d20b65501645ac07c7ba2eb65b7e501a3eb9de023febe63bfdb36&scene=21#wechat_redirect

    这篇沈剑的帖子给出的建议是先操作缓存,然后再操作数据库。但我感觉有问题,多线程操作的时候,可能数据不一致。

    比如:A,B两个线程要做更新,C线程做查询。A先删除缓存(成功),B后删除缓存(成功),B先更新了数据库,这时候C线程读取到了数据库的数据(B)的数据,并更新到了缓存中,然后A后更新了数据库,结果是:数据库中是A的数据,缓存中是B的数据。(缓存有效期,延时双删策略(cache aside pattern))

    为何在写数据库的时候,是删除缓存而不是重新设置缓存?

    答:如果A,B两个线程同时做数据更新,A先更新了数据库,B后更新数据库,则此时数据库里存的是B的数据。而更新缓存的时候,是B先更新了缓存,而A后更新了缓存,则缓存里是A的数据。这样缓存和数据库的数据也不一致。

    一、先更新缓存,再更新数据库

    (这里对缓存的操作都是删除,而不是设置)

    更新缓存(失败),不再继续----->合理

    更新缓存(成功),更新数据库成功---->合理

    更新缓存(成功),更新数据库失败----->合理,仅仅是会在后续过程多一次对数据库的查询

    但会存在我上边提到的疑问。

    二、先更新数据库,再更新缓存

    更新数据库(失败),不再继续---->合理

    更新数据库(成功),更新缓存(成功)------->合理

    更新数据库(成功),再更新缓存(失败)-------->不合理,这时读取到的是旧数据

    这种方式,可以通过集中方式来弥补:

    1.给缓存设置有效期,这样错误的缓存迟早会变成正确的。而且过期时间越短,缓存的正确率越高。但是,这样也同时会增加数据服务器的负担。

    2.当第二步中的更新(删除)缓存失败的时候,进行重试。

    3.定期全量更新(定期把缓存全部失效,然后全量加载),这种方式值得商榷,有可能会有大量的请求在这时打到数据库。

    还有另外一种情况:

    A,B两个线程读取缓存,C线程进行写数据库。

    A读缓存,没有数据,进入读库流程,从数据库读取数据,读取成功,还没有写缓存。

    C进来写数据库,写成功。

    B读缓存,没有数据,进入读库流程,从数据库读取数据。读取成功(B读取的是C写入的最新数据),还没有写缓存。

    B写缓存成功,然后A写缓存成功,结果---->数据库是最新数据,缓存中是旧的数据。

    这个时候需要使用redis分布式锁(https://www.imooc.com/article/34098)。也可以使用有效期策略。(看具体业务要求)

    五、缓存与数据库不一致,怎么办?

    (1+2)先一个写请求,淘汰缓存,写数据库

    (3+4+5)接着立刻一个读请求,读缓存,cache miss,读从库,写缓存放入数据,以便后续的读能够cache hit(主从同步没有完成,缓存中放入了旧数据)

    (6)最后,主从同步完成

    导致的结果是:旧数据放入缓存,即使主从同步完成,后续仍然会从缓存一直读取到旧数据。

    可以看到,加入缓存后,导致的不一致影响时间会很长,并且最终也不会达到一致。

    (6)主从同步

    (7)通过工具订阅从库的binlog,这里能够最准确的知道,从库数据同步完成的时间

    画外音:本图画的订阅工具是DTS,可以是cannal,也可以自己订阅和分析binlog

    (8)从库执行完写操作,向缓存再次发起删除,淘汰这段时间内可能写入缓存的旧数据

    如此这般,至少能够保证,引入缓存之后,主从不一致,不会比没有引入缓存更坏。

    画外音:即使引入缓存,也只有一个很小的时间间隔,可能读到旧数据。

    也可以使用延时双删策略

    六、主从数据库不一致,怎么办?

    常见的数据库集群架构:

    一主多从,主从同步,读写分离

    为何会主从不一致,因为同步会有时间差:

    如何避免这种时间差导致的不一致:

    方案一:忽略

    任何脱离业务的架构设计都是耍流氓,绝大部分业务,例如:百度搜索,淘宝订单,QQ消息,58帖子都允许短时间不一致。

    如果业务能够接受,别把系统架构搞得太复杂。

    方案二:强制读主

    (1)使用一个高可用主库提供数据库服务

    (2)读和写都落到主库上

    (3)采用缓存来提升系统读性能

    方案三:选择性读

    (1)写主库

    (2)将哪个库,哪个表,哪个主键三个信息拼装一个key设置到cache里,这条记录的超时时间,设置为“主从同步时延”

    key的格式为“db:table:PK”,假设主从延时为1s,这个key的cache超时时间也为1s。

    这是要读哪个库,哪个表,哪个主键的数据呢,也将这三个信息拼装一个key,到cache里去查询,如果,

    (1)cache里有这个key,说明1s内刚发生过写请求,数据库主从同步可能还没有完成,此时就应该去主库查询

    (2)cache里没有这个key,说明最近没有发生过写请求,此时就可以去从库查询

    以此,保证读到的一定不是不一致的脏数据。

    总结

    数据库主库和从库不一致,常见有这么几种优化方案:

    (1)业务可以接受,系统不优化

    (2)强制读主,高可用主库,用缓存提高读性能

    (3)在cache里记录哪些记录发生过写请求,来路由读主还是读从

    参考资料:

    何时使用mysql缓存以及提高缓存命中率的建议:https://blog.csdn.net/qq_25622107/article/details/80223470

    现代WEB应用程序的服务器端缓存策略  https://alankent.me/2018/08/25/server-side-caching-strategies-for-modern-web-applications/

    58沈剑 缓存  https://mp.weixin.qq.com/s/V1hGa6D9aGrP6PiCWEmc0w

    Cache Aside Pattern https://mp.weixin.qq.com/s/-fk-cEIo3iDCUSwT_l8d2w

    相关文章

      网友评论

        本文标题:缓存专题--服务端缓存

        本文链接:https://www.haomeiwen.com/subject/ifznvqtx.html