|
| 1 | +## cache经验总结 |
| 2 | + |
| 3 | +--- |
| 4 | + |
| 5 | +### 为什么要使用cache |
| 6 | + |
| 7 | +关系型数据库的数据量比较小,以mysql为例,单表的量尽量控制在千万级别。 |
| 8 | + |
| 9 | +关系型数据库在TPS上的瓶颈往往会比其他瓶颈更容易暴露出来,尤其对于大型web系统,由于每天大量的并发访问,对数据库的读写性能要求非常高;而传统的关系型数据库的处理能力确实捉襟见肘;以我们常用的MySQL数据库为例,常规情况下的TPS大概只有1500左右(各种极端场景下另当别论)。 |
| 10 | + |
| 11 | +下面是MySQL官方所给出的一份测试数据: |
| 12 | + |
| 13 | +系统配置: |
| 14 | + |
| 15 | +Sun V40z / 4x 2390MHZ / Solaris 10 / 8GB RAM |
| 16 | + |
| 17 | +1m rows,Read Only,4 CPU |
| 18 | + |
| 19 | +|Connections| Trans/sec | |
| 20 | +|--|--| |
| 21 | +|1|382| |
| 22 | +|2|677| |
| 23 | +|4|1130| |
| 24 | +|8|1479| |
| 25 | +|32|1418| |
| 26 | +|256|947| |
| 27 | +|1024|224| |
| 28 | + |
| 29 | +[详细压测报告:](img/UC2005-Advanced-Innodb-Optimization.pdf) |
| 30 | + |
| 31 | +https://www.percona.com/blog/files/presentations/UC2005-Advanced-Innodb-Optimization.pdf |
| 32 | + |
| 33 | +对于一个PV上亿的网站,每一次请求涉及多次数据库交互,每天的读写请求量远远超过关系型数据库的处理能力,所以必须通过高效的缓存抵挡大部分的数据请求。 |
| 34 | + |
| 35 | +### 缓存类型 |
| 36 | + |
| 37 | +* 本地缓存 |
| 38 | + |
| 39 | + 本地缓存会减少网络层的交互,无论是本地内存还是磁盘,速度比较快。但对分布式系统来讲有一个缺点,当数据库更新时,没有一个简单有效的方法去更新本地缓存。 |
| 40 | + |
| 41 | + **本地缓存适用两种场景:** |
| 42 | + |
| 43 | + * 一、对缓存内容时效性要求不高,能接受一定的延迟,可以设置较短过期时间,被动失效更新保持数据的新鲜度。 |
| 44 | + * 二、缓存的内容不会改变。比如订单号与uid的映射关系,一旦创建就不会发生改变。 |
| 45 | + |
| 46 | + **注意问题:** |
| 47 | + |
| 48 | + * 内存Cache数据条目上限控制,避免内存占用过多导致应用瘫痪。 |
| 49 | + * 内存中的数据移出策略 |
| 50 | + * 虽然实现简单,但潜在的坑比较多,最好选择一些成熟的开源框架 |
| 51 | + |
| 52 | +* 分布式缓存 |
| 53 | + |
| 54 | + 本地缓存的使用很容易让你的应用服务器带上“状态”,而且容易受内存大小的限制。 |
| 55 | + |
| 56 | + 分布式缓存借助分布式的概念,集群化部署,独立运维,容量无上限,虽然会有网络传输的损耗,但这1~2ms的延迟相比其更多优势完成可以忽略。 |
| 57 | + |
| 58 | + 优秀的分布式缓存系统有大家所熟知的Memcached、Redis。对比关系型数据库和缓存存储,其在读和写性能上的差距可谓天壤之别,redis单节点已经可以做到8W+ QPS。设计方案时尽量把读写压力从数据库转移到缓存上,有效保护脆弱的关系型数据库。 |
| 59 | + |
| 60 | + |
| 61 | +* 客户端缓存 |
| 62 | + |
| 63 | + 大部分的web应用、微服务应用都会尽量做到无状态,方便于线性扩容。有状态的后端存储:DB、NoSQL、分布式文件系统、CDN等。 |
| 64 | + |
| 65 | + 另一个很重要的就是客户端缓存了,对客户端存储的合理使用,原本每天几千万甚至上亿的接口调用,一下就可能降到了几百万甚至更少,而且即便是用户更换浏览器,或者缓存丢失需要重新访问服务器,由于随机性比较强,请求分散,给服务器的压力也很小。另外再加上合理的缓存过期时间,就可以在数据准确和性能上做一个很好的折衷。 |
| 66 | + |
| 67 | + |
| 68 | +### 常用技术框架 |
| 69 | + |
| 70 | +* Guave |
| 71 | +* Memcached |
| 72 | +* Redis |
| 73 | + |
| 74 | +更多缓存框架:http://www.oschina.net/project/tag/109/cacheserver |
| 75 | + |
| 76 | +### 更新策略 |
| 77 | + |
| 78 | +* 被动失效 |
| 79 | + |
| 80 | + 缓存数据主要是服务读请求的,通常会设置一个过期时间,或者当数据库状态改变时,通过一个简单的delete操作,使数据失效掉;当下次再去读取时,如果发现数据过期了或者不存在了,那么就重新去数据库读取,然后更新到缓存中,这即是所谓的被动失效策略。 |
| 81 | + |
| 82 | + 被动策略有一个很大的风险,从缓存失效到数据再次被预热到cache这段时间,所有的读请求会直接打到DB上,对于一个高访问量的系统,很容易被击垮。 |
| 83 | + |
| 84 | + |
| 85 | +* 主动更新 |
| 86 | + |
| 87 | + 主动更新,很容易理解,就是数据库存储发生变化时,会直接同步更新到Cache,主要是为了解决cache空窗期引发的问题。比如电商的卖家修改商品详情,具有读多写少特点。 |
| 88 | + |
| 89 | + 但如果是读多写多,同样会带来另一个问题,就是并发更新。多台应用服务器同时访问一份数据是很正常的,这样就会存在一台服务器读取并修改了缓存数据,但是还没来得及写入的情况下,另一台服务器也读取并修改旧的数据,这时候,后写入的将会覆盖前面的,从而导致数据丢失。解决的方式主要有三种: |
| 90 | + |
| 91 | +``` |
| 92 | + 1、锁控制。这种方式一般在客户端实现(在服务端加锁是另外一种情况),其基本原理就是使用读写锁,即任何线程要调用写方法时,先要获取一个排他锁,阻塞住所有的其他访问,等自己完全修改完后才能释放。如果遇到其他线程也在修改或读取数据,那么则需要等待。锁控制虽然是一种方案,但是很少有真的这样去做的,其缺点显而易见,其并发性只存在于读操作之间,只要有写操作存在,就只能串行。 |
| 93 | +
|
| 94 | + 2、单版本机制(乐观锁)。为每份数据保存一个版本号,当缓存数据写入时,需要回传这个版本号,然后服务端将传入的版本号和数据当前的版本号进行比对,如果等于当前版本号,则成功写入,否则失败。这样解决方式比较简单;但是增加了高并发下客户端的写失败概率; |
| 95 | +
|
| 96 | + 3、多版本机制。即存储系统为每个数据保存多份,每份都有自己的版本号,互不冲突,然后通过一定的策略来定期合并,再或者就是交由客户端自己去选择读取哪个版本的数据。 |
| 97 | +``` |
| 98 | + |
| 99 | +### 序列化 |
| 100 | + |
| 101 | +分布式缓存的本质就是将所有的业务数据对象序列化为字节数组,然后保存到自己的内存中。所使用的序列化方案也自然会成为影响系统性能的关键点之一 |
| 102 | + |
| 103 | +* 序列化速度 |
| 104 | +* 对象压缩比例 |
| 105 | +* 支持的序列化数据类型范围 |
| 106 | +* 反序列化的速度 |
| 107 | +* 框架接入易用性 |
| 108 | + |
| 109 | +常见的序列化框架: |
| 110 | + |
| 111 | +* Java源生序列化 |
| 112 | +* Hessian |
| 113 | +* Protobuf |
| 114 | +* Kryo |
| 115 | + |
| 116 | +### 开发注意事项 |
| 117 | + |
| 118 | +* 评估当前业务使用的空间大小。避免空间不足,导致热数据被置换出去,影响缓存命中率 |
| 119 | +* 不要把缓存当DB使用,因为它会丢失 |
| 120 | +* 最好设置过期时间,可以自己回收 |
| 121 | +* key定义遵循一定规则,相同业务采用同一前缀 |
| 122 | +* 缓存对象粒度。高内聚低耦合,考虑尽可能复用,不要一个小字段修改导整个大对象全部失效 |
| 123 | + |
| 124 | +``` |
| 125 | +方案一: |
| 126 | +uid---> 发过的贴子内容列表 |
| 127 | +
|
| 128 | +方案二: |
| 129 | +uid--->发过的贴子tid列表 |
| 130 | +tid--->贴子内容 |
| 131 | +
|
| 132 | +``` |
| 133 | + |
| 134 | +* 另外缓存对象大小要控制,不要过大,占用过多带宽。之前遇到过一个业务团队,单key下挂了5M的大对象,每次用时,从缓存中取出,反序列化,然后取其中一小部分。后来随着业务并发量上升,把网卡打爆,进而影响其它正常业务访问。 |
| 135 | +* 根据业务需求,选择合适的缓存框架,比如memcache只支持kv对存储,redis则支持较丰富的数据结构 |
| 136 | +* 是否要引入多级缓存,本地内存--》非持久化缓存(如memcache)---》持久化缓存---》DB,要注意数据一致性问题 |
| 137 | +* 提前考虑扩容问题 |
| 138 | + |
| 139 | + |
| 140 | +### 问题汇总 |
| 141 | + |
| 142 | +##### 1、缓存穿透 |
| 143 | + |
| 144 | +我们在项目中使用缓存通常都是先检查缓存中是否存在,如果存在直接返回缓存内容,如果不存在就直接查询数据库然后再缓存查询结果返回。这个时候如果我们查询的某一个数据在缓存中一直不存在,就会造成每一次请求都查询DB,这样缓存就失去了意义,在流量大时,可能DB就挂掉了。那这种问题有什么好办法解决呢? |
| 145 | + |
| 146 | +有一个比较巧妙的做法是,可以将这个不存在的key预先设定一个值。比如,"NULL" ,在返回这个NULL值的时候,我们的应用就可以认为这是不存在的key。 |
| 147 | + |
| 148 | +缓存穿透如果被恶意攻击,造成的影响面很容易放大。比如文章详情页,查询一个不存在的tid,每次都会访问DB,如果有人恶意破坏,很可能直接对DB造成影响。 |
| 149 | + |
| 150 | +##### 2、缓存集体失效 |
| 151 | + |
| 152 | +对于一些活动期间的数据通常会提前预热到缓存中,并设置一个过期时间,如果系统的并发量很高,恰巧缓存又失效了,此时会将压力转嫁给后面的DB,很容易击垮系统。 |
| 153 | + |
| 154 | +那如何解决这些问题呢? |
| 155 | + |
| 156 | +其中的一个简单方案就是将缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。还有一种方式,就是计算好缓存的过期时间。 |
| 157 | + |
| 158 | +##### 3、DB和缓存不一致 |
| 159 | + |
| 160 | +当修改了数据库后,没有及时修改缓存,或者缓存服务器挂了。如果是因为网络问题引起的没有及时更新,可以通过重试机制来解决。而缓存服务器挂了,请求首先自然也就无法到达,从而直接访问到数据库。那么我们在修改数据库后,无法修改缓存,这时候可以将这条数据放到数据库中,同时启动一个异步任务定时去检测缓存服务器是否连接成功,一旦连接成功则从数据库中按顺序取出修改数据,依次进行缓存最新值的修改。 |
| 161 | + |
| 162 | +##### 4、命中率较低,影响性能 |
| 163 | + |
| 164 | +* 过期时间太短, 这种场景可以根据实际情况适当增大过期时间 |
| 165 | +* 存在不合理缓存删除逻辑, 导致有效的缓存频繁被删除 |
| 166 | +* 不合理的key规则设计, 每次缓存访问的key都在变化, 导致无法命中缓存和频繁的新缓存创建 |
| 167 | +* key确实不存在,但是应用还是在频繁的访问, 这种应该从业务逻辑上杜绝 |
| 168 | + |
| 169 | +### 性能指标 |
| 170 | + |
| 171 | +* 缓存空间的使用率 |
| 172 | +* topN 命令的执行次数 |
| 173 | +* 缓存的命中率 |
| 174 | +* 缓存的接口平均RT,最大RT,最小RT |
| 175 | +* 缓存的QPS |
| 176 | +* 网络出口流量 |
| 177 | +* 客户端连接数 |
| 178 | +* key个数统计 |
0 commit comments