Redis的数据类型
1.五大数据类型:
string :(字符串)可以用来做最简单的数据,可以缓存某个简单的字符串、某个json格式的字符串、Redis分布式锁、计算器、Session共享、分布式ID
hash:(哈希表) 可用来存储一些KV值,存储对象。
list:(列表)Redis的列表通过命令的组合,可以用来当作栈、队列,也可以用来缓存类似微信公众号、微博等消息流数据。
set:(集合)和列表类似,也可存储多个元素,但是不能重复,集合可以进行交集、并集、差集操作,从而可以实现类似,共同好友、朋友圈点赞等功能。
特点 :元素不重复、无重复、无序
zset:(有序集合)元素不重复、无重复、有序,可用来实现排行榜功能。
2.三种特殊数据类型:
Geospatial 地理位置
Hyperloglog 数据统计
Bitmap(位图,数据结构) 统计用户信息,只要是两种状态的业务都可用Bitmap处理。比如签到之类的业务。
使用场景
Redis的数据持久化策略有哪些?
RDB
RDB持久化: 在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储
fork采用的是copy-on-write技术:
- 当主进程执行读操作时,访问共享内存
- 当主进程执行写操作时,则会先拷贝一份数据,执行写操作
页表:记录虚拟地址和物理地址的映射关系
优点:恢复大数据集的速度比AOF快、对CPU和内存的影响比较小
适用场景:需要做冷备份、对数据恢复要求不高
AOF
AOF持久化: AOF机制对每条写入命令作为日志,以 append-only 的模式写入一个日志文件中,在 redis 重启的时候,可以通过回放 AOF 日志中的写入指令来重新构建整个数据集。
AOF默认是关闭的,需要在redis.config中更改配置来开启AOF;
AOF的命令记录的频率也可以通过redis.config文件来配,具体的执行策略如下:
优点:更加稳定,数据的完整性更好、对数据安全性要求较高
使用场景:购物车、订单等关键业务
AOF和RDB的优缺点对比
RDB:
优点:数据文件的大小相比于aof较小,使用rdb进行数据恢复速度较快
缺点:比较耗时,存在丢失数据的风险
AOF:
优点:数据丢失的风险大大降低了
缺点:数据文件的大小相比于rdb较大,使用aof文件进行数据恢复的时候速度较慢
缓存
什么是缓存穿透,怎么解决?
缓存穿透:查询一个一定不存在的数据,redis中查询不到缓存也不会直接写入缓存,就会导致每次请求都查数据库,可能会导致DB挂掉。
解决方案:
方案一:缓存一个空数据,查询返回的数据为空,仍把这个空结果进行缓存。
优点:简单
缺点:消耗内存,可能导致发生不一致的问题
方案二:布隆过滤器
实现方案:Redisson Guava
优点:内存占用较少
缺点:实现复杂,存在误判
什么是布隆过滤器?
布隆过滤器可以用于检索一个元素是否存在一个集合中。
bitmap:相当于是一个以(bit)为单位的数组,数组中的每个单元只能存储二进制数0或者1。
存储数据::id为1的数据,通过多个hash函数获取hash值,根据hash计算数据对应位置改为1。
查询数据:使用相同的hash函数获取hash值,判断对应位置是否都为1。
但是使用布隆过滤器也存在一定的误判,假如恰好一个不存在数据登经过hash函数的计算,误判存在,则也会导致判断的失误。误判率:数据越大误判率越低,反之则越高。但是同时也会带来内存过多的消耗。
解决方式:设置误判率,不超过5%以内或者增加数组长度
推荐使用redisson实现布隆:
它的底层主要先去初始化一个较大的数组,里面存放二进制,在一开始都是0,当一个key来了之后,经过3次hash计算,模于数组长度找到数据的下标,然后将数组中的原来的0改为1,这样的话,三个数组的位置就能标明一个key的存在,查找过程也是一样的。
缺点:可能产生一定的误判,通过设置误判率(不超过5%以内)或者增加数组长度来解决
误判率p受3个因素影响:二进制位的个数m,哈希函数的个数k,数据规模n
什么是缓存击穿,怎么解决?
缓存击穿:给某一个key设置了过期时间,当key过期的时候,恰好这时间点对这个key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并返回数据到缓存中,这个时候大量并发的请求可能会瞬间把DB压垮。
解决方案有两种:
一、互斥锁(分布式锁)
当缓存失效时,不立即去加载DB,先使用Redis的setnx去设置一个互斥锁,当操作成功返回时在进行加载DB的操作并回设缓存,否则重试get缓存的方法
缺点:如果选择数据的强一致性,建议使用分布式锁的方式,性能上可能没有那么高,锁需要等,也有可能产生生死锁的问题。(涉及钱之类的业务推荐使用)
二、逻辑过期
基本思路:
1、在在设置key时,设置一个过期时间字段,一块存入缓存中,不给当前key设置过期时间
2、当查询的时候,从redis取出数据后判断时间是否过期
3、如果过期则开通另一个线程进行线程同步,当前线程正常返回数据,这个数据不是最新
缺点:若优先考虑高可用性,性能比较高 ,但是数据同步上做不到强一致
什么是缓存雪崩,怎么解决?
缓存雪崩是指同一段时间大量的缓存key同时失效(采用了相同的过期时间)或者redis服务宕机,导致大量请求到达DB,DB瞬间压力过重雪崩。
(雪崩是很多key,击穿是某一个key)
解决方案:
1、主要是可以将缓存失效时间分散开,比如可以在原有的失效时间基础上增加一个随机值。比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的时间。(即给不同的Key的TTL添加随机值)
2、利用Redis集群提高服务的可用性
3、给缓存业务添加降级限流的策略(降级可作为系统的保底策略,适用于穿透、击穿、雪崩)
4、给业务添加多级缓存
redis双写一致问题?
即要求DB中的数据和redis中的数据保持一致
根据不同的业务情况可分为两种;
对于实时性要求不高,允许延迟一致的业务,(热点类数据)采用异步的方案同步数据:
1、使用MQ消息中间件,更新数据之后,通知缓存删除
2、利用canal中间键,不需要需改代码,伪装为mysql的一个从节点,当完成更新之后,canal会读取binlog数据的客户端获取到数据,更新缓存即可。
强一致性的,采用Redisson提供的读写锁
1、共享锁:读锁readLock,加锁之后,其他线程可以共享读操作
2、排他锁:(独占锁writerLock)加锁之后,阻塞其他线程的读写操作
场景:
需要数据库和redis中的数据保持高度一致,因为要求时效性比较高,所以采用读写锁保证的强一致性。
采用的是redisson实现的读写锁,在读的时候添加共享锁,可以保证读读不互斥,读写互斥。当我们更新数据的时候,添加排他锁,他是读写,读读都互斥,这样就能保持在写数据的同时是不会让其他线程读数据,避免了脏数据。这里面注意的是读写方法上都需要使用同一把锁才行。
排他锁的底层使用的是setnx,保证了同时只能有一个线程操作锁住的方法。
延迟双删:先删除Redis缓存数据,再更新MySQL,延迟几百毫秒再删除Redis缓存数据。
为什么不使用?
延时时间不好把控,在延时的过程中可能会出现脏数据,并不能保证强一致性,故不选择。
数据过期
Redis的过期键的删除策略?
Redis是key-value数据库,我们可以设置redis中缓存的key的过期时间。redis的过期策略就是指当redis中缓存的key过期了,Redis如何处理。
Redis的过期删除策略:惰性删除+定期删除两种策略进行配合使用。
惰性过期:
惰性删除:设置该key的过期时间后,则不再去管他,只有当访问一个key时,才会判断该key是否已过期,过期则清除。
优点:该策略可以最大化的节省CPU资源。对CPU友好,只会在使用该key时才会进行过期检查,对于很多用不到的key不用浪费时间进行过期检查。
缺点:对内存不友好。极端情况下可能会出现大量key过期没有被再次访问,从而不会被清除,占用大量内存。
定期过期
定期删除:每隔一定时间,会扫描一定数量的key,删除里面过期的key。该策略是一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。
定期清理有两种模式:
- SLOW模式是定时任务,执行频率默认为10HZ,每次不超过2 5ms,已通过修改配置文件redis.conf的hz选项来调整这个次数
- FAST模式执行频率不固定,单两次间隔不低于2ms,每次耗时不超1ms
优点:可以通过限制删除操作执行的时长和频率来减少删除操作对CPU的影响,另外定期删除,也能有效释放过期键占用的内存
缺点:难以确定删除操作执行的时长和频率。
淘汰策略
数据的淘汰策略:当Redis中的内存不够用时,此时在向Redis中添加新的key,那么Redis就会按照某种规则将内存中的数据删除掉,这种数据的删除规则被称为内存的淘汰策略。
Redis支持8种不同策略来选择要删除的key:
- noeviction:不淘汰任何key,但是当内存满时不允许写入新的数据,默认使用
- volatile-TTL:对设置了TTL的key,比较key的剩余TTL,TTL越小越先被淘汰
- allkeys-random:对全体key,随机进行淘汰(数据访问频率不大,没有明显区分冷热数据,则推荐使用)
- volatile-random:对全体设置了TTL的key,随机进行淘汰
- allkeys-LRU:对全体key,基于LRU算法进行淘汰(推荐使用,保留最近最常访问的数据,若业务有明显的冷热区分,则使用)
- volatile-LRU:对全体设置了TTL的key,基于LRU算法进行淘汰(如有置顶业务,则不设置过期时间,可保证一直置顶)
- allkeys-LFU:对全体key,基于LFU算法进行淘汰
- volatile-LFU:对全体设置了TTL的key,基于LFU算法进行淘汰
LRU最近最少使用。用当前时间减去最后一次访问时间,这个值越大则淘汰的优先级越高
LFU最少频率使用。会统计每个key的访问频率,这个值越小则淘汰的优先级越高
分布式锁
分布式锁使用的场景:集群情况下的定时任务、抢单、幂等性场景
Redis实现分布式锁主要利用Redis的setnx命令。setnx是SET if not exists(如果不存在,则SET)的简写。
获取锁的方式:# 添加锁,NX是互斥,EX是设置超时时间 SET lock value NX 10;
# 释放锁,删除即可 DEL key
redisson实现的分布式锁-执行流程:(setnx和lua脚本:保证原子性)
加锁、设置过期时间等操作都是基于lua脚本完成的
由于Redis的单线程,用了setnx命令之后,只能有一个客户端对某一个key设置值,在没有过期或者删除key的时候,其他客户端是不能设置这个key的。
控制redis实现分布式锁有效时长:
采用的是redis的一个框架redisson实现的。
在redisson中需要手动加锁,并且可以控制锁的失效时间和等待时间,当锁住的一个业务还未执行完的时侯,早redisson中引入了一个看门狗机制,就是说每隔一段时间就检查当前业务是否还持有锁,如果还持有锁就增加持有时间,当业务执行完之后需要使用释放锁就可以了。(WatchDog默认每隔10秒续期一次)
还有一个好处就是在高并发下。一个业务有可能会执行的很快,先客户1持有锁时,客户2来了以后它并不会马上拒绝,他会选择不断尝试获取锁,如果客户1释放完之后,客户2就可以马上持有锁,性能也得到了提升。
redis实现的分布式锁是否时可重入的:
是的可重入的,这样做是为了避免死锁的产生,这个重入其实在内部就是判断是否是当前线程持有的锁,如果当前线程持有锁就会计数,如果释放锁就会在计算上减1,在存储数据的时候采用的hash结构,大key可以按照自己的业务进行定制,其中小key是当前线程的唯一标识,value是当前线程重入的次数。
Redisson锁不嫩解决主从数据一致的问题:
但是可以通过redisson提供的红锁来解决,但是这样的话,性能就太低了,如果业务中非要保证数据的强一致性,建议采用zookeeper实现分布式锁。
(RedLock)红锁:不能只在一个redis实例上创建锁,应该是在多个redis实例上创建锁(n/2+1),避免在一个redis实例上加锁。缺点:实现复杂、性能差、运维复杂。
根据需求选择不同的数据库:
AP思想(redis): 要求最终一致性
CP思想(zookeeper):强一致性
计数器
保存token
消息队列
延迟队列
redis集群及事务相关问题
集群
Redis都存在哪些集群方案?
1、主从复制
保证高可用性
实现故障转移需要手动实现
无法实现海量数据存储
2、哨兵模式
保证高可用性
可以实现自动化的故障转移
无法实现海量数据存储
3、Redis分片集群
保证高可用性
可以实现自动化的故障转移
可以实现海量数据存储
事务
首先,我们要清楚redis中的事务不同于MySQL中的事务,redis中的单条命令保证原子性,但是,Redis中的事务不保证其的原子性,也不存在事务隔离级别之类的概念。
Redis中的事务并不是直接进行的必须通过EXEC命令才能执行。
Redis事务:
开启事务(multi),总是返回OK。multi执行之后,客户端可以继续向服务端发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列之中,当EXEC命令被调用时,所有队列中的命令才会被执行。
命令入队(进行赋值):例如进行GET SET 值之类的命令,会返回一个QUEIED,表示入队成功。
执行事务(EXEC):执行事务块内的命令。返回事务块内所有命令的返回值,按照命令的先后顺序排列。当操作被打断时,返回空值nil; 每次事务执行完毕之后,必须再次重新开启事务
WATCH命令(乐观锁):可以为Redis提供check-and-set(CAS)行为,可以监控一个或多个键,一旦其中有一个或者多个键被修改或者删除,之后的事务就不会执行,监控一致持续到EXEC命令。
通过调用Discard,客户端可以清空事务队列,并放弃执行事务,并且客户端从事务状态中退出。
Unwatch:取消watch对所有key的监控。
事务中的异常:
1、编译时异常:即代码存在问题,则所有的命令都不会进行执行
2、运行时异常:(1/0)如果事务队列中存在语法性错误,那么在执行命令的时候,其他命令还是可以正常执行的,错误命令抛出异常。
Redis为什么快?
1、完全基于内存的
2、采用单线程,避免不必要的上下文切换可竞争条件
3、数据简单,数据操作也相对简单
4、使用多路I/O复用模型,非阻塞IO