下面以查询业务中缓存使用为例来说明:
1 我们平时写程序时一般按照如下流程来使用缓存
根据上面的流程,当缓存中没有数据A或A失效时,如果在高峰期,可能会出现所有的压力压到DB,导致DB变慢,从而导致所有涉及到db的业务变慢,甚至引起服务挂掉。
2 所以需要一个类似全局的锁,当一个客户端或线程查询缓存时,缓存中没有数据,其他客户端都等着之前的客户端从数据库中查询数据,改进的流程如下:
改进的地方使用了redis,其思想在于先去设置一个kv对,如果该kv对已经存在,则说明有线程或别的客户端已经去访问db了,只要等一会再从缓存取数据即可。
当然,可以不用redis,思想是相通的。
但是这个流程仍有一个问题,那就是只有一个客户端或线程去访问db,但是如果这个客户端挂掉,其余的客户端都在那等着,会陷入死循环。
3 所以需要这个全局的锁有个超时时间,超时自动删除,防止创建它的客户端死掉,改进的流程如下:
客户端在成功设置了kv对后,再对key做一个过期设置,这样,即使客户端由于某种原因挂掉了,key到期会被自动删除,其他客户端又可以重现进行设置了。
但是还有一个问题,假设客户端在对key设置过期时间时挂掉了,又会陷入第二中方案描述的情况。
4 所以需要在设置锁时加上过期时间,即设置锁和设置超时时间是个原子操作,要么成功,要么失败。但是redis的setNx不支持同时设置过期时间,所以只能将过期时间放到value中,而过期则需要其他客户端在获取不到锁时执行检查任务,看看该key是否过期了,过期就把key删除:
不幸的是,即使如上策略,也会发生问题,问题比较复杂,我描述如下:
1 A客户端设置kv获得锁: 123->1389606178281,5 表示key为123的键值对,在时间为1389606178281时设置,5秒后过期
2 B客户端设置kv,发现已经存在,故获取key为123的value,检查是否过期。
3 C客户端设置kv,发现已经存在,故获取key为123的value,检查是否过期。
假设B和C客户端同时执行检查,而此时如果恰巧A客户端超时了,那么B或C会将key为123的键值对删除。
我们假设B执行的快,那么key会被B删除,那么其他客户端此时会再次创建kv。
问题在这出现了,假设其他客户端执行的快,又创建了kv,而此时C由于检查的是A客户端那时的kv,即应该属于脏数据,所以C又执行了删除。
所以可能照样会有不止一个的客户端进行DB操作。
当然了,这种情况已经属于比较极端的情况,已经能够防止大量客户端访问DB了,但是还不够完美,原因就是出在判断超时是用的是脏数据。
如果想解决这种情况,我们需要使用redis的一个特性getset key value, 即将key设置为value,并返回旧的value,这个操作是原子的。
5 改进的流程如下:
改造后,即使多个线程或客户端执行getset,后执行的获取返回的值由于是新的客户端设置进去的,所以不会出现超时,就不存在多个客户端操作db了