Redis

性能优秀,数据在内存中,读写速度非常快。单进程单线程,是线程安全的。丰富的数据类型,支持字符串 (strings)、散列(hashes)、列表(lists)、集合(sets)、有序集合(sorted sets) 等。支持数据持久化,可以将内存中数据保存在磁盘中,重启时加载。主从复制,哨兵,高可用。

Redis 是使用了一个「哈希表」保存所有键值对,哈希桶存放的是指向键值对数据的指针(dictEntry*), key 指向的是 String 对象。

void * key 和 void * value 指针指向的是 Redis 对象。Redis 中的每个对象都由 redisObject 结构表示:type,标识该对象是什么类型的对象(String 对象、 List 对象、Hash 对象、Set 对象和 Zset 对象);encoding,标识该对象使用了哪种底层的数据结构;ptr,指向底层数据结构的指针。

58d3987af2af868dca965193fb27c464

数据类型
有效期

Redis 中有个设置缓存时间过期的功能,即对存储在 redis 数据库中的值可以设置一个过期时间。redisTemplate.opsForValue().set(key, value, 20, TimeUnit.SECONDS);

内存淘汰机制

如果定期删除漏掉了很多过期 key,然后也没及时去查,也就没走惰性删除,如果大量过期 key 堆积在内存里,导致 redis 内存块耗尽。

持久化机制

1,只追加文件(append-only file,AOF):类似mysql的基于语句的binlog方式,每执行一条会更改 Redis 中的数据的命令,先执行写操作命令后,再将该命令记录到 AOF 日志里的(避免额外的语法检查开销,不会阻塞当前写操作命令的执行)。

当 Redis 在还没来得及将命令写入到硬盘时,服务器发生宕机了,这个数据就会有丢失的风险。写操作命令执行成功后才记录到 AOF 日志,所以不会阻塞当前写操作命令的执行,但是可能会给「下一个」命令带来阻塞风险(写操作和日志是同步的,上一次的写日志会阻塞下一次的写数据)。

Redis 执行完写操作命令后,会将命令追加到 server.aof_buf 缓冲区->然后通过 write() 系统调用,将 aof_buf 缓冲区的数据拷贝到了内核缓冲区 page cache,等待内核将数据写入硬盘->具体内核缓冲区的数据什么时候写入到硬盘,由内核决定。写文件频率可以根据需求调节,高性能VS高可靠:总是、每秒、被动由OS决定,三种策略只是将内核缓冲区写入文件的频率不同。

98987d9417b2bab43087f45fc959d32a

重写机制:尽管某个键值对被多条写命令反复修改,最终也只需要根据这个「键值对」当前的最新状态,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令,这样就减少了 AOF 文件中的命令数量。最后在重写工作完成后,将新的 AOF 文件覆盖现有的 AOF 文件。重写的过程是由后台子进程完成的,这样可以使得主进程可以继续正常处理命令。

用 AOF 日志的方式来恢复数据其实是很慢的,因为 Redis 执行命令由单线程负责的,而 AOF 日志恢复数据的方式是顺序执行日志里的每一条命令,如果 AOF 日志很大,这个「重放」的过程就会很慢了。

2,快照(snapshotting,RDB):Redis 可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本,内容是二进制数据。Redis 创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis 主从结构,主要用来提高 Redis 性能),还可以将快照留在原地以便重启服务器的时候使用。

在 Redis 恢复数据时,直接将 RDB 文件(二进制文件)读入内存就可以,不需要像 AOF 那样还需要额外执行操作命令的步骤才能恢复数据。执行快照是一个比较重的操作,如果频率太频繁,可能会对 Redis 性能产生影响。如果频率太低,服务器故障时,丢失的数据会更多。

执行快照时,数据被修改:如果主线程(父进程)要修改共享数据里的某一块数据(比如键值对 A)时,就会发生写时复制,于是这块数据的物理内存就会被复制一份(键值对 A'),然后主线程在这个数据副本(键值对 A')进行修改操作。与此同时,bgsave 子进程可以继续把原来的数据(键值对 A)写入到 RDB 文件。

3,混合持久化,AOF 文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据。重启 Redis 加载数据的时候,由于前半部分是 RDB 内容,这样加载的时候速度会很快。AOF内容写入成本底,可以频繁写入,可以使得数据更少的丢失。

缓存雪崩

为了保证缓存中的数据与数据库中的数据一致性,会给 Redis 里的数据设置过期时间,缓存同一时间大面积的失效或者 Redis 故障宕机,所以后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。

解决办法:宕机:1,尽量保证整个 redis 集群的高可用性,发现机器宕机尽快补上;2,对请求限流 ,避免 MySQL在redis崩溃后也崩掉,只要数据库正常工作,就可以处理用户请求,保证系统仍然可用。缓失效:1,把每个 Key 的失效时间都加个随机值,保证数据不会再同一时间大面积失效。2,对请求限流 。3,对缓存数据可以使用两个 key,一个是主 key,会设置过期时间,一个是备 key,不会设置过期,它们只是 key 不一样,但是 value 值是一样的,相当于给缓存数据做了个副本。4,让缓存“永久有效”,并将更新缓存的工作交由后台线程定时更新。

当业务线程访问不到「主 key 」的缓存数据时,就直接返回「备 key 」的缓存数据,然后在更新缓存的时候,同时更新「主 key 」和「备 key 」的数据。3,选择合适的内存淘汰策略,防止爆内存。5,利用 redis 持久化机制保存的数据尽快恢复缓存。

14534869-cefa2f5519af3a09

缓存击穿

缓存击穿是指一个 Key 非常热点,在不停地扛着大量的请求,大并发集中对这一个点进行访问,当这个 Key 在失效的瞬间,持续的大并发直接落到了数据库上,就在这个 Key 的点上击穿了缓存。

解决办法:1,设置热点数据永不过期,由后台异步更新缓存。2,在访问数据据时加上互斥锁,保证同一时间只有一个业务线程更新缓存。

缓存穿透

黑客恶意攻击时请求大量既不在缓存中,也不在数据库中的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。

解决办法:1,在接口层增加校验,比如用户鉴权,参数做校验;2,采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,用于快速判断出 Key 是否在数据库中存在,一个一定不存在的数据会被这个 bitmap 拦截掉,这个恶意请求就会被提前拦截,从而避免了对DB的查询压力。3,如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。4,在访问数据据时加上互斥锁。

061e2c04e0ebca3425dd75dd035b6b7b

Redis 单进程单线程的模型,因为 Redis 完全是基于内存的操作,CPU 不是 Redis 的瓶颈,Redis 的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且 CPU 不会成为瓶颈,那就顺理成章的采用单线程的方案了。

主从复制
redis和数据库一致性

1,双写:无论是「先更新数据库,再更新缓存」,还是「先更新缓存,再更新数据库」,这两个方案都存在并发问题,当两个请求并发更新同一条数据的时候,因为两个请求执行更新的顺序是不可预测的(DB:0->1->2,缓存:0->2->1),可能会出现缓存和数据库中的数据不一致的现象。数据库和缓存双写,就必然会存在不一致的问题。如果对数据有强一致性要求,不能放缓存。

如果我们的业务对缓存命中率有很高的要求,我们可以采用「更新数据库 + 更新缓存」的方案,因为更新缓存并不会出现缓存未命中的情况。为解决数据不一致:在更新缓存前先加个分布式锁,保证同一时间只运行一个请求更新缓存,就会不会产生并发问题了,当然引入了锁后,对于写入的性能就会带来影响;在更新完缓存时,给缓存加上较短的过期时间,这样即时出现缓存不一致的情况,缓存的数据也会很快过期,对业务还是能接受的。

2,删除更新:在更新数据时,只更新数据库,不更新缓存,而是删除缓存中的数据。读取数据时,发现缓存中没了数据之后,再从数据库中读取数据,更新到缓存中。

如果一致性不是太高可以采取正确更新策略,先更新数据库,再删缓存,并且给缓存数据加上了「过期时间」,就算在这期间存在缓存数据不一致,有过期时间来兜底,这样也能达到最终一致。

工作模式
分布式锁

Redis因为单进程、性能高的特点,它还经常被用于做分布式锁,用来控制分布式系统之间同步访问共享资源。

1,SETEX key seconds value如果不存在就设定键值对,并添加过期时间,两步动作是原子性的,会在同一时间完成,防止加锁成功后设置有效时间失败导致其它线程永远获取不到锁,最后返回1,如果已经存在直接返回0。获得锁就尝试设置某个key,成功后当过期时间到或者手动删除键值对表示释放锁,如果设置失败,表示有其它地方占据该锁需要等待对方释放。

2,缺陷

3,RedLock算法:Redis必须是多节点部署的,可以有效防止单点故障。

流程:

缺点: