1、Redis的特性:
1)快:
- 基于内存,操作不需要与磁盘交互
- 命令执行单线程
- 多路复用
- k-v 底层也有很多数据结构支持 比如跳跃表(空间换时间)
2)高可用
- 持久化
- 主从、集群
3)支持很多种数据类型
4)支持多种语言客户端
2、Redis的五种数据结构:
1)string(底层结构:sds):
适用场景:
1.session过期机制、缓存
2.分布式锁:Redis单线程
3.分布式ID:incr
2)hash(底层结构:哈希表):
适用场景:
1、重入锁
2、购物车
3)list(底层结构:linkedlist或者ziplist(3.0以前);quicklist(3.2及以后)):
linkedlist:
ziplist:
适用场景:
1、消息队列(有丢失数据的风险)
2、浏览记录
4)set:
补充:如果元素都是整数,底层使用的是整数集合
使用场景:
1、抽奖
使用场景:
1、定时任务
2、排行榜
3、Redis面试题
1、有一个100M的数据,可以放到redis中吗?
可以放,但是不建议。数据量太大,查询会有io开销,因为Redis查询时单线程的,会阻塞到其它查询命令。
2、Redis的过期策略
主动删除:定时删除(内存友好、CPU不友好)、定期删除(折中方案)
主动删除:被动过期
1)被动过期(即拿的时候发现过期):对内存不友好,对CPU友好
2)定期删除:
1、只需要找到设置了过期时间的key
2、也不是一次性全部拿到设置过期时间的,根据hash桶的维度,拿到20(可配)个值为止:
假如第一个hash桶是30,拿30
假如第一个hash桶是18,第二个hash桶是100,拿118
3、删掉拿到的值里面过期的数据,如果删除比例超过10%,继续执行2,3步
4、最多只会拿16次
补充:多久执行一次?
1秒执行10次,也就是100ms执行一次
3、Redis的淘汰策略(正常的数据进行清理,内存满了)
1)LRU(Least Recently Used):最近最少使用,操作时间离当前越远的约容易淘汰
1.volatile(设置了过期时间的)
2.allkeys(所有key)
1、键值对lru时钟值的初始化:
Redis结构都是key - value,其中value结构:
typedefstructredisObject { unsignedtype:4; // 类型:string、list。。。unsignedencoding:4; // 底层结构:quicklist、ziplist。。。unsignedlru:LRU_BITS; /* LRU time (relative to global lru_clock) or* LFU data (least significant 8 bits frequency* and most significant 16 bits access time). */// lru:代表这个redisObject被操作的// 时间(秒单位)的最后24bitintrefcount; void*ptr; // 指向真正数据地址的指针} robj;
创建RedisObject时,会给这个对象加上lru属性
参数lru解析:
maxmemory-policy:
为lru时:
被操作的时间(秒单位)的最后24bit
lfu:
前16 bits access time:被操作的时间(分单位)的最后16bit
后8 bits frequency:操作次数的8bit
当后续这个对象被访问时,lru属性就会被更新
2、lru算法何时执行?
每次redis-service执行操作命令时:不超时并且没有载入数据时执行
3、lru的具体执行:
1)判断当前内存使用情况
配置文件设置了maxmemory,判断内存使用是否大于maxmemory,当大于时会while循环进行淘汰策略
2)更新待淘汰的候选键值对集合
从哈希表中随机取出一定数量(默认是5个)的key,注意如果maxmemory_policy为allkeys_lru则取全局key,否则取设置过期时间的key
3)选择被淘汰的键值对并删除:找到符合淘汰的key进行删除,每次删除数组中最久未被操作过的key
/* Max value of obj->lru */unsignedlonglongestimateObjectIdleTime(robj*o) { unsignedlonglonglruclock=LRU_CLOCK(); // 当前时间的最后24位bit/*如果当前时间大于等于redisObject.lru,返回当前时间减去lru的时间差值如果小于,返回当前时间加上24bit减去lru的时间差值*/if (lruclock>=o->lru) { return (lruclock-o->lru) *LRU_CLOCK_RESOLUTION; } else { return (lruclock+ (LRU_CLOCK_MAX-o->lru)) *LRU_CLOCK_RESOLUTION; } }
总结:每个RedisObjective-C(redis对象)会存储lru(保存最近操作的时间),redis进行淘汰时,会每次取5个,将操作时间越久的淘汰了。
2)LFU(least frequently used):最不频繁使用
1.volatile(设置了过期时间的)
2.allkeys(所有key)
1、键值对的初始化:lru变量存储:
前16 bits access time:被操作的时间(分单位)的最后16bit
后8 bits frequency:操作次数的8bit
2、key更新操作
第一步,根据距离上次访问的时长,衰减访问次数。
/* Update LFU when an object is accessed.* Firstly, decrement the counter if the decrement time is reached.* Then logarithmically increment the counter, and update the access time. */voidupdateLFU(robj*val) { // 首先,递减计数器unsignedlongcounter=LFUDecrAndReturn(val); // 然后以logN级别递增计数器,并更新访问次数。counter=LFULogIncr(counter); val->lru= (LFUGetTimeInMinutes()<<8) |counter; }
第二步,根据当前访问更新访问次数。
unsignedlongLFUTimeElapsed(unsignedlongldt) { unsignedlongnow=LFUGetTimeInMinutes(); if (now>=ldt) returnnow-ldt; return65535-ldt+now; } // 返回多久没有访问过unsignedlongLFUDecrAndReturn(robj*o) { unsignedlongldt=o->lru>>8; unsignedlongcounter=o->lru&255; unsignedlongnum_periods=server.lfu_decay_time?LFUTimeElapsed(ldt) /server.lfu_decay_time : 0; if (num_periods) counter= (num_periods>counter) ?0 : counter-num_periods; returncounter; } // 原本次数约大越不容易被更新
3、lfu淘汰流程:
1)第一步,调用 getMaxmemoryState 函数计算待释放的内存空间;
2)第二步,调用 evictionPoolPopulate 函数随机采样键值对,并插入到待淘汰集合 EvictionPoolLRU 中;
具体来说,在实现 LRU 算法时,待淘汰候选键值对集合 EvictionPoolLRU 中的每个元素,都使用成员变量 idle 来记录它距离上次访问的空闲时间。
而当实现 LFU 算法时,因为 LFU 算法会对访问次数进行衰减和按概率增加,所以,它是使用访问次数来近似表示访问频率的。相应的,LFU 算法其实是用 255 减去键值对的访问次数,这样来计算 EvictionPoolLRU 数组中每个元素的 idle 变量值的。而且,在计算 idle 变量值前,LFU 算法还会调用 LFUDecrAndReturn 函数,衰减一次键值对的访问次数,以便能更加准确地反映实际选择待淘汰数据时,数据的访问频率。
if (server.maxmemory_policy&MAXMEMORY_FLAG_LRU) { idle=estimateObjectIdleTime(o); } elseif (server.maxmemory_policy&MAXMEMORY_FLAG_LFU) { idle=255-LFUDecrAndReturn(o); }
3)第三步,遍历待淘汰集合 EvictionPoolLRU,选择实际被淘汰数据,并删除。
3)random:随机
4)ttl:最接近过期
5)novication(默认的):不能写,能读
4、redis持久化
1)rdb(redis database):默认的持久化方式,快照(将某个时间点的redis信息保存到jump.db二进制文件中),保存数据库键值对
1、触发机制:
1)自动触发
save9001// 900s内至少有1个key被修改、添加save30010save6010000
2)关闭redis时触发
3)flushall
4)手动
- save 阻塞操作
- bgsave 后台非阻塞操作
缺点:可靠性很低,会丢失很多数据
有点:恢复快
2)aof(Append Only File),保存Redis执行的写命令
默认是关闭的,开启之后,默认会用aof去备份,rdb还是会同时存在
redis提供了跟aof文件交互的配置
//交给操作系统同步//每次都会交互// 每秒同步一次,优先保证性能
优点:数据可靠性更强
缺点:性能没有rdb快
3)重写机制
- 4.0版本之前:比较指令
lpushtestalpushtestbcdlpop----------重写机制lpushtestabc
缺点:效率低
- 4.0以后:RDB和AOF混合模式:重写的时候,会将aof文件压缩成rdb(二进制)格式的文件,后面又是命令的形式
- 触发重写机制
createIntConfig( "auto-aof-rewrite-percentage", NULL, MODIFIABLE_CONFIG, 0, INT_MAX, server.aof_rewrite_perc, 100, INTEGER_CONFIG, NULL, NULL) //下一次触发的条件是:aof文件的大小跟上次压缩过之后的大小进行比较,如果超过了一倍了,就会进行重写createOffTConfig( "auto-aof-rewrite-min-size", NULL, MODIFIABLE_CONFIG, 0, LLONG_MAX, server.aof_rewrite_min_size, 64*1024*1024, MEMORY_CONFIG, NULL, NULL)//aof文件必须打到64MB才能重写
5、主从复制
1)主从复制的过程
- 建立连接:主从互相保存对方的IP、端口、偏移量等信息
- 全量同步:从库发送指令到主库,主收到后,通过BGSAVE备份RDB文件发送给从库,从库删除自己的数据,将RDB数据同步过来
- 增量同步:全量数据同步过程中,主的增量数据会先放到缓存中,等从库同步完全量数据后,主库会把缓存和后面的增量命令同步给从库
2)主从数据丢失的场景?
1、自动切换:哨兵、cluster
- 主库每次的命令都会给到从库,假如没给到从库就挂了,这部分数据只在主库有
- 这个时候会重新去从节点找一个从库顶替主库,产生了新的主节点
- 如果挂了的旧主节点重启了,旧主节点就会变成新的从节点
- 数据同步,旧主节点的数据被删除,这部分数据丢失
2、脑裂:哨兵和主节点失去连接,这个时候可能会出现双节点,也会出现数据丢失
6、cluster集群
最低配置:3主3从
分片思想:保证高可用
分片的一般实现:
1、取模,缺点:每次改动要处理所有数据
2、一致性hash:会导致数据不均匀问题
redis的解决方法:使用虚拟槽(0-16383个虚拟槽),解决数据不均匀问题。
虚拟节点跟真实节点有对应关系,例如:
节点1: 0-5460的虚拟槽
节点2: 5461-10922的虚拟槽
节点3: 10923-16383的虚拟槽
操作key时,根据key取模,决定放在哪个节点上
7、redission分布式锁
lua + hash(可重入):看门狗
特点:
1、防止死锁
2、防止业务没有执行完,锁释放。你去设置时间的话,就没有看门狗了,也就是等于-1的时候才有看门狗
8、场景分析
缓存雪崩、缓存穿透、缓存击穿
缓存雪崩:redis大量的热点数据同时过期,所有请求打到DB,解决:过期时间不一致
缓存穿透:一条数据过期,但是过期的时候发生大量的并发,并发请求打到DB,解决:加锁
缓存穿透:redis没有,DB没有,但是前端大量的请求过来,会打到DB。解决:布隆过滤器。布隆过滤器:bitmap+hash 没法判断一定存在,但是能判断一定不存在。存在一定的误判率;减少误判率:1、多次hash;2、扩容值:会带来性能消耗
9、Redis跟DB的数据一致性问题
(1)造成原因:在多并发的场景下,操作不是原子性的。例如:
线程A请求数据,没有缓存,访问DB,更新缓存
线程A没有同步完,线程B将DB改成2,删除缓存
线程A继续同步,缓存的数据是1,数据不一致
(2)解决:延迟双删(并不是最好的办法),最好的办法是别管,业务场景下的话就舍弃一个,Redis和DB只能一个为主