Redis是目前使用最广泛的NoSql基础设施之一,除了使用最广泛的缓存之外,还可以用做分布式锁、队列、简单统计、布隆过滤器、限流等非常广泛的用途,随着redis的进一步发展,可以想象越来越多的特性将会加入redis中。
Redis最大的特点就是单线程工作模式下,仍然保持了高性能高吞吐量,关键就是操作是内存式级别的运算而且没有锁等线程安全机制。但是在处理网络连接部分采用了多路复用的NIO机制来提升处理多个客户端连接的能力。但是,redis的吞吐量对执行时间长的慢查询非常敏感,使用Redis一定要注意监控和及时处理慢查询。
一、五种基础数据结构
- string(字符串):最简单的数据结构,内部是字符数组。注意,redis的字符串是动态字符串,可以动态修改的,采用预分配冗余空间的方式减少内存频繁分配,字符串的扩容规则是:当字符串长度小于1M的时候,扩容是加倍现有空间,长度超过1M时,扩容是多扩1M的空间,字符串的最大长度是512M;
- list(列表):redis的list实质是链表,这意味着list的插入和删除非常快,时间复杂度O(1),但是定位索引很慢,时间复杂度O(N);
- hash(字典):相当于Java中的HashMap,是无序字典,实现上也是“数组+链表”的二维结构,当字典需要扩容时,由于redis的单线程执行,为了避免堵塞服务,采用了渐进式rehash策略,移除了最后一个元素之后,数据结构被删除,内存被回收;
- set(集合):相当于一个特殊字典,字典中的所有value都是Null,其他都类似hash;
- zset(有序列表):redis提供的非常有特色的数据结构,一方面,他是set,保证了内部value的唯一性,另一方面,可以给每个value赋予一个score用于排序,他的内部实现用的是一种称为“跳跃列表”的数据结构。
list,set,hash,zset这四种数据结构是容器型结构,共享下面两条通用规则:1、create if not exists;2、drop if no elements;
二、redis的其他应用场景
- 分布式锁:redis的单线程工作模式使得redis很适合用在分布式锁的场景,有多个达到set if not exists的原子指令都可以达到分布式锁的效果,需要注意的是,为了避免出现死锁获取到锁时必须同时设置过期时间。另外,使用分布式锁解决业务问题时,最好在业务中也处理一下幂等或者排重;
- 位图:位图是种非常节省内存的数据结构,使用一个bit代表boolean值数据结构,redis数组长度是可以自动扩展的,如果突然使用一个很大的offset,很可能导致服务器阻塞,所以要尽量避免这种情况;
- HyperLogLog:Redis提供的一种不精确的去重计数方案,它只使用了12K的存储空间,能提供标准差不超过0.81%的精度(小于100时没有误差),对于大数据量场景对精度要求不是特别高的场景非常有用;
- 布隆过滤器:bloom filter是一种用于大数据场景下判断是否存在的数据结构,有很低的误判率,但不会漏判。使用布隆过滤器有两个特别重要的参数,一个是元素数量,另一个是错误率,一旦元素数量超过预计元素数时,错误率会急剧攀升;想要更低的错误率,就需要使用更大的内存;
- 地理位置:Redis3.2新增了地理位置GEO模块,用来计算地理位置的距离或者按距离排序,实质是使用了一个zset。使用GEO数据结构时,尽可能控制单个key的数据量不超过1MB;
- 限流:Redis4.0开始提供了一个叫Redis-Cell的限流模块,主要使用了漏斗算法,并提供了原子限流指令。
可以看到,Redis提供了一些其他中间件的功能,总体来说,对小厂而言可以使用这些功能替代那些专业的中间件,但是对大厂而言,还是尽量使用专业的中间件,对于Redis还是尽可能简单的使用它。
三、Redis的持久化机制
Redis有两种持久化机制:RDB快照和AOF,RDB是全量备份,是内存数据的二进制序列化,格式紧凑、体积小巧,恢复速度快,但由于持久化RDB是个重量级操作,对Redis服务性能有严重影响,所以不能频繁的进行RDB备份数据;而AOF是连续的增量备份,记录的是修改指令,所以可以通过回放AOF日志来恢复Redis数据,Redis保存AOF日志前先执行指令,AOF日志体积大、不紧凑、执行时间长。为了在redis的数据安全和性能之间取得比较好的平衡,需要选择Redis的持久化频率,持久化的越频繁,丢数据的可能性就越小,但性能就越差。Redis可以通过bgrewriteaof指令对AOF进行瘦身。
Redis的RDB备份的文件小、恢复速度快,但容易丢数据;而AOF文件丢失的数据少,但文件体积大、恢复速度慢,所以从Redis4.0开始,带来了新的持久化选项--混合持久化,将RDB文件的内容和AOF日志文件存在一起,重启的时候先加载RDB内容,然后再重放AOF增量日志,恢复效率大幅度提高。
四、Redis重要的高级玩法
1、pipeline管道
pipeline可以大幅提高Redis存取效率,其本质是客户端提供的对多个指令打包,从而减少网络请求次数,对于服务器的执行,是没有任何区别的。
2、Redis的事务
Redis的事务和数据库的事务不是一回事儿,没有ACID特性,仅仅做到了事务的“隔离性”中的串行化,保证执行的事务中的命令顺序不被其他事务打断或者不被插队。
3、Stream
Stream是一个强大的支持多播的可持久化消息队列,和kafka的特性非常像。Stream由名称、消费组和消费者组成,其中名称和消费组不能自动创建,而消费者在组内有唯一的名称。在一个消费组内部,每条消息只能被消费一次。由于Redis是内存存储,所以Stream的消息不能积累太多,目前通过指定定长参数,超长后对丢弃老消息。
五、Redis集群
主从复制是Redis集群和高可用的基础,而Redis的主从复制是异步同步的,所以Redis集群下,只能做高可用,无法保证数据的一致性,Redis能做到的是最终一致性。另外,Redis支持主从同步和从从同步,同步机制分为快照同步和增量同步两种。快照同步非常耗费资源,首先在主节点上执行一次bgsave,将当前内存数据快照到磁盘文件,然后将快照文件内容全部传送到从节点,之后在从节点先清空数据,再执行一次全量加载,加载完毕后就可以进行增量同步了;增量同步是将对主节点的状态产生修改性影响的指令记录在本地内容的buffer区,然后异步将buffer中的指令同步到从节点,之后一边在从节点执行同步流,一边向主节点反馈自己的同步偏移量,达到主从数据状态一致性的效果。buffer区是有限的定长环形数组,不能存储所有的指令,如果数组满了就会覆盖前面的内容。回看整个过程,在执行快照同步过程中,主节点的buffer也在往前移动,如果快照同步时间过长或者buffer过小,都会导致同步期间的增量指令在buffer中被覆盖,这样会导致快照同步完成后无法进行增量复制,然后再次发起快照同步,很可能陷入快照同步的死循环。
1、Sentinel
Sentinel集群主要解决Redis的高可用问题,但无法保证消息不丢失。Sentinel监控主从节点的健康状况,当主节点挂掉时,自动选择一个最优的从节点切换成主节点。在这个过程中,主节点挂掉后,原先的主从复制也被中断了,客户端和原主节点也断开了,一个从节点被提升为主节点,其他从节点开始重新和新的主节点建立复制关系。
2、Cluster
Cluster是官方的Redis集群化方案,特点是去中心化,每个节点负责集群的一部分数据,Redis Cluster将所有数据划分为16384个槽位,每个节点负责一部分槽位,而槽位信息存储于每个节点中,不需要额外的分布式存储空间来存储槽位信息。Cluster的客户端连接集群时会缓存集群的槽位信息,当客户端要查找某个key时,可以直接定位到目标节点。客户端的缓存槽位信息可能与服务器的槽位信息不一致,当客户端向错误的节点发出指令时,节点发现指令的key所在的槽位不归自己管,就会向客户端发送一个特殊的跳转指令,携带目标操作的节点地址,告诉客户端去连接这个节点,以获得数据。Redis Cluster可以为每个主节点设置若干个从节点,当主节点发送故障时,集群自动将其中的某个从节点提升为主节点。
六、Redis的部分底层机制
1、过期策略
由于是单线程工作,Redis在处理过期问题上非常小心。Redis过期策略依赖两个机制:1、过期key集合;2、定时扫描策略。首先说过期key集合,Redis会将每个设置了过期时间的key放入一个独立字典中,以后会定时遍历这个字典来删除到期的key;还会使用惰性策略来删除过期的key,惰性删除就是就是客户端在访问这个key的时候检查是否过期,如果过期了就立即删除。然后说定时扫描策略,Redis默认每秒钟进行10次扫描,每次扫描不会遍历过期字典中所有的key,而是采用了一种简单的贪心策略,步骤如下:a、从过期字典中随机选出20个key;b、删除这20个key中已经过期的key;c、如果过期key的比例超过1/4,那就重复步骤a。为了避免出现过期扫描循环过度导致线程卡死现象,算法的扫描时间上限是25ms。从以上的分析可以看出,如果某个时间点突然出现大量的key过期,Redis会持续扫描过期字典,直到过期字典的key变得稀疏才会停止,这就会导致线上读写请求出现明显的卡顿现象,业务开发人员一定要注意不要让大批量key同一时间点过期。
从节点不进行过期扫描,主节点过期时会在AOF文件里增加一条del指令,del指令同步到从节点之后执行del删除过期key。
2、Redis的LRU
当Redis内存超出物理内存限制时,内存数据开始和磁盘产生频繁的交换(swap),交换会让Redis性能急剧下降,生产环境上会配置maxmemory参数来限制内存超出期望的大小。当实际内存超过maxmemory时,Redis提供了几种策略让用户自己决定该如何腾出新的空间以继续提供读写服务:a、noeviction:不继续提供写请求(del请求除外),读请求继续进行,这是Redis默认的淘汰策略,这种策略保证不会丢失数据,但是会让线上业务不能持续进行;b、volatile-lru:尝试淘汰设置了过期时间的key,最少使用的key优先淘汰,没有设置过期时间的key不会淘汰,这样保证持久化的数据不会丢失,而且仍然可以提供完整的服务;c、volatile-ttl:跟上面策略几乎一样,不过淘汰策略不是LRU,而是比较key的剩余寿命ttl的值,ttl越小越优先淘汰;d、volatile-random:跟上面策略几乎一样,不过淘汰策略是过期key集合中随机的key;e、allkeys-lru:跟上面几乎一样,不过淘汰的策略是全体key中最少使用的key,这意味着没有设置过期时间的key也会被淘汰;f、allkeys-random:跟上面几乎一样,不过淘汰的策略是从全体key中随机选择key淘汰。
3、懒惰删除
对单线程的Redis来说,删除一个很大的对象时会导致线程卡顿。为了解决这个问题,Redis4.0引入了unlink指令,它能对删除操作进行懒处理,丢给后台线程异步回收内存。unlink删除key之后,key对应的值是马上不可访问的,unlink也不总是异步的,如果集合且集合的值小于64,就马上释放,如果集合的大小很大,这时候将启用异步线程,分段回收集合。
4、redis6的多线程
从redis4开始,redis就有了一些异步线程,上面提到的unlink清理大集合时,就使用了异步线程。redis6开始正式支持多线程,但是多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程顺序执行,所以即使使用了Redis6,也不需要考虑线程安全问题。
5、redis的慢查询
Redis的慢查询统计的是命令执行阶段比较慢的操作,没有包含命令排队的时间,所以没有慢查询不代表没有超时,也有可能排队的命令比较多。另外Redis的命令排队机制,慢查询发生时,也会导致其他命令级联阻塞。如果你想用Redis来优化一个高并发系统,即使Redis的执行时间是1ms也可能对系统的吞吐量造成很大影响。
总之,Redis是个强大的工具,但也有他的脆弱性,使用的时候千万不能想当然,多了解底层原理,能帮我们避免很多坑。
参考资料:
1、redis官网:https://redis.io/
2、Redis深度历险:核心原理与应用实践
3、Redis使用手册
个人公众号:
自己在软件这个行业10来年的工作和学习历程中犯了不少错误,也走了不少弯路,在此公众号分享自己的成长心路和工作心得,希望给后来的从业者一个参照,不要再犯我犯过的错误,不要再走我走过的弯路。在此我也会分享工作中遇到的技术问题和自己研究技术的记录与心得;在项目过程中遇到的风险和暴露于化解风险的过程和方法;在团队管理过程中的心得和体会。后续会发布一系列专题技术文章,程序员成长系列文章,项目的系列文章,行业发展分析和展望的文章,甚至会包含婚恋和育儿的心得文章,我是个不专注却热爱生活的工程师!