一、缓存架构设计

缓存的设计要分多个层次,在不同的层次上选择不同的缓存,包括JVM缓存、文件缓存和Redis缓存。

JVM缓存:JVM缓存就是本地缓存,设计在应用服务器中(tomcat)。通常可以采用Ehcache和Guava Cache,在互联网应用中,由于要处理高并发,通常选择GuavaCache。适用场景:对性能有非常高的要求,不经常变化,占用内存不大,有访问整个集合的需求,数据允许不时时一致。

文件缓存: 这里的文件缓存是基于http协议的文件缓存,一般放在nginx中。因为静态文件(比如css,js, 图片)中,很多都是不经常更新的。nginx使用proxy_cache将用户的请求缓存到本地一个目录。

Redis缓存: 分布式缓存,采用主从+哨兵或RedisCluster的方式缓存数据库的数据。在实际开发中作为数据库使用,数据要完整。作为缓存使用,一般作为Mybatis的二级缓存使用。

二、缓存预热

缓存预热就是系统启动前,提前将相关的缓存数据直接加载到缓存系统。避免在用户请求的时候直接查数据库。

加载缓存思路:数据量不大,可以在项目启动的时候自动进行加载。利用定时任务刷新缓存,将数据库的数据刷新到缓存中。

三、缓存问题及解决方案

3.1  缓存穿透

概念:高并发下查询缓存和数据库中key都不存在的数据,会一直穿过缓存查询数据库。导致数据库压力过大而宕机。

解决方案:1. 查询结果为空的情况也缓存,缓存时间(ttl)设置短一点或该key对应的数据insert之后删除缓存。缺点:缓存太多空值占用了更多的空间。2.使用布隆过滤器,在缓存之前在加一层布隆过滤器,在查询的时候先去布隆过滤器查询 key 是否存在,如果不存在就直接返回,存在再查缓存和DB。

布隆过滤器的原理:当一个元素被加入集合时,通过K个Hash函数将这个元素映射成一个数组中的K个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在。这就是布隆过滤器的基本思想。

3.2  缓存雪崩

概念:突然间大量的key失效了或redis重启导致大量访问数据库,引起数据库崩溃。

解决方案:1.不同的key设置不同的有效期  2. 设置二级缓存  

3.3 缓存击穿

概念:数据库中有缓存中没有,常见的是热点key过期,高并发访问导致数据库崩溃。

解决方案: 1. 用分布式锁控制访问的线程,使用redis的setnx互斥锁先进行判断,这样其他线程就处于等待状态,保证不会有大并发操作去操作数据库。 2. 不设超时时间,可能会造成写一致问题,采用延时双删策略。

3.4 数据不一致

强一致性很难,追求最终一致性。采用延时双删策略:1、先更新数据库同时删除缓存项(key),等读的时候再填充缓存    2、2秒后再删除一次缓存项(key)    3、设置缓存过期时间 Expired Time 比如 10秒 或1小时

4、将缓存删除失败记录到日志中,利用脚本提取失败记录再次删除(缓存失效期过长 7*24)。

3.3 数据并发竞争

这里的并发指的是多个redis的client同时set同一个key引起的并发问题,可能导致最终值不正确。

第一种方案:Redis分布式锁+时间戳, 主要用到的是redis函数是setnx()。要求key的操作需要顺序执行,所以需要保存一个时间戳判断set顺序。假设系统B先抢到锁,将key1设置为{ValueB 7:05}。接下来系统A抢到锁,发现自己的key1的时间戳早于缓存中的时间戳(7:00<7:05),那就不做set操作了。

第二种方案:消息队列,在并发量过大的情况下,可以通过消息中间件进行处理,把并行读写进行串行化。把Redis的set操作放在队列中使其串行化,必须的一个一个执行。

3.4 Hot Key

如何发现热key: 1、预估热key,比如秒杀的商品、火爆的新闻等  2、在客户端进行统计,实现简单,加一行代码即可  3、如果是Proxy,比如Codis,可以在Proxy端收集  4、利用Redis自带的命令,monitor、hotkeys。但是执行缓慢(不要用)  5、利用基于大数据领域的流式计算技术来进行实时数据访问次数的统计,比如 Storm、Spark Streaming、Flink,这些技术都是可以的。发现热点数据后可以写到zookeeper中。

