最近有人问我这个问题,我个人没有这方面的实战经验。我个人的想法是,由于访问数据库并写入缓存需要一定的时间。可能导致较早的部分请求直接读取数据库,当这部分数据要写入缓存时,判断缓存是否存在,不存在则写入,存在则不写入,并返回结果。
if ($cache) {
return $cache;
} else {
$data = read database;
if (!$cache) write $cache $data;
return $data;
}
但思前想后,觉得这样的回答似乎没有正确回答多个请求同时读取数据库的问题,虽然可以屏蔽后期的请求直接访问数据库,但前期还是有多了链接直接访问了数据库。不知道各位是否有更好的解决方案。求教!
回复内容:
答案都在问题里了。
「如果缓存失效」
在设计的时候就可以把 cache 设计成持久化的,避免因为失效导致缓存被穿透。如果对稳定性有更高的要求,就在 cache 上下功夫做灾备。比如 redis 的主从模式,rdb dump 等等。
如果系统或者涉及 cache 的 feature 是第一次上线,则需要提前对缓存做预热。方法有很多,写脚本或者灰度发布的方式都可以。
「瞬间大量的请求会访问数据库」
这里有两个问题,一是瞬间大量的请求怎么处理,二是如何避免这么多请求对数据库造成压力。
具体如何实施还得看业务场景,但解决的思路就是尽可能避免瞬间大量的请求,再就是大量请求产生时不要让数据库压力过大。
限流的方法要看业务场景,并非所有场景都适合限流所以这里不多说。所以这里要限制的是后端到数据库的最大连接数自己数据库自己能接受的最大连接数。只要请求数据库设计合理,即使有瞬间很高的并发一般也不会造成什么实际的问题。
所以怎么从代码层面解决这个问题,是取决于你是如何设计这个系统的。
二级缓存。把数据放在一个失效时间比较长的key里。雪崩的时候加锁,保证只有一个php进程访问数据库,其余的看到锁就不再访问,直接返回在缓存里的数据。效果就是虽然好几个人没看到最新的数据,再刷一下就成了。抢到锁的看到了最新数据。锁可以用mc的add来实现。在我大渣浪的时候这套东西扛过nba的直播,c好几十k,数据库没事,带宽才是问题
facebook 放过一篇论文《Scaling Memcache at Facebook》有讨论过这个问题:
3.2.1 Leases
We introduce a new mechanism we call leases to address
two problems: stale sets and thundering herds.
其中 "thundering herds" 正是楼主提到的数据库穿透问题,一个热的缓存如果失效,在第一个访问数据库的请求得到结果写入缓存之前,期间的大量请求打穿到数据库;然后 “stale set” 属于数据一致性问题,假如一个实例更新了数据想去刷新缓存,而另一个实例读 miss 尝试读取数据库,这时两次缓存写入顺序不能保证,可能会导致过期数据写入缓存。
这两个问题都是 look-aside cache 所固有的,需要提供一个机制来协调缓存的写入,这篇论文给出的方案就是 lease 机制,限制同一时刻一个键只有拥有唯一 lease 的客户端才能有权写入缓存:
- 如果 get 某键读 miss,返回客户端一个 64 位的 lease;
- 然后该键在写入之前如果收到 get 请求,将返回一个 hot miss 报错,客户端依据它判断自己要稍后重试,而不向数据库读取数据;
- 如果该键收到 delete 请求,那么会使 lease 失效;持有失效 lease 的 set 请求仍将成功,但后来的 get 请求将得到 hot miss 报错,并携带一个新的 lease;这里的 hot miss 报错中带有最后的值,但认为它处于 stale 状态,留给客户端去判断是否采用它,在一致性要求不严格的场景中可以进一步减少数据库请求;
这一来允许 memcache 服务端协调数据库的访问,从而解决这两个问题。
不过 lease 方案并不完美,因为 1. 需要改 memcache;2. 仍泄露逻辑到客户端,要求客户端遵循 lease 和 hot miss 的约定。
在 facebook 后面的论文《TAO: Facebook's Distributed Data Store for the Social Graph》中介绍TAO 系统尝试解决的问题之一提到:
Distributed control logic: In a lookaside cache architecture
the control logic is run on clients that don’t communicate
with each other. This increases the number of
failure modes, and makes it difficult to avoid thundering
herds. Nishtala et al. provide an in-depth discussion of
the problems and present leases, a general solution [21].
For objects and associations the fixed API allows us to
move the control logic into the cache itself, where the
problem can be solved more efficiently.
也就是说,我们并不一定非 look aside cache 不可,如果把缓存的修改入口封装起来,走 write though cache,就不需要分布式地去协调所有客户端,在一个地方排队就够了。
References
- Scaling Memcache at Facebook
- TAO: Facebook's Distributed Data Store for the Social Graph
- https://www.quora.com/How-does-the-lease-token-solve-the-stale-sets-problem-in-Facebooks-memcached-servers
根据实际场景选择方案,访问数据库的请求处理使用队列并对同样的请求做加锁处理是比较稳妥的解决思路。
这种问题业内有个名词,叫雪崩效应,就是说集群在正常负载的时候没有问题,一旦其中几台服务器崩了,服务器过载的压力压到后端数据库上面,就会导致整个集群像雪崩一样完全崩溃。
读多写少的业务场景下:
对于这种问题,缓存挡在数据库服务器前面是必须的,即问题的解决方案之一,根据sql查询的条件在缓存服务器层面做锁处理,同样的请求只放一个到后端数据库上面,其它请求则阻塞等待数据更新。
其次是挡住这些缓存大批失效导致的峰值,不允许直接连接数据库进行查询,在缓存服务器向数据库发起的请求处理上全部都要使用队列,把峰值分摊到更长的时间段上避免后端数据库受到冲击。
此外为了避免大批缓存失效又重新生成的这些缓存再次同时失效的问题,缓存服务器的数据缓存时间需要去一个时间令牌分发的服务器申请令牌,在基本缓存时间的基础上,再加上一个令牌缓存时间,将峰值分摊到更长的时间上。
这样的处理可以避免数据库负载被峰值请求冲垮。
写入比例更大的业务场景下:对数据更新频繁且实时性要求较高的时候,比如直播室场景,抢购场景,缓存失效时间很短。需要考虑业务切分,把同类业务路由到不同服务器上面,独立开来。尽可能把写入负载分摊到单点集群可以承担的程度。技术上面可选的策略就和业务相关度很大了,类似直播室的场景,甚至不需要走到数据库服务器,业务都在缓存上面周转,持久化到数据库的业务则走异步扔队列里面慢慢处理。电商抢购这类必须要校验的业务又不一样,但是思路还是同样的,秒杀业务瞬时进来的业务太大,那就加验证码加排队,总之尽可能拖延提交订单的时间,把瞬时负载分摊到系统可以接受的时间段内,数据库层面依然还是加锁与队列,不过在队列入队上还需要做个锁,一旦排队长度超过商品总数的一个倍数(具体按需求过往订单支付成功率计算),则加锁将后续请求挡住不再入队。
还有很多写入业务量非常极端的场景,类似LBS产品的地理位置更新,SNS产品的消息推送,大致思路都是类似的,加锁和队列,数据库层面尽可能选用写入性能优良的nosql承担这些持久化的需求。否则也尽可能减少索引的使用,比如把mysql当key-value数据库用等形式,尽可能减少写入的性能消耗提高。
在DB的监控或者接口层面处理
高并发其实是区分宏观和微观的,你理解的并发是同一时刻多个请求读数据库,但是其实请求对于计算机一定不是同一时刻,只是宏观上是同时。比如1-10毫秒之间或者微秒之间,那么其实计算机是怎么处理的呢?说白了是会对计算操作,io操作做独立的进程来分配处理器的任务,内存的分配,比如同一时刻你在读数据库,同一时刻又有别的io操作,那么你这里也可以再模拟一下计算机这种设计。
比如你在写缓存时,把这个写操作推到一个独立的进程,做一个写缓存的队列,然后再做一个检测进程来组织这个队列的处理顺序和失败分支,比如提前和后排,比如写缓存操作失败了,那么进行队列的下一个任务或者再试一次。所有写入成功的缓存数据再推入缓存池,那么这个队列会保留更新缓存的所有任务,也不会阻塞主进程,也就是用户访问的主进程不会被阻塞。他们只是去读缓存池的数据,不关心时效。如果写得慢就读老的,甚至数据库挂了,都不会影响用户读数据。
并发的概念再仔细想想都是在一个相对的时间里来看待的。计算机会自己处理,你也不用太操心,业务代码只需要解决阻塞的问题就ok了。
ps:我是个前端,大过年无聊瞎扯的…折叠吧。
再补充一下,如果缓存失效了,这个问题要解决的是缓存的容灾处理办法,比如多个缓存池快速切换,负载均衡等。超大量请求直接访问数据库,这个可以用多机多数据库,多个进程来读,来解决。我觉得从代码层面不太好做…
因为解决办法是扩容加分配请求到多机,不能实时扩容又想支持高并发大量请求是不可能的啊…光让驴跑不给吃草,就想着怎么给驴美容不好用啊…
-------
专业上这种雪崩后缓存穿透直接到了db层,实际先挂的是带宽,db不会是瓶颈,之后要解决可以使用最大连接数来控制读数据库,超过的使用2级缓存来做,之后预警,或者动态扩容机制来解决。(咨询的后端同事)
throttle
解决之道就在书上, 在(书名忘记了,封面上有动车的那个)那个小书上开始就是介绍的这个问题,然后提出了好几种的解决方案。。
三本书,操作系统,数据库原理,分布式系统。很古老的问题。
1、假设单机缓冲。锁机制就可以了,因为你的问题是读数据,不是别的。无论是多进程还是多线程,加锁就可以了。
2、用缓冲池,降低等待的延迟。
3、以上是操作系统的内容。
4、如果同时有更新操作,要考虑数据一致性的问题,即悲观锁或乐观锁。
1、服务本身要提供过载保护、熔断机制,说到底就是限流,要懂得保护自己
2、缓存重建的时候要加锁,cache mutex