本文是对redis的简单介绍,底层逻辑和深层设计思想不在本文,目的是让读者能够对redis又一个基本且全方位的认识。
redis基础
为什么说redis能够快速执行
i. 绝大部分请求是纯粹的内存操作(非常快速)
ii. 采用单线程,避免了不必要的上下文切换和竞争条件
iii. 非阻塞IO - IO多路复用
Redis中的五种数据结构
- string (字符串)
redis是使用C语言开发,但C中并没有字符串类型,只能使用指针或符数组的形式表示一个字符串,所以redis设计了一种简单动态字符串(SDS[Simple Dynamic String])作为底实现:
定义SDS对象,此对象中包含三个属性:
- len buf中已经占有的长度(表示此字符串的实际长度)所以取字符串的长度的时间复杂度为O(1)
- free buf中未使用的缓冲区长度
- buf[] 实际保存字符串数据的地方
空间分配原则:当len小于IMB(1024*1024)时增加字符串分配空间大小为原来的2倍,当len大于等于1M时每次分配 额外多分配1M的空间。
由此可以得出以下特性:
- 二进制安全的
- 高效的计算字符串长度(时间复杂度为O(1))
- 高效的追加字符串操作
- list (列表)
在3.2版本之前,列表是使用ziplist和linkedlist实现的。
列表对象使用ziplist编码有如下条件:
- 列表对象保存的所有字符串元素的长度都小于64字节
- 列表对象保存的元素数量小于512个
- 当有任一条件 不满足时将会进行一次转码,使用linkedlist。
而在3.2版本之后,重新引入了一个quicklist的数据结构,列表的底层都是由quicklist实现的,它结合了ziplist和linkedlist的优点。按照原文的解释这种数据结构是【A doubly linked list of ziplists】意思就是一个由ziplist组成的双向链表
ziplist的结构
由表头和N个entry节点和压缩列表尾部标识符zlend组成的一个连续的内存块。
- 主要用于存储整数和比较短的字符串
- 插入/删除元素时需要对内存进行调整,还要进行部分数据的移动操作
- 更新效率低下
linkedlist的结构
双向链表,插入和删除效率很高,查询的效率却是O(n)[n为元素的个数]。
quicklist结构
它整体宏观上就是一个链表结构,只不过每个节点都是以压缩列表ziplist的结构保存着数据,而每个ziplist又可以包含多个entry。也可以说一个quicklist节点保存的是一片数据,而不是一个数据。
- set (集合)
通过散列表(hashtable)来保证自已存储的每个字符串都是各不相同的值(这些散列表只有键,但没有与键相关联的值)集合是无序的
intset用于存储整数的有序集合,里面存放同一类型的整数。共有三种整数:int16_t、int32_t、int64_t。查找的时间复杂度为O(logN),但是插入的时候,有可能会涉及到升级(比如:原来是int16_t的集合,当插入int32_t的整数的时候就会为每个元素升级为int32_t)这时候会对内存重新分配,所以此时的时间复杂度就是O(N)级别的了。注意:intset只支持升级不支持降级操作。
- hash (哈希)
hash底层的数据结构实现有两种:
ziplist实现
当存储的数据超过配置的阀值时就是转用hashtable的结构。这种转换比较消耗性能,所以应该尽量避免这种转换操作。同时满足以下两个条件时才会使用这种结构:
当键的个数小于hash-max-ziplist-entries(默认512)
当所有值都小于hash-max-ziplist-value(默认64)
hashtable实现
这种结构的时间复杂度为O(1),但是会消耗比较多的内存空间。
- zset (有序集合)
- 有序集合和散列一样,都用于存储键值对
- 有序集合的键被称为成员(member),每个成员都是各不相同的
- 有序集合的值则被称为分值(score),分值必须为浮点数
- 有序集合是redis里面唯一一个既可以根据成员访问元素(这一点和散列一样),又可以根据分值以及分值的排列顺序访问元素的结构
它的存储方式也有两种:
- ziplist结构与上面的hash中的ziplist类似,member和score顺序存放并按score的顺序排列
- skiplist与dict的结合,skiplist用来保障有序性和访问查找性能,dict就用来存储元素信息,并且dict的访问时间复杂度为O(1)
Redis的持久化
- RDB持久化
RDB持久化即通过创建快照(压缩的二进制文件)的方式进行持久化,保存某个时间点的全量数据。RDB持久化是Redis默认的持久化方式。RDB持久化的触发包括手动触发与自动触发两种方式。
- AOF持久化
AOF(Append-Only-File)持久化即记录所有变更数据库状态的指令,以append的形式追加保存到AOF文件中。在服务器下次启动时,就可以通过载入和执行AOF文件中保存的命令,来还原服务器关闭前的数据库状态。
- RDB、AOF混合持久化
Redis从4.0版开始支持RDB与AOF的混合持久化方案。
首先由RDB定期完成内存快照的备份,然后再由AOF完成两次RDB之间的数据备份,由这两部分共同构成持久化文件。
该方案的优点是充分利用了RDB加载快、备份文件小及AOF尽可能不丢数据的特性。
缺点是兼容性差,一旦开启了混合持久化,在4.0之前的版本都不识别该持久化文件,同时由于前部分是RDB格式,阅读性较低。
- Redis 持久化方案的建议
如果Redis只是用来做缓存服务器,比如数据库查询数据后缓存,那可以不用考虑持久化,因为缓存服务失效还能再从数据库获取恢复。
如果你要想提供很高的数据保障性,那么建议你同时使用两种持久化方式。如果你可以接受灾难带来的几分钟的数据丢失,那么可以仅使用RDB。
通常的设计思路是利用主从复制机制来弥补持久化时性能上的影响。即Master上RDB、AOF都不做,保证Master的读写性能,而Slave上则同时开启RDB和AOF(或4.0以上版本的混合持久化方式)来进行持久化,保证数据的安全性。
- Redis 持久化方案的优缺点
RDB持久化
优点:RDB文件紧凑,体积小,网络传输快,适合全量复制;恢复速度比AOF快很多。当然,与AOF相比,RDB最重要的优点之一是对性能的影响相对较小。
缺点:做不到实时持久化,RDB文件需要满足特定格式,兼容性差(如老版本的Redis不兼容新版本的RDB文件)。
AOF持久化
与RDB持久化相对应,AOF的优点在于支持秒级持久化、兼容性好,缺点是文件大、恢复速度慢、对性能影响大。
缓存穿透、缓存击穿、缓存雪崩解决方案
- 缓存穿透
指查询一个一定不存在的数据,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到 DB 去查询,可能导致 DB 挂掉。
解决方案:
i. 查询返回的数据为空,仍把这个空结果进行缓存,但过期时间会比较短
ii. 布隆过滤器:将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对 DB 的查询。
- 缓存击穿
对于设置了过期时间的 key,缓存在某个时间点过期的时候,恰好这时间点对这个 Key 有大量的并发请求过来,这些请求发现缓存过期一般都会从后端 DB 加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把 DB 压垮。
解决方案:
i. 使用互斥锁:当缓存失效时,不立即去 load db,先使用如 Redis 的 setnx 去设置一个互斥锁,当操作成功返回时再进行 load db 的操作并回设缓存,否则重试 get 缓存的方法。
ii 永远不过期:物理不过期,但逻辑过期(后台异步线程去刷新)。
- 缓存雪崩
设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到 DB, DB 瞬时压力过重雪崩。与缓存击穿的区别:雪崩是很多 key,击穿是某一个key 缓存。
解决方案:
将缓存失效时间分散开,比如可以在原有的失效时间基础上增加一个随机值,比如 1-5 分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
Redis 的集群模式
- 主从复制
当从数据库启动时,会向主数据库发送sync命令,主数据库接收到sync后开始在后台保存快照rdb,在保存快照期间收到的命令缓存起来,当快照完成时,主数据库会将快照和缓存的命令一块发送给从数据库。
复制初始化结束。 之后,主每收到1个命令就同步发送给从。
当出现断开重连后,2.8之后的版本会将断线期间的命令传给从数据库。
- 哨兵模式
哨兵的作用:
1、监控redis主、从数据库是否正常运行
2、主出现故障自动将从数据库转换为主数据库。
哨兵的核心知识
1、哨兵至少需要 3 个实例,来保证自己的健壮性。
2、哨兵 + redis 主从的部署架构,是不保证数据零丢失的,只能保证 redis 集群的高可用性。
3、对于哨兵 + redis 主从这种复杂的部署架构,尽量在测试环境和生产环境,都进行充足的测试和演练。
4、配置哨兵监控一个系统时,只需要配置其监控主数据库即可,哨兵会自动发现所有复制该主数据库的从数据库。
Redis分布式锁
先拿setnx来争抢锁,抢到之后,再用expire给锁加一个过期时间防止锁忘记了释放。
如果在setnx之后执行expire之前进程意外crash或者要重启维护了,这个锁就永远得不到释放了,使用set指令把setnx和expire合成一条指令来用
一些问题
- **内存淘汰机制**(当内存不足以容纳新写入数据时)
- noeviction: 新写入操作直接报错
- allkeys-lru:移除最近最少使用的 key
- allkeys-random:随机移除某个 key
- volatile-lru:在设置了过期时间的键空间中,移除最近最少使用的 key
- volatile-random:在设置了过期时间的键空间中,随机移除某个 key
- volatile-ttl:在设置了过期时间的键空间中,有更早过期时间的 key 优先移除
- Redis 和 Mysql 的数据不一致怎么办
对于热点数据我们通常会放入redis中来增加读取效率,但是需要保证从数据库到redis的数据最终一致性。为了保证一致性我们需要考虑两个问题:
- redis数据是采用更新操作还是删除操作更好?
- 操作数据库与操作缓存的顺序应该是怎么样的?
直接说结论:
- redis删除操作更加友好。更新缓存的消耗更大(更新操作交给缓存执行)且更容易造成数据不一致(涉及到复杂业务计算时可能会反复更新缓存)。
- 无论谁前谁后在多线程环境下都有可能在两者间隙中发生意外从而引起数据不一致。虽然间隙无法避免但先操作数据库再操作缓存会更好控制一些。
延时双删
- 第一步:先删除缓存(为了清除老数据)
- 第二步:再写入数据库
- 第三步:休眠xxx毫秒(根据具体的业务时间来定)(避免立即删除缓存导致的其他同步线程将脏数据刷入)
- 第四步:再次删除缓存(为了清除其他线程增加的脏数据)
1 | public void use(String key, Object data){ |
异步更新缓存+重试策略
- 基于mysql binlog进行数据库更新分析
- 异步分析binlog来更新缓存
- 更新缓存失败借助mq实现重试来达到最终一致性
- Redis常见性能问题和解决方案
i. Master最好不要做任何持久化工作,如RDB内存快照和AOF日志文件;(Master写内存快照,save命令调度rdbSave函数,会阻塞主线程的工作;AOF文件过大会影响Master重启的恢复速度)
ii. 如果数据比较重要,某个Slave开启AOF备份数据,策略设置为每秒同步一次
iii. 为了主从复制的速度和连接的稳定性,Master和Slave最好在同一个局域网内
iv. 尽量避免在压力很大的主库上增加从库
v. 主从复制不要用图状结构,用单向链表结构更为稳定,即:Master <- Slave1 <- Slave2 <- Slave3…;这样的结构方便解决单点故障问题,实现Slave对Master的替换。如果Master挂了,可以立刻启用Slave1做Master,其他不变。
- mySQL里有2000w数据,redis中只存20w的数据,如何保证redis中的数据都是热点数据
1、如果数据呈现幂律分布,也就是一部分数据访问频率高,一部分数据访问频率低,则使用allkeys-lru
2、如果数据呈现平等分布,也就是所有的数据访问频率都相同,则使用allkeys-random
- Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如何将它们全部找出来?
使用keys指令可以扫出指定模式的key列表
对方接着追问:如果这个redis正在给线上的业务提供服务,那使用keys指令会有什么问题?这个时候你要回答redis关键的一个特性:redis的单线程的。keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长。
- 项目中有没有用Redis事务
采用的是Redis Cluster集群架构,不同的key是有可能分配在不同的Redis节点上的,在这种情况下Redis的事务机制是不生效的。其次,Redis事务不支持回滚操作,所以基本不用!
参考资料
[1]https://blog.csdn.net/qq1515312832/article/details/113880849
版权声明:本文为博主原创文章,欢迎转载,转载请注明作者、原文超链接,感谢各位看官!!!
本文出自:monkeyGeek
座右铭:生于忧患,死于安乐
欢迎志同道合的朋友一起交流、探讨!
