目录
Redis五大数据结构
String
简介
String字符串类型,可以存储字符串、整数、浮点数、图片(图片的 base64 编码或者解码或者图片的路径)、序列化后的对象。
常见问题
1.String数据结构(SDS)
SDS,我们都知道Redis是C语言编写的,但是却没有采用c语言中已经设置好的String类型,而是创建了一种新的数据结构,下面对SDS属性和优点进行解析。
SDS中的属性:
SDS的优点:
- 避免内存泄漏:采用SDS对数据类型修改的时候,会判断alloc-len所剩空间,避免内存泄漏
- 获取字符长度简单:c语言获取字符长度通过遍历,而SDS直接通过获取len属性值
- 减少内存分配次数:SDS实现空间预分配和惰性空间释放。alloc就是空间预分配的一种体现,惰性空间释放就是当减少字符串的时候不会立刻回收
- 二进制安全:C语言无法判断空字符串作为结束标识,SDS可以通过len去判断是否结束
2.String应用场景
- 分布式锁:通过SETNX命令,如果存在该key值就会返回0,如果不存在该key值返回1
- 简短计数场景:String类型提供incr、decr命令,可以进行数据统计,例如统计网站访问人数
- 存储对象(共享Session,图片地址,序列化后的对象)
3.Hash与String存储对象的区别
Hash存储对象或者数据是分段储存,对每一段单独数据都可以进行控制修改,适用于网络传播、单个字段经常修改。String类型存储对象占据空间资源是Hash的一半,而且存储多层嵌套的方式也更为便捷。但按照日常使用来说还是String存储对象更为常见。
Set
简介
Redis中的一种无序集合,集合存储数据顺序不确定,但是保证唯一。类似于Java中HashSet
常见问题
Set应用场景
- 进行网站UV统计
- 订单下单,搭配Lua脚本进行一人一单验证
- 抽奖转盘,将奖品放到set中随机取奖品
List
简介
Redis 的 List 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。
常见问题
list应用场景
信息流展示,可以做最新文章,最新动态展示
Hash
简介
一个 String 类型的 field-value(键值对) 的映射表,特别适合用于存储对象,后续操作的时候,你可以直接修改这个对象中的某些字段的值。其实就是KEY Map结构
常见问题
Hash应用场景
- 购物车,key为用户,Map的Key商品,Value商品数量(因为商品数量会经常修改,Hash比String更适合)
SortedSet
简介
SortedSet是在Set集合的基础上添加了Score字段,通过Score字段,可以对数据权重进行分配。例如Score中添加时间戳,就可以通过zrange(从小到大)命令,读取最先发布的文章、存入步数大小,就可以通过zrevrange(从大到小)命令,读取步数最大的值
常见问题
SortedSet常见场景
- 排行榜,例如微信步数排行榜(根据步数大小排行)、微信朋友圈点赞(根据时间排序)
Redis三种特殊数据结构
BitMap(位图)
简介
BitMap(位图),Key存String,Value存二进制数。BitMap的作用是什么那,将只有两种状态的情况抽象为用0/1表示。这样的作用就是更加节省资源。Bit指一个比特位,Map指映射。底层通过String实现BitMap
举例
如果我们要做一个用户签到功能,我们通过数据库表进行实现,我们统计一个月可能就需要600个字节。但是我们去使用BitMap,Key为 张三:2002-01,Value 0 0 1 1 1 0 1,Value一个月只需要4个字节就能实现。
位图的作用就是将只有两种业务状态抽象为0/1表示,节约资源
bitMap使用场景
- 用户签到
- 朋友圈点赞
HyperLoglog
简介
HyperLoglog是一种基数计数概率算法,前身是Loglog。Redis提供的HyperLoglog占用的空间很小,只需要12k的空间,就可以存储2^64个元素,但是为了节省空间该算法会有0.81%的误差。
Redis优化HyperLoglog的计数方法分为稀疏矩阵,稠密矩阵。其中稀疏矩阵,在计数少的时候使用占用空间少。稠密矩阵,在数量达到某个阈值时,固定占用12k资源。
HyperLoglog使用场景
网站Ip统计,热门帖子UV统计。
GEO
简介
Geospatial index(地理空间索引,简称 GEO) 主要用于存储地理位置信息,基于 Sorted Set 实现。
GEO使用场景
附近的人
布隆过滤器
布隆过滤器功能
判断存在布隆过滤器中的数据是否存在。常用于解决缓存穿透问题,判断Mysql中是否有数据,如果没有就不访问Mysql提高程序响应速度。
布隆过滤器可能误判,布隆过滤器判断元素不存在,元素一定不存在。判断存在时,可能不存在。
布隆过滤器原理
布隆过滤器新增过程:
布隆过滤器查询:比新增多一步,确定所有索引值都为1,布隆过滤器在查询的时候,只有当多个Hash值计算出来的下标值都是1的时候,才能认为该值存在。
布隆过滤器查询删除:同样布隆过滤器不能删除元素的原因也在这里,删除的元素可能出现Hash冲突,导致删除多个元素。
布隆过滤器查询误差原因:Hash时,出现Hash碰撞。将来存在的数组下标值返回给用户,但是这里其实存的并不是“你好”,而是"Hello"。
Redis持久化方案
RDB(内存快照)
RDB(内存快照),类似于照相机,记录某一个时刻的内存数据。通过save或者bgsave命令可以使redis开启RDB模式。
save命令直接阻塞Redis进程,只进行备份操作。
bgsave是开启一个子进程,异步进行备份,但是从主进程产生子进程的过程被阻塞。
执行频率:
save 900 1; #每900秒有一次修改,启动RDB备份数据
save 300 10; #每300秒有十次修改,启动RDB备份数据
save 60 10000; #每60秒有一万次修改,启动RDB备份数据
RDB原理bgsave原理:
页表 : liunx操作系统无法直接修改物理内存内容,通过页表对真实内存进行映射,从而达到修改内存的目的。
bgsave过程:
- 主进程fork,产生子进程(Redis线程阻塞)
- 通过页表机制,主进程与子进程共享内存资源,子进程对当前数据进行备份
- 当主进程出现写请求,将当前数据标记为只读,子进程继续进行备份。
- 主进程开辟新的内存空间进行写操作
RDB缺点:
因为RDB会阻塞Redis进程,所以必须有一段时间的间隔才能进行数据备份,间隔时间的过程中如果Redis宕机会造成数据泄露。
AOF(追加文件)
AOF(追加文件)会将Redis中每次执行的命令都记录到aof文件当中。
执行流程:
#在redis-config redis配置文件中修改
#开启AOF
appendonly yes;
#配置执行频率
#always表示:每写一次命令都记录到AOF当中
#everysec:表示写命令先放到缓存AOF区中,每隔1s将缓存区中数据写到AOF文件中,默认方案
#no:由操作系统决定何时进行AOF操作
appendfsync always/everysec/no
#何时触发AOF重写
auto-aof-rewrite-percentage 100 #比上次文件,增长多少百分比触发重写
auto-aof-rewrite-min-size 64mb #文件到达64mb触发重写
AOF重写机制
因为AOF文件通常比RDB问价大许多,而且会对同一个Key进行重复操作,往往只有最后一次操作才有意义。所以就提供bgrewriteaof(重写),重写aof文件。采用最少的命令,达到相同的效果。需要注意的是重写后的AOF文件依然比RDB大许多。
Redis集群配置
如何搭建主从集群
在从Redis中执行slaveof <主节点ip> <主节点端口>,这样给从节点设置了主节点
全量同步
全量同步一般发现在第一次主从同步或者是增量同步时从节点宕机太久。
ReplicationId与offset:
ReplicationId:每一个节点都有一个ReplicationId,当第一次进行主从同步时,通过校验ReplicationId来判断是否是第一次同步。
offset:偏移量,记录RDB或者是从节点宕机时的数据。类似于版本信息,当需要增量同步时就会按照offset去更新对应版本数据。
全量同步流程:
- 从节点发送全量同步请求,携带ReplicationId和offset信息,通过判断ReplicationId是否一致,来判断是不是第一次同步。(完成主次同步的集群ReplicationId相同)
- 通过bgsave(RDB)生成RDB文件,进行全量保存。同时在RDB期间写入的数据记录到repl_baklog文件中。
- 发送repl_baklog中的数据。
增量同步
增量同步过程:
- 从节点发送数据同步请求,主节点判断replid,如果相同进行增量同步。
- 通过从节点的offset版本与主节点版本对比然将命令发送给从节点进行数据同步。
repl_baklog的缺点:
repl_baklog本质是个数组,当salve宕机太久,master写入的数据就会将整个数据覆盖,甚至会将原有的master数据覆盖掉,导致slave重启无法获取全部数据。
如何优化同步效率:
1.在master中配置repl-diskless-sync为yes,启用无磁盘复制,减少全量同步RDB的磁盘消耗。
2.适当提高repl_loklog文件大小,发现salve节点宕机需要快速启动。
3.当从节点过多时,限制master中从节点的数量,设置为主从从链式结构,减少master压力。
Redis哨兵机制
什么是哨兵机制
当我们的从节点宕机时,可以通过增量同步恢复数据。那么如果主节点宕机了,该怎么办?
主节点如果宕机,就会将其他从节点作为主节点继续执行任务,并且通知Java客户端。
哨兵机制的本质就是添加一层监控者的角色,当Java客户端访问Redis时,通过哨兵去找到健康的Redis节点。
哨兵机制的作用:
- 监控各节点健康状态。
- 故障转移,选举其他节点作为Master(避免原主节点宕机导致无法进行写操作),后续还以新Master作为主节点。
- 通知客户端,将新选举的Master告知Java客户端。
哨兵如何监控集群监控状态
哨兵监控机制:
第一步:单个哨兵每个1s对节点ping一次,如果在规定时间内节点没有pang,该哨兵标记该节点为主观下线。
第二步:多个哨兵对节点评估,如果超过一半的哨兵认为节点主观下线,那么该节点就被认为客观下线(节点宕机)
如何选取新Master:
- 判断slave与Master节点断开时间,如果超过指定值(down-after-milliseconds*10),排除该slave节点。
- 判断offset值,值越大优先级越高,当offset一样时随机选一个slave。
如何配置哨兵
Java客户端配置哨兵集群(www.bilibili.com/video/BV1cr4y1671t)
Redis三大问题击穿、穿透、雪崩
缓存击穿
产生原因:
热点Key失效,导致大量请求直接访问到数据库,导致数据库压力提升。
解决办法:
- 热点Key设置永久有效期
- 热点数据预加载(将热点数据提前缓存进redis)
- 业务添加分布式锁,当出现缓存击穿分布式锁会将请求打入数据库的速度减慢,非常好用
缓存穿透
产生原因:
大量请求访问到redis,但是redis没有缓存对应数据,请求就会访问到数据库中。
解决办法:
- 缓存空值(将数据库不存在的值,在redis中存入空)
- 布隆过滤器
- 添加锁
缓存雪崩
产生原因:
大量key设置同一过期时间,导致一瞬间全部key失效,导致雪崩
解决办法:
- 添加过期时间的时候,添加一个随机时间(5 + random)
- 添加锁
设计完善的Redis问题解决架构
组合方案流程:
- 第一步:大量请求访问缓存(有可能发生缓存击穿、缓存穿透)
- 第二步:通过布隆过滤器判断数据库是否存在值(放止缓存穿透)
- 第三步:检验空值(防止缓存穿透)
- 第四步:通过分布式锁对数据库进行查询(放止缓存击穿、雪崩)
- 第五步:加载数据到缓存,不存在缓存空值
第四步进行分布式锁执行的过程中,添加双重判定锁。大量请求串行执行时,第一个请求获取数据之后就会将数据加载到缓存中,其他请求就可以直接去缓存中寻找结果,不必再次查询数据库。
Redis实现分布式锁
为什么要使用分布式锁?
单体架构中,可以通过sycn、乐观锁(CAS+sycn)保证线程之间的互斥性,原因是一个JVM中会记录sycn保证不重复。
但是在分布式架构中,一个模块被拆分为多个服务,不同服务之间JVM不同,所以sycn的id可能会出现重复,不能保证线程安全。
为保证多服务共享锁id,需要一个公共组件去记录并且保证其互斥性,这个时候就需要去使用Redis实现分布式锁。
原生Redis实现分布式锁
1. Redis提供Set Nx命令(Key为锁标识,Value为当前线程id),不存在存入数据,存在不添加返回nil,保证锁的互斥性。、
2.Redis在Set Nx命令的基础上,可以添加EX命令,设置锁超时过期时间,放止Redis宕机出现死锁问题,添加兜底策略。
3.Redis在释放锁时,在多线程操作下可能出现锁误删情况,所以在释放锁时要添加判断Key对应的线程id是不是当前正在执行任务的线程id,然后再去释放锁。
判断误删条件,释放锁虽然中间没有任何业务代码,但是JVM可能因为fullGC阻塞,导致超时释放,从而引发误删。
释放锁前进行误删条件判断,然后通过Lua脚本封装判断误删条件和释放锁操作保证原子性。
Redission如何解决可重入、可重试、超时续费、
什么是可重入锁ReentranLock?
同一个线程获得可重入锁之后,可以重复获取同一把锁而不会等待。
原理:信号量机制,如果同一线程多次获取锁,就将信号量+1,反之 减一。
Redission的trylock方法流程:
1.第一步:获取锁。通过redis获取锁,如果锁被占用就对锁的占用次数+1(Hash结构,Key:锁名称,Value:ThreadId,ThreadId对应是重入次数)。通过Redis的Hash结构,通过记录重入次数实现可重入机制(Jdk中ReentryLock的机制一样,记录重入次数实现重入)。
2.第二步:超时续费。获取锁成功后,判断返回结果执行scheduleExpirationRenew()方法执行看门狗(定时任务+递归),创建一个定时任务,在定时任务中(过期时间的1/3时执行定时任务),重新设置过期时间,并且进行递归操作,保证线程任务不会因为过期时间而中断任务(同时避免了死锁)。
3.第三步:可重试机制。获取锁失败,通过订阅unlock中广播情况(减少无效循环,降低CPU压力),等待其他锁释放之后再去执行循环持续获取锁。
Redis与Mysql数据一致性
注意事项:
- 数据一致性问题不会发生在只读情况下,而是在读写同时操作时。
- Redis与Mysql数据一致性是最终一致性。如果通过锁保证Redis与Mysql共同操作的原子性,就违背了使用Redis提高数据读写效率的初衷。
- 删除缓存而不是修改缓存。修改缓存逻辑复杂,删除缓存速度更快。
- Redis与Mysql数据一致性一定是在高并发环境下考虑的问题(不要脱离前提谈问题)。
先操作缓存再操作数据库
延时双删
第一次删除
修改数据操作时,先去删除缓存,避免并行的查询操作一直读取旧数据。
第二次删除
数据不一致原因(线程1修改数据库被阻塞):线程1删除缓存,线程2查询缓存无内容;线程1接着修改数据库但被阻塞,线程2查询数据库为老数据存入到缓存;线程1醒来修改数据库为新数据,此时redis与mysql出现数据不一致,其他线程读到脏数据。
第二次删除:当线程1更新完数据库时,去执行删除缓存逻辑,后续线程就会继续查询数据库中的新数据。
延时原因
在第5步,如果线程2查询到老数据后准备返回给缓存,但是此时线程1删除操作先执行,之后线程2才把老数据返回给缓存。那么更新操作的删除就没有起到作用,必须要让更新操作在其他查询操作之后删除才有效,所以需要进行延时操作(默认500ms)。
先操作数据库再操作缓存
直接下定义:先操作数据库的方案更优秀,比先删缓存少了一步删除操作,而且编码难度更低。
出现数据不一致的原因:
删除操作时,删除失败,只能等缓存数据过期。但是该问题先操作缓存同样有。
Canal保证重试删除
对于删除操作,我们可以通过MQ去接收和发送消息,通过MQ对删除失败的消息进行重试,但是只使用MQ耦合度太高了。
我们可以使用Canal订阅Mysql的binLog日志,如果Mysql数据发生改动Canal就会通知删除服务对Redis缓存进行删除,如果删除失败发送消息到MQ中,MQ发送消息到删除服务中进行重试。
Redis场景题(个人思考)
以下都是个人思考,如有错误评论告诉我,我会及时改正。
1.使用过期时间预防redis宕机产生的死锁,锁之后的业务代码未执行如何处理,事后如何恢复。
首先我们应该提前配置Redis主从集群,提前启动哨兵机制,去避免宕机问题。
第一步:业务代码对Redis加锁操作时,在加锁后添加Try Catch,在finally里面释放锁(Redission释放锁可以提前添加过期时间,就算宕机也可以释放锁)。
第二步:在Catch中添加兜底机制,例如:当出现Redis连接异常,将当前数据、异常记录到日志中。
第三步:Redis重启后,可以通过MQ重新执行业务逻辑,也可以手动重新执行。
2.在Redis中,现在我有一亿个数据,其中十万个数据是有固定开头的索引,我应该怎么将他找出来哪。
如果使用 keys 前缀+*(通配符)方式会一次性获取全部数据,大数据量下Redis内存占用会很大,不推荐这种方式。
如何查询:
1. 数据如果按照顺序在sortset,可以通过ZRANGEBYLEX(范围查询),因为是有序集合,速度会很快。
2. SCAN cursor [MATCH pattern] [COUNT count]
cursor:表示游标第一次从0次开始查,第二次使用返回值就行,当返回值为0时就表示查询完毕所以数据
[MATCH pattern]:表示匹配什么前缀
[COUNT count] :表示每次去寻找多少条
通过Scan命令就可以每次获取少量数据,也不会导致一次获取大量数据导致Redis内存占用过大
3.在使用分布式锁时,获取锁失败该怎么办(高并发情况下)
Redission获取分布式锁,提供trylock()和lock()方式获取分布式锁。
tryLock:失败就会立刻返回异常,响应迅速但是会抛出异常,适合秒杀业务,当用户没有抢到商品会重新抢购。
lock:失败就会再次重试,等待其他线程释放锁,响应慢但是用户体验好。
首先要根据具体业务去判断,如果是传统秒杀业务这种高并发的场景下适合使用tryLock,获取锁失败快速返回错误信息,用户也会自己去刷新。