如何处理热Key:1、变分布式缓存为本地缓存 发现热key后,把缓存数据取出后,直接加载到本地缓存中。可以采用Ehcache、Guava Cache都可以,这样系统在访问热key数据时就可以直接访问自己的缓存了。(数据不要求时时一致) 2、在每个Redis主节点上备份热key数据,这样在读取时可以采用随机读取的方式,将访问压力负载到每个Redis上。 3、利用对热点数据访问的限流熔断保护措施。每个系统实例每秒最多请求缓存集群读操作不超过 400 次,一超过就可以熔断掉,不让请求缓存集群,直接返回一个空白信息,然后用户稍后会自行再次重新刷新页面之类的。(首页不行,系统友好性差)通过系统层自己直接加限流熔断保护措施,可以很好的保护后面的缓存集群。

3.5 Big Key

概念:大key指的是存储的值(Value)非常大,常见场景:热门话题下的讨论,大V的粉丝列表,序列化的图片......。  大key会占用内存,性能下降导致复制异常。

处理:优化big key的原则就是string减少字符串长度,list、hash、set、zset等减少成员数。1、string类型的big key,尽量不要存入Redis中,可以使用文档型数据库MongoDB或缓存到CDN上。如果必须用Redis存储,最好单独存储,不要和其他的key一起存储。采用一主一从或多从。2、单个简单的key存储的value很大,可以尝试将对象分拆成几个key-value, 使用mget获取值,这样分拆的意义在于分拆单次操作的压力,将操作压力平摊到多次操作中,降低对redis的IO影响。

3、hash, set,zset,list 中存储过多的元素,可以将这些元素分拆。

4、删除大key时不要使用del,因为del是阻塞命令,删除时会影响性能,使用 lazy delete (unlink命令)。

3.6 乐观锁

乐观锁基于CAS(Compare And Swap)思想(比较并替换),是不具有互斥性,不会产生锁等待而消耗资源,但是需要反复的重试,但也是因为重试的机制,能比较快的响应。因此我们可以利用redis来实现乐观锁。具体思路如下:

1、利用redis的watch功能,监控这个redisKey的状态值

2、获取redisKey的值

3、创建redis事务

4、给这个key的值+1

5、然后去执行这个事务,如果key的值被修改过则回滚,key不加1。

3.7 单线程的Redis为什么这么快

redis在内存中操作,持久化只是数据的备份,正常情况下内存和硬盘不会频繁swap。基于内存,CPU不是Redis的瓶颈。

多机主从,集群数据扩展。

maxmemory的设置+淘汰策略。

数据结构简单,有压缩处理,是专门设计的。

单线程没有锁,没有多线程的切换和调度,不会死锁,没有性能消耗。

使用I/O多路复用模型,非阻塞IO。

构建了多种通信模式,进一步提升性能。

进行持久化的时候会以子进程的方式执行,主进程不阻塞。

3.8 Redis为什么是单线程的

1)绝大部分请求是纯粹的内存操作(非常快速)

2)采用单线程,避免了不必要的上下文切换和竞争条件
3)非阻塞IO

3.9 有没有尝试进行多机redis 的部署?如何保证数据一致的主从复制,读写分离

1、redis的复制功能是支持多个数据库之间的数据同步。一类是主数据库(master)一类是从数据库(slave),主数据库可以进行读写操作,当发生写操作的时候自动将数据同步到从数据库,而从数据库一般是只读的,并接收主数据库同步过来的数据,一个主数据库可以有多个从数据库,而一个从数据库只能有一个主数据库。
2、通过redis的复制功能可以很好的实现数据库的读写分离,提高服务器的负载能力。主数据库主要进行写操作,而从数据库负责读操作。

当一个从数据库启动时,会向主数据库发送sync命令,主数据库接收到sync命令后会开始在后台保存快照(执行rdb操作),并将保存期间接收到的命令缓存起来,当快照完成后,redis会将快照文件和所有缓存的命令发送给从数据库。从数据库收到后,会载入快照文件并执行收到的缓存的命令。