前言
本文主要是对Redis使用过程中遇到的问题进行总结。
![ca198aa6956979d0e3ea1305751869fd.png](https://img-blog.csdnimg.cn/img_convert/ca198aa6956979d0e3ea1305751869fd.png)
Redis是使用 C 语言写成的 一个开源的,基于内存的结构化数据存储媒介,可以作为数据库、缓存服务或消息服务使用。Redis支持多种数据结构,包括字符串、哈希表、链表、集合、有序集合等。Redis具备LRU淘汰、事务实现、以及不同级别的硬盘持久化等能力,并且支持副本集和通过Redis Sentinel实现的高可用方案,同时还支持通过Redis Cluster实现的数据自动分片能力。
特点
Redis的主要功能都基于单线程模型实现,也就是说Redis使用一个线程来服务所有的客户端请求,同时Redis采用了非阻塞式IO,并精细地优化各种命令的算法时间复杂度,这些信息意味着:
- Redis是线程安全的(因为只有一个线程),其所有操作都是原子的,不会因并发产生数据异常。
- Redis的速度非常快(因为使用非阻塞式IO,且大部分命令的算法时间复杂度都是O(1))。
- 使用高耗时的Redis命令是很危险的,会占用唯一的一个线程的大量处理时间,导致所有的请求都被拖慢 。
过期策略
采用的 定期删除+惰性删除
定期删除: redis默认每个100ms检查,是否有过期的key,有过期key则删除。需要说明的是,redis不是每个100ms将所有的key检查一次,而是随机抽取进行检查。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。
惰性删除: 也就是说在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除。
采用定期删除+惰性删除就完美了吗? 考虑一种情况,如果定期删除没删除key。然后你也没即时去请求key,也就是说惰性删除也没生效。这样,redis的内存会越来越高。那么就应该采用内存淘汰机制。
淘汰策略
在32位OS中,Redis最大使用3GB的内存,在64位OS中则没有限制。
- volatile-lru:从已设置过期时间的数据集(淘汰上次使用时间最早的,且使用次数最少的key )中挑选最近最少使用的数据淘汰
- volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰
- volatile-random:从已设置过期时间的数据集中任意选择数据淘汰
- allkeys-lru:从数据集中挑选最近最少使用的数据淘汰
- allkeys-random:从数据集中任意选择数据淘汰
- no-enviction(驱逐):不进行数据淘汰
持久化
Redis提供了将数据定期自动持久化至硬盘的能力,包括RDB和AOF两种方案,两种方案分别有其长处和短板,可以配合起来同时运行,确保数据的稳定性。
RDB
采用RDB持久方式,Redis会定期保存数据快照至一个rbd文件中,并在启动时自动加载rdb文件,恢复之前保存的数据。
优点:
- 对性能影响最小。如前文所述,Redis在保存RDB快照时会fork出子进程进行,几乎不影响Redis处理客户端请求的效率。
- 每次快照会生成一个完整的数据快照文件,所以可以辅以其他手段保存多个时间点的快照(例如把每天0点的快照备份至其他存储媒介中),作为非常可靠的灾难恢复手段。
- 使用RDB文件进行数据恢复比使用AOF要快很多。
缺点:
- 快照是定期生成的,所以在Redis crash时或多或少会丢失一部分数据。
- 如果数据集非常大且CPU不够强(比如单核CPU),Redis在fork子进程时可能会消耗相对较长的时间(长至1秒),影响这期间的客户端请求。
AOF
采用AOF持久方式时,Redis会把每一个写请求都记录在一个日志文件里。在Redis重启时,会把AOF文件中记录的所有写操作顺序执行一遍,确保数据恢复到最新。
AOF提供了三种fsync配置,always/everysec/no,通过配置项[appendfsync]指定:
- appendfsync no:不进行fsync,将flush文件的时机交给OS决定,速度最快。
- appendfsync always:每写入一条日志就进行一次fsync操作,数据安全性最高,但速度最慢。
- appendfsync everysec:折中的做法,交由后台线程每秒fsync一次。
优点:
- 最安全,在启用appendfsync always时,任何已写入的数据都不会丢失,使用在启用appendfsync everysec也至多只会丢失1秒的数据。
- AOF文件在发生断电等问题时也不会损坏,即使出现了某条日志只写入了一半的情况,也可以使用redis-check-aof工具轻松修复。
- AOF文件易读,可修改,在进行了某些错误的数据清除操作后,只要AOF文件没有rewrite,就可以把AOF文件备份出来,把错误的命令删除,然后恢复数据。
缺点:
- AOF文件通常比RDB文件更大。
- 性能消耗比RDB高。
- 数据恢复速度比RDB慢。
数据类型
- String 常规的set/get操作,value可以是String也可以是数字
- hash value存放的是结构化的对象
- list 使用List的数据结构
- set 类似Java set集合,存放的是不重复值的集合
- sorted set 权重参数score,集合中的元素能够按score进行排列。
事务
通过MULTI和EXEC命令
Redis在接收到MULTI命令后便会开启一个事务,这之后的所有读写命令都会保存在队列中但并不执行,直到接收到EXEC命令后,Redis会把队列中的所有命令连续顺序执行,并以数组形式返回每个命令的返回结果。
注意:Redis事务不支持回滚
CAS
Redis提供了WATCH命令与事务搭配使用,实现CAS乐观锁的机制。
WATCH的机制:在事务EXEC命令执行时,Redis会检查被WATCH的key,只有被WATCH的key从WATCH起始时至今没有发生过变更,EXEC才会被执行。如果WATCH的key在WATCH命令到EXEC命令之间发生过变化,则EXEC命令会返回失败。
Script
通过EVAL与EVALSHA命令,可以让Redis执行LUA脚本。 Redis官方推荐使用LUA Script来代替事务 ,请参考官方文档 。
Redis为什么这么快
- 纯内存操作
- 单线程操作,避免了频繁的上下文切换
- 采用了非阻塞I/O多路复用机制 ,只有1个线程,通过跟踪每个I/O流的状态来管理多个I/O流 。
实战问题
数据库双写一致性问题
一致性问题是分布式常见问题,还可以再分为最终一致性和强一致性。数据库和缓存双写,就必然会存在不一致的问题。如果对数据有强一致性要求,不能放缓存。我们所做的一切,只能保证最终一致性。另外,我们所做的方案其实从根本上来说是降低不一致发生的概率,无法完全避免。因此,有强一致性要求的数据,不能放缓存。
采取正确更新策略,先更新数据库,再删缓存。其次,因为可能存在删除缓存失败的问题,提供一个补偿措施即可,例如利用消息队列。
缓存穿透
去请求缓存中不存在的数据,导致所有的请求压力都到数据库上,从而数据库连接异常或者崩溃。
- 利用互斥锁,缓存失效的时候,先去获得锁,得到锁了,再去请求数据库。没得到锁,则休眠一段时间重试
- 采用异步更新策略,无论key是否取到值,都直接返回。value值中维护一个缓存失效时间,缓存如果过期,异步起一个线程去读数据库,更新缓存。需要做缓存预热(项目启动前,先加载缓存)操作。
- 提供一个能迅速判断请求是否有效的拦截机制,比如,利用布隆过滤器,内部维护一系列合法有效的key。迅速判断出,请求所携带的Key是否合法有效。如果不合法,则直接返回。
缓存雪崩
缓存同一时间大面积的失效,恰好这时候又来了一波请求,请求压力都到数据库上,从而导致数据库连接异常。
- 给缓存的失效时间,加上一个随机值,避免集体失效。
- 使用互斥锁,但是该方案可能会导致吞吐量明显下降了。
- 采用双缓存,缓存A和缓存B。缓存A的失效时间为20分钟,缓存B不设失效时间 。
并发竞争
Redis为单进程单线程模式,采用队列模式将并发访问变为串行访问。
Redis本身没有锁的概念,Redis对于多个客户端连接并不存在竞争,但是在Jedis客户端对Redis进行并发访问时会发生连接超时、数据转换错误、阻塞、客户端关闭连接等问题,这些问题均是由于客户端连接混乱造成。
解决方法:
- 客户端角度,为保证每个客户端间正常有序与Redis进行通信,对连接进行池化,同时对客户端读写Redis操作采用内部锁synchronized。
- 服务器角度,利用setnx实现锁。
当同时有多个子系统去set一个key。这时候可能会发生问题。
- redis事务机制, 如果是redis集群环境 ,这多个key不一定都存储在同一个redis-server上。该方案会失效。
- 分布式锁,利用setnx实现锁
- 利用队列,将set方法变成串行访问。
大量数据插入
客户端执行大量数据插入时候,一个个的插入会有大量的时间浪费在每一个命令往返时间上。
可以使用Redis提供的pipelining功能来实现在一次交互中执行多条命令。
常用应用场景
- 排行榜类的应用,取TOP N操作,前面操作以时间为权重,这个是以某个条件为权重,比如按顶的次数排序计数器应用
- 存储关系:比如社交关系,比如Tag等
- 获取某段时间所有数据排重值,使用set,比如某段时间访问的用户ID,或者是客户端IP
- 构建队列系统,List可以构建栈和队列,使用zset可以构建优先级队列
Redis与Memcached
比较:
![466f793bf688b20e66364716adbff1d4.png](https://img-blog.csdnimg.cn/img_convert/466f793bf688b20e66364716adbff1d4.png)