Redis:从应用到底层,一文帮你搞定
Hollis
共 11235字,需浏览 23分钟
·
2021-02-09 02:26
1.1、String
适用于简单key-value存储、setnx key value实现分布式锁、计数器(原子性)、分布式全局唯一ID。
SDS
(simple dynamic string)封装char[],这是是Redis存储的最小单元
,一个SDS最大可以存储512M信息。struct sdshdr{
unsigned int len; // 标记char[]的长度
unsigned int free; //标记char[]中未使用的元素个数
char buf[]; // 存放元素的坑
}
RedisObject
,核心有两个作用:
说明是5种类型哪一种。 里面有指针用来指向 SDS。
set name sowhat
的时候,其实Redis会创建两个RedisObject对象,键的RedisObject 和 值的RedisOjbect 其中它们type = REDIS_STRING,而SDS分别存储的就是 name 跟 sowhat 字符串咯。
SDS修改后大小 > 1M时 系统会多分配空间来进行 空间预分配
。SDS是 惰性释放空间
的,你free了空间,可是系统把数据记录下来下次想用时候可直接使用。不用新申请空间。
1.2、List
adlist.h
会发现底层就是个 双端链表,该链表最大长度为2^32-1。常用就这几个组合。lpush + lpop = stack 先进后出的栈 lpush + rpop = queue 先进先出的队列 lpush + ltrim = capped collection 有限集合 lpush + brpop = message queue 消息队列
1.3、Hash
1.3.1、dictEntry
真正的数据节点,包括key、value 和 next 节点。
1.3.2、dictht
1、数据 dictEntry 类型的数组,每个数组的item可能都指向一个链表。 2、数组长度 size。 3、sizemask 等于 size - 1。 4、当前 dictEntry 数组中包含总共多少节点。
1.3.3、dict
1、dictType 类型,包括一些自定义函数,这些函数使得key和value能够存储 2、rehashidx 其实是一个标志量,如果为 -1
说明当前没有扩容,如果不为 -1
则记录扩容位置。3、dictht数组,两个Hash表。 4、iterators 记录了当前字典正在进行中的迭代器
1.3.4、渐进式扩容
rehashindex
来记录转移的情况,当全部转移完成,将ht[1]改成ht[0]使用。rehashidx
,其为第一个数组正在移动的下标位置,如果当前内存不够,或者操作系统繁忙,扩容的过程可以随时停止。1、如果是新增,则直接新增后第二个数组,因为如果新增到第一个数组,以后还是要移过来,没必要浪费时间 2、如果是删除,更新,查询,则先查找第一个数组,如果没找到,则再查询第二个数组。
1.4、Set
t.set.c
就可以了解本质了。int setTypeAdd(robj *subject, robj *value) {
long long llval;
if (subject->encoding == REDIS_ENCODING_HT) {
// 看到底层调用的还是dictAdd,只不过第三个参数= NULL
if (dictAdd(subject->ptr,value,NULL) == DICT_OK) {
incrRefCount(value);
return 1;
}
....
1.5、ZSet
redis.h
后就会发现 Zset用的就是可以跟二叉树媲美的跳跃表
来实现有序。跳表就是多层链表的结合体,跳表分为许多层(level),每一层都可以看作是数据的索引,这些索引的意义就是加快跳表查找数据速度。从上往下,从左往右
进行查找。现在找出值为37的节点为例,来对比说明跳表和普遍的链表。没有跳表查询 比如我查询数据37,如果没有上面的索引时候路线如下图: 有跳表查询 有跳表查询37的时候路线如下图: 应用场景:
积分排行榜、时间排序新闻、延时队列。
1.6、Redis Geo
1.7、HyperLogLog
概率
数据结构,它使用概率算法来统计集合的近似基数。而它算法的最本源则是伯努利过程 + 分桶 + 调和平均数
。具体实现可看 HyperLogLog 讲解。1.8、bitmap
用户签到
key = 年份:用户id offset = (今天是一年中的第几天) % (今年的天数)
统计活跃用户
使用日期作为 key,然后用户 id 为 offset 设置不同offset为0 1 即可。
1.9、Bloom Filter
不存在的一定不存在,存在的不一定存在
。当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点(有效降低冲突概率),把它们置为1。检索时,我们只要看看这些点是不是都是1就知道集合中有没有它了:如果这些点有任何一个为0,则被检元素一定不在;如果都是1,则被检元素很可能在。这就是布隆过滤器的基本思想。
guava
包玩耍一番。1.10 发布订阅
发布、订阅
模式的消息机制,其中消息订阅者与发布者不直接通信,发布者向指定的频道(channel)发布消息,订阅该频道的每个客户端都可以接收到消息。不过比专业的MQ(RabbitMQ RocketMQ ActiveMQ Kafka)相比不值一提,这个功能就算球了。2.1、RDB
1、压缩后的二进制文,适用于备份、全量复制,用于灾难恢复加载RDB恢复数据远快于AOF方式,适合大规模的数据恢复。 2、如果业务对数据完整性和一致性要求不高,RDB是很好的选择。数据恢复比AOF快。
1、RDB是周期间隔性的快照文件,数据的完整性和一致性不高,因为RDB可能在最后一次备份时宕机了。 2、备份时占用内存,因为Redis 在备份时会独立fork一个子进程,将数据写入到一个临时文件(此时内存中的数据是原来的两倍哦),最后再将临时文件替换之前的备份文件。所以要考虑到大概两倍的数据膨胀性。
1、 SAVE
直接调用 rdbSave ,阻塞
Redis 主进程,导致无法提供服务。2、BGSAVE
则 fork 出一个子进程,子进程负责调用 rdbSave ,在保存完成后向主进程发送信号告知完成。在BGSAVE 执行期间仍可以继续处理客户端的请求。3、Copy On Write 机制,备份的是开始那个时刻内存中的数据,只复制被修改内存页数据,不是全部内存数据。 4、Copy On Write 时如果父子进程大量写操作会导致分页错误。
2.2、AOF
AOF是一秒一次去通过一个后台的线程fsync操作,数据丢失不用怕。
1、对于相同数量的数据集而言,AOF文件通常要大于RDB文件。RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。 2、根据同步策略的不同,AOF在运行效率上往往会慢于RDB。总之,每秒同步策略的效率是比较高的。
aof_buf
然后再同步到AO磁盘,如果实时写入磁盘会带来非常高的磁盘IO,影响整体性能。1、在重写期间,由于主进程依然在响应命令,为了保证最终备份的完整性;它 依然会写入旧
的AOF中,如果重写失败,能够保证数据不丢失。2、为了把重写期间响应的写入信息也写入到新的文件中,因此也会 为子进程保留一个buf
,防止新写的file丢失数据。3、重写是直接把 当前内存的数据生成对应命令
,并不需要读取老的AOF文件进行分析、命令合并。4、无论是 RDB 还是 AOF 都是先写入一个临时文件,然后通过 rename
完成文件的替换工作。
1、降低fork的频率,比如可以手动来触发RDB生成快照、与AOF重写; 2、控制Redis最大使用内存,防止fork耗时过长; 3、配置牛逼点,合理配置Linux的内存分配策略,避免因为物理内存不足导致fork失败。 4、Redis在执行 BGSAVE
和BGREWRITEAOF
命令时,哈希表的负载因子>=5,而未执行这两个命令时>=1。目的是尽量减少写操作,避免不必要的内存写入操作。5、哈希表的扩展因子:哈希表已保存节点数量 / 哈希表大小。因子决定了是否扩展哈希表。
2.3、恢复
2.4、建议
3、Redis为什么那么快
3.1、 基于内存实现:
数据都存储在内存里,相比磁盘IO操作快百倍,操作速率很快。
3.2、高效的数据结构:
Redis底层多种数据结构支持不同的数据类型,比如HyperLogLog它连2个字节都不想浪费。
3.3、丰富而合理的编码:
1、String:自动存储int类型,非int类型用raw编码。 2、List:字符串长度且元素个数小于一定范围使用 ziplist 编码,否则转化为 linkedlist 编码。 3、Hash:hash 对象保存的键值对内的键和值字符串长度小于一定值及键值对。 4、Set:保存元素为整数及元素个数小于一定范围使用 intset 编码,任意条件不满足,则使用 hashtable 编码。 5、Zset:保存的元素个数小于定值且成员长度小于定值使用 ziplist 编码,任意条件不满足,则使用 skiplist 编码。
3.4、合适的线程模型:
I/O 多路复用
模型同时监听客户端连接,多线程是需要上下文切换的,对于内存数据库来说这点很致命。
3.5、 Redis6.0后引入多线程
提速:
>>
Redis运行执行耗时,Redis的瓶颈主要在于网络的 IO 消耗, 优化主要有两个方向:1、提高网络 IO 性能,典型的实现比如使用 DPDK 来替代内核网络栈的方式 2、使用多线程充分利用多核,典型的实现比如 Memcached。
1、可以充分利用服务器 CPU 资源,目前主线程只能利用一个核 2、多线程任务可以分摊 Redis 同步 IO 读写负荷
Redis 6.0 版本 默认多线程是关闭的 io-threads-do-reads no Redis 6.0 版本 开启多线程后 线程数也要 谨慎设置。 多线程可以使得性能翻倍,但是多线程只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程顺序执行。
4、常见问题
4.1、缓存雪崩
Redis中大批量key在同一时间同时失效导致所有请求都打到了MySQL。而MySQL扛不住导致大面积崩塌。
1、缓存数据的过期时间加上个随机值,防止同一时间大量数据过期现象发生。 2、如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中。 3、设置热点数据永远不过期。
4.2、缓存穿透
缓存穿透 是 指缓存和数据库中 都没有
的数据,比如ID默认>0,黑客一直 请求ID= -12的数据那么就会导致数据库压力过大,严重会击垮数据库。
1、后端接口层增加 用户鉴权校验,参数做校验等。 2、单个IP每秒访问次数超过阈值直接拉黑IP,关进小黑屋1天,在获取IP代理池的时候我就被拉黑过。 3、从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null 失效时间可以为15秒防止恶意攻击。 4、用Redis提供的 Bloom Filter 特性也OK。
4.3、缓存击穿
现象:大并发集中对这一个热点key进行访问,当这个Key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库。
设置热点数据永远不过期 加上互斥锁也能搞定了
4.4、双写一致性
缓存
跟数据库
均更新数据,如何保证数据一致性?安全问题:线程A更新数据库->线程B更新数据库->线程B更新缓存->线程A更新缓存。 导致脏读
。业务场景:读少写多场景,频繁更新数据库而缓存根本没用。更何况如果缓存是叠加计算后结果更 浪费性能
。
A 请求写来更新缓存。 B 发现缓存不在去数据查询旧值后写入缓存。 A 将数据写入数据库,此时缓存跟数据库不一致。
失效:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。 命中:应用程序从cache中取数据,取到后返回。 更新: 先把数据存到数据库中,成功后,再让缓存失效
。
4.5、脑裂
Hadoop
、Spark
集群中都会出现这样的情况,只是解决方法不同而已(用ZK配合强制杀死)。min-replicas-to-write 3 表示连接到master的最少slave数量
min-replicas-max-lag 10 表示slave连接到master的最大延迟时间
4.6、事务
1、redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。 2、Redis事务没有隔离级别的概念:批量操作在发送 EXEC 命令前被放入队列缓存,并不会被实际执行,也就不存在事务内的查询要看到事务里的更新,事务外查询不能看到。 3、Redis不保证原子性:Redis中单条命令是原子性执行的,但事务不保证原子性。 4、Redis编译型错误事务中所有代码均不执行,指令使用错误。运行时异常是错误命令导致异常,其他命令可正常执行。 5、watch指令类似于乐观锁,在事务提交时,如果watch监控的多个KEY中任何KEY的值已经被其他客户端更改,则使用EXEC执行事务时,事务队列将不会被执行。
4.7、正确开发步骤
上线前
:Redis 高可用,主从+哨兵,Redis cluster,避免全盘崩溃。上线时
:本地 ehcache 缓存 + Hystrix 限流 + 降级,避免MySQL扛不住。上线后
:Redis 持久化采用 RDB + AOF 来保证断点后自动从磁盘上加载数据,快速恢复缓存数据。
5、分布式锁
5.1、 Zookeeper实现分布式锁
zookeeper
知识:1、持久节点:客户端断开连接zk不删除persistent类型节点 2、临时节点:客户端断开连接zk删除ephemeral类型节点 3、顺序节点:节点后面会自动生成类似0000001的数字表示顺序 4、节点变化的通知:客户端注册了监听节点变化的时候,会调用回调方法
只
监控它前面那个节点状态,从而避免羊群效应
。关于模板代码百度即可。频繁的创建删除节点,加上注册watch事件,对于zookeeper集群的压力比较大,性能也比不上Redis实现的分布式锁。
5.2、 Redis实现分布式锁
SETNX 是SET if Not eXists的简写,日常指令是 SETNX key value
,如果 key 不存在则set成功返回 1,如果这个key已经存在了返回0。
SETEX key seconds value 表达的意思是 将值 value 关联到 key ,并将 key 的生存时间设为多少秒。如果 key 已经存在,setex命令将覆写旧值。并且 setex是一个 原子性
(atomic)操作。
一般就是用一个标识唯一性的字符串比如UUID 配合 SETNX 实现加锁。
这里用到了LUA脚本,LUA可以保证是原子性的,思路就是判断一下Key和入参是否相等,是的话就删除,返回成功1,0就是失败。
这个锁是无法重入的,且自己实心的话各种边边角角都要考虑到,所以了解个大致思路流程即可,工程化还是用开源工具包就行。
5.3、 Redisson实现分布式锁
6、Redis 过期策略和内存淘汰策略
6.1、Redis的过期策略
每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即对key进行清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。 expires字典会保存所有设置了过期时间的key的过期时间数据,其中 key 是指向键空间中的某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。
惰性删除
+ 定期删除
。memcached采用的过期策略:惰性删除
。6.2、6种内存淘汰策略
1、volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰 2、volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰 3、volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰 4、allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰 5、allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰 6、no-enviction(驱逐):禁止驱逐数据,不删除的意思。
LinkedHashMap
中也实现了LRU
算法的,实现如下:class SelfLRUCache<K, V> extends LinkedHashMap<K, V> {
private final int CACHE_SIZE;
/**
* 传递进来最多能缓存多少数据
* @param cacheSize 缓存大小
*/
public SelfLRUCache(int cacheSize) {
// true 表示让 linkedHashMap 按照访问顺序来进行排序,最近访问的放在头部,最老访问的放在尾部。
super((int) Math.ceil(cacheSize / 0.75) + 1, 0.75f, true);
CACHE_SIZE = cacheSize;
}
@Override
protected boolean removeEldestEntry(Map.Entryeldest) {
// 当 map中的数据量大于指定的缓存个数的时候,就自动删除最老的数据。
return size() > CACHE_SIZE;
}
}
6.2、总结
7、Redis 集群高可用
redis主从复制
、Sentinel哨兵模式
、Redis Cluster
。7.1、redis主从复制
增量同步
跟 全量同步
两种机制。7.1.1、全量同步
1、slave连接master,发送 psync
命令。2、master接收到 psync
命名后,开始执行bgsave命令生成RDB文件并使用缓冲区记录此后执行的所有写命令。3、master发送快照文件到slave,并在发送期间继续记录被执行的写命令。4、slave收到快照文件后丢弃所有旧数据,载入收到的快照。 5、master快照发送完毕后开始向slave发送缓冲区中的写命令。 6、slave完成对快照的载入,开始接收命令请求,并执行来自master缓冲区的写命令。
7.1.2、增量同步
7.1.3、Redis主从同步策略:
1、 主从刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步
。当然,如果有需要,slave 在任何时候都可以发起全量同步。redis 策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步。2、slave在同步master数据时候如果slave丢失连接不用怕,slave在重新连接之后丢失重补
。3、一般通过主从来实现读写分离,但是如果master挂掉后如何保证Redis的 HA呢?引入 Sentinel
进行master的选择。
7.2、高可用之哨兵模式
Redis-sentinel 本身是一个独立运行的进程,一般sentinel集群 节点数至少三个且奇数个,它能监控多个master-slave集群,sentinel节点发现master宕机后能进行自动切换。Sentinel可以监视任意多个主服务器以及主服务器属下的从服务器,并在被监视的主服务器下线时,自动执行故障转移操作。这里需注意
sentinel
也有single-point-of-failure
问题。大致罗列下哨兵用途:集群监控:循环监控master跟slave节点。 消息通知:当它发现有redis实例有故障的话,就会发送消息给管理员 故障转移:这里分为主观下线(单独一个哨兵发现master故障了)。客观下线(多个哨兵进行抉择发现达到quorum数时候开始进行切换)。 配置中心:如果发生了故障转移,它会通知将master的新地址写在配置中心告诉客户端。
7.3、Redis Cluster
7.3.1、分区规则
节点取余
:hash(key) % N一致性哈希
:一致性哈希环虚拟槽哈希
:CRC16[key] & 16383
虚拟槽分区
方式,具题的实现细节如下:1、采用去中心化的思想,它使用虚拟槽solt分区覆盖到所有节点上,取数据一样的流程,节点之间使用轻量协议通信Gossip来减少带宽占用所以性能很高, 2、自动实现负载均衡与高可用,自动实现failover并且支持动态扩展,官方已经玩到可以1000个节点 实现的复杂度低。 3、每个Master也需要配置主从,并且内部也是采用哨兵模式,如果有半数节点发现某个异常节点会共同决定更改异常节点的状态。 4、如果集群中的master没有slave节点,则master挂掉后整个集群就会进入fail状态,因为集群的slot映射不完整。如果集群超过半数以上的master挂掉,集群都会进入fail状态。 5、官方推荐 集群部署至少要3台以上的master节点。
8、Redis 限流
缓存
、降级
和限流
。那么何为限流呢?顾名思义,限流就是限制流量,就像你宽带包了1个G的流量,用完了就没了。通过限流,我们可以很好地控制系统的qps,从而达到保护系统的目的。1、基于Redis的setnx、zset
1.2、setnx
1.3、zset
2、漏桶算法
3、令牌桶算法
1、所有的请求在处理之前都需要拿到一个可用的令牌才会被处理。 2、根据限流大小,设置按照一定的速率往桶里添加令牌。 3、设置桶最大可容纳值,当桶满时新添加的令牌就被丢弃或者拒绝。 4、请求达到后首先要获取令牌桶中的令牌,拿着令牌才可以进行其他的业务逻辑,处理完业务逻辑之后,将令牌直接删除。 5、令牌桶有最低限额,当桶中的令牌达到最低限额的时候,请求处理完之后将不会删除令牌,以此保证足够的限流。
1、自定义注解、aop、Redis + Lua 实现限流。 2、推荐 guava 的RateLimiter实现。
9、常见知识点
字符串模糊查询时用 Keys
可能导致线程阻塞,尽量用scan
指令进行无阻塞的取出数据然后去重下即可。多个操作的情况下记得用 pipeLine
把所有的命令一次发过去,避免频繁的发送、接收带来的网络开销,提升性能。bigkeys可以扫描redis中的大key,底层是使用scan命令去遍历所有的键,对每个键根据其类型执行STRLEN、LLEN、SCARD、HLEN、ZCARD这些命令获取其长度或者元素个数。缺陷是线上试用并且个数多不一定空间大, 线上应用记得开启Redis慢查询日志哦,基本思路跟MySQL类似。 Redis中因为内存分配策略跟增删数据是会导致 内存碎片
,你可以重启服务也可以执行activedefrag yes
进行内存重新整理来解决此问题。
1、Ratio >1 表明有内存碎片,越大表明越多严重。 2、Ratio < 1 表明正在使用虚拟内存,虚拟内存其实就是硬盘,性能比内存低得多,这是应该增强机器的内存以提高性能。 3、一般来说,mem_fragmentation_ratio的数值在1 ~ 1.5之间是比较健康的。
有道无术,术可成;有术无道,止于术
欢迎大家关注Java之道公众号
好文章,我在看❤️
评论