一、数据一致性
1.缓存使用场景
针对读多写少的高并发场景,我们可以使用缓存来提升查询速度。
使用Redis作为缓存的流程
-
如果数据在Redis中存在,客户端可以直接从Redis中拿到数据,不用直接访问数据库。
-
客户端新增数据,只保存到数据库中,如果Redis中没有,先到数据库查询然后写入到缓存,再返回给客户端
2.一致性问题
因为数据一定是以数据库为准的,如果Redis中没有数据,就不存在这个问题。当Redis和数据库都有同一条记录,而这条记录发生变化的时候,就可能出现一致性问题。
当缓存的数据发生变化(修改、删除)时,我们纪要操作数据库中的数据也要操作Redis缓存的数据,才能保证缓存和数据库的数据一致。所以问题出现了:
- Redis中的旧数据删除还是更新?
- 先操作Redis再操作数据库
- 先操作数据库再操作Redis
(1)删除/更新
当存储的数据发生变化,Redis的数据也要更新的时候,我们有两种方案,一直是直接更新Redis数据,调用set;还有一种时直接删除Redis中的数据,客户端下次查询的时候重新写入。
首先需要考虑,更新缓存前是否要经过其他的各种操作,最后才能得到最新的数据,而不是从数据库直接拿到的值?基于这种复杂场景的考量,建议还是直接删除缓存,这种方案更加简单,而且避免了数据库的数据和缓存不一致的情况。
(2)先数据库后缓存
正常情况
更新数据库:成功
更新缓存:成功
异常情况
- 更新数据库失败,抛出异常,缓存不更新,不会出现数据不一致的额情况
- 更新数据库成功,删除缓存失败。数据库是新数据,缓存是旧数据,发生了不一致情况。
(3)先缓存后数据库
正常情况
更新缓存:成功
更新数据库:成功
异常情况
- 删除缓存,程序捕获异常,不会走到下一步,不会出现数据不一致的情况
- 删除缓存成功,更新数据库失败。因为以数据库的数据为准,所以不存在数据不一致的情况。
但是当并发情况下:线程A需要更新数据,首先删除了缓存
线程B需要查询数据,发现缓存不存在,到数据库查询旧数据,然后写入Redis返回
-
线程A更新数据库
这时候,缓存中是旧数据,数据库中是新数据,造成了数据不一致
最终方案
双删,先删除Redis,再更新数据库,再删除Redis。
二、高并发问题
再Redis存储的所有数据中,有一部分是被频繁访问的。有两种情况可能导致热点问题。一是用户集中访问的数据,比如抢购商品,热点新闻等。还有一个是再数据进行分片的情况下,负载不均衡,超过了单个服务的承受能力。热点问题的出现可能会引起服务的不可用,最终造成数据库压力过大。
1.热点数据发现
首先Redis中有缓存淘汰机制,能够留下一些热点的Key,不管是LRU还是LFU
除了自动的缓存淘汰机制,如何找出访问频率高的key?
(1)客户端
在客户端,我们可以统计热点数据的访问频率。比如再所有调用get、set方法的地方加上key 的计数。
会导致的问题:
- 对客户端的代码造成入侵
- 不知道要存多少个key,可能会出现内存泄漏的问题OOM
- 只能统计当前客户端热点key
(2)代理层
可以使用Spring的AOP动态代理,来完成上面客户端的操作。但是如果项目中没有用到支持代理相关功能的框架呢?
(3)服务层
Redis中有一个monitor命令,可以监控所有Redis执行的命令
Jedis中也封装了相关API
jedis.monitor(new JedisMonitor(){
@Override
public void onCommand(String command){
System.out.println("#monitor:"+command);
}
})
(4)机器层面
通过TCP协议进行抓包,也有一些开源的方案供使用。
2.缓存雪崩
(1)什么是缓存雪崩?
当Redis大量热点数据同时过期,此时请求量又特别大,就会导致所有请求打到数据库中。
(2)解决方案
- 查询写入Redis时,相同的查询操作只允许一条请求访问到数据库,其他的等待,并访问Redis。
- 缓存定时预先更新,避免同时失效
- 通过加随机数,使key再不同的事件过期(梯度过期)
- 热点缓存不设置过期时间
3. 缓存穿透
当请求访问不存在的数据,会直接跳过Redis访问数据库,此时数据库中也不存在。
解决方案:1. 缓存空数据。2.缓存自定义特殊字符串
当这个请求再次进入系统时,会访问到自定义的特殊字符串,这时候系统就知道访问的是不存在的数据,直接返回静态资源404给客户端。
如果有恶意请求,发送大量不重复请求到服务端,虽然我们已经要写入自定义特殊字符到Redis,但由于它每次请求的key都是不同的,还是会给数据库造成很大的压力,占用数据库资源。
(1)经典面试题
如何在海量数据中,快速判断一个key是否存在?
首先我们想到的是先放入客户端缓存,避免请求服务端数据库及Redis。那么我们的基本数据结构能支撑这么大数据量吗,会发生OOM,内存溢出。
最节省空间的数据结构—位图。位图是一个有序数组,只有两个值,0和1。0代表不存在,1代表存在。
如何使用位图来做这个需求呢?
- 可以先将key做hash,再取模,获取到一个索引下标,将位图中对应的下标元素变成1
- 如果这样做,哈希碰撞的几率相当大,比如多个key哈希取模后下标一致,那么这些key是否真正存在呢?
- 可以将存在的key多个哈希函数运算取模放入位图中,比如key=java,第一次取模放在index=1,第二次取模放在index=5,第三次取模放在index=23,当这三个index对应的元素都为1时,我才大概率确认这个key确实是存在的。
- 当一个key经过三次哈希函数的运算后,生成的三个index中只要有一个index对应元素为0,那这个key必不存在
- 当一个key经过三次哈希函数运算后,生成的三个index对应元素都为1,那这个key大概率存在(不是百分百存在)
布隆过滤器
谷歌的Guava中提供了一个现成布隆过滤器,实现原理大致如上所述。可以直接拿来使用。
使用在访问Redis之前: