-
Redis数据结构
-
字符串(String):底层是redis自定义的一种简单动态字符串SDS;在Redis中,包含字符串值的键值对底层都是用SDS实现的
-
SDS是
二进制安全
的数据结构:SDS API是以处理二进制的方式来处理SDS存放在buf数组里的数据,可以存储任意二进制数据;不像C语言字符串那样以‘\0’来标识字符串结束,因为传统C字符串符合ASCII编码,这种编码的操作的特点就是:遇零则止
;因此,如果传统字符串保存图片,视频等二进制文件,操作文件时就被截断了;
-
SDS提供了
内存预分配机制
,避免了频繁的内存分配
-
-
列表(List):底层实现就是压缩列表、双向链表,压缩列表比双向链表更节省空间
-
散列(Hash):底层存储有两种数据结构,
ziplist + hashtable
-
集合(Set):整数集合、哈希表
-
有序集合(Sorted Set):每个元素有权重,可以用作排行榜,zset底层在数据量少的时候使用
压缩表
,数据量大的时候转成跳表
;
-
String操作和适用场景:key和value大小最大
512M
-
常用操作
- set [key] [value] 给指定key设置值(set 可覆盖老的值)
- get [key] 获取指定key 的值
- del [key] 删除指定key
- exists [key] 判断是否存在指定key
- expire [key] [time] 给指定key 设置过期时间 单位秒
- setex [key] [time] [value] 等价于 set + expire 命令组合
- setnx [key] [value] 如果key不存在则set 创建,否则返回0
- incr [key] 如果value为整数 可用 incr命令每次自增1
- incrby [key] [number] 使用incrby命令对整数值 进行增加 number
- mset [key1] [value1] [key2] [value2] … 批量存键值对
- mget [key1] [key2] … 批量取key
-
适用场景
- 计数器:incr和decr命令的作用是将key中储存的数字值加一/减一,这两个操作具有原子性;如微博的
评论数、点赞数、分享数
,抖音作品的收藏数,京东商品的销售量、评价数; - 存储对象:将对象转换为
JSON字符串
,再存储在string类型中;如用户信息、商品信息等。
- 计数器:incr和decr命令的作用是将key中储存的数字值加一/减一,这两个操作具有原子性;如微博的
-
-
-
list操作和适用场景
-
常用命令
- rpush [key] [value1] [value2] … 链表右侧插入
- rpop [key] 移除右侧列表头元素,并返回该元素
- lpop [key] 移除左侧列表头元素,并返回该元素
- llen [key] 返回该列表的元素个数
- lrem [key] [count] [value] 删除列表中与value相等的元素,count是删除的个数。 count>0 表示从左侧开始查找,删除count个元素,count<0 表示从右侧开始查找,删除count个相同元素,count=0 表示删除全部相同的元素
- lindex [key] [index] 获取list指定下标的元素 (需要遍历,时间复杂度为O(n))
- lrange [key] [start_index] [end_index] 获取list 区间内的所有元素 (时间复杂度为 O(n))
- ltrim [key] [start_index] [end_index] 保留区间内的元素,其他元素删除(时间复杂度为 O(n))
-
应用场景
- 消息队列:Redis的lpush + brpop命令组合即可实现阻塞队列,生产者客户端使用lpush从列表左侧插入元素,多个消费者客户端使用brpop命令
阻塞式的争抢
列表尾部的元素,多个客户端保证了消费的负载均衡和高可用 - 最新列表:list类型的lpush命令和lrange命令能实现最新列表的功能,每次通过lpush命令往列表里插入新的元素,然后通过lrange命令读取最新的元素列表,如朋友圈的点赞列表、评论列表
- 消息队列:Redis的lpush + brpop命令组合即可实现阻塞队列,生产者客户端使用lpush从列表左侧插入元素,多个消费者客户端使用brpop命令
-
-
set操作和适用场景
-
常用操作
- sadd [key] [value] 向指定key的set中添加元素
- smembers [key] 获取指定key 集合中的所有元素
- sismember [key] [value] 判断集合中是否存在某个value
- scard [key] 获取集合的长度
- spop [key] 弹出一个元素
- srem [key] [value] 删除指定元素
-
适用场景
- 去重场景:好友、关注、粉丝、感兴趣的人集合;每日签到用户ID池
-
-
hash操作和适用场景
-
常用操作
- hset [key] [field] [value] 新建字段信息
- hget [key] [field] 获取字段信息
- hdel [key] [field] 删除字段
- hlen [key] 保存的字段个数
- hgetall [key] 获取指定key 字典里的所有字段和值 (字段信息过多,会导致慢查询 慎用:亲身经历 曾经用过这个这个指令导致线上服务故障)
- hmset [key] [field1] [value1] [field2] [value2] … 批量创建
- hincr [key] [field] 对字段值自增
- hincrby [key] [field] [number] 对字段值增加number
-
使用场景
- 结构型数据存储:用户购物车、存储对象
-
-
zset操作和适用场景
-
常用操作
- zadd [key] [score] [value] 向指定key的集合中增加元素
- zrange [key] [start_index] [end_index] 获取下标范围内的元素列表,按score 排序输出
- zrevrange [key] [start_index] [end_index] 获取范围内的元素列表 ,按score排序 逆序输出
- zcard [key] 获取集合列表的元素个数
- zrank [key] [value] 获取元素在集合中的排名
- zrangebyscore [key] [score1] [score2] 输出score范围内的元素列表
- zrem [key] [value] 删除元素
- zscore [key] [value] 获取元素的score
-
适用场景
- 动态排行榜:比如说粉丝关注列表,score为关注时间;用户成绩,score为成绩;小说热度排行表,score为热度值
-
-
Redis是单线程到多线程的演化
- Redis客户端对服务端的每次调用都经历了发送命令,执行命令,返回结果三个过程;其中执行命令阶段,由于Redis是单线程来处理命令的,所有每一条到达服务端的命令不会立刻执行,会进入一个队列中,然后逐个被执行,所以多个客户端发送的命令的执行顺序是不确定的
- 演化主要经历两个阶段,redis4.0之前、redis4.0之后、redis6.0之后
- redis4.0之前执行原理:通过
IO多路复用器监
听来自客户端的socket网络连接,然后由主线程进行IO请求的处理以及命令的处理,所有操作都是线性的 - redis4.0之后引入Lazy Free机制:由于Redis单线程执行命令,若客户端向Redis发送一条耗时较长的命令,比如删除一个含有上百万对象的Set键,或者执行flushdb,flushall操作,Redis服务器需要回收大量的内存空间,这事就会导致Redis服务阻塞;所以Redis4.0引入Lazy Free,目的是将
慢操作异步化
,将慢操作放入异步任务队列中,有单独的线程处理 - Redis6.0引入多线程,将
网络I/O异步化
:单线程的性能瓶颈主要在网络IO操作上
,即在读写网络 read/write 命令期间会占用大部分 CPU 时间,这对于单线程来说,会阻塞后边操作;Redis6使用多线程,将网络数据读写和协议解析
通过多线程的方式来处理,对于命令执行
来说,仍然使用单线程
操作
-
redis为什么会这么快?
- 完全基于内存
- 数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的,其中哈希表和压缩列表是复用比较多的数据结构
- 单线程:利用
队列技术
将并发访问
变为串行访问
,避免了上下文切换
和竞争锁
等而消耗CPU - 使用多路I/O复用模型,非阻塞IO
-
redis内存回收机制:过期策略(删除过期时间的key)+淘汰策略(内存使用达到maxmemony上限时触发内存淘汰机制)
- 定时过期:为每个设置过期时间的key设置一个定时器,到期立即清除堆内存友好,但会占用大量的CPU资源
- 惰性过期:只有当访问key时,去判断key是否过期,过期则删除;对内存不友好,但可以最大化节省CPU资源
- 定期过期:每个一段时间(默认每秒运行10次)扫描一定数量的key,并清除已过期key
- 折中方案:Redis中同时使用了惰性过期和定期过期两种过期策略;
- volatile-lru:从已设置过期的数据集中挑选最近最少使用的淘汰
- volatile-ttl:从已设置过期的数据集中挑选将要过期的数据淘汰
- volatile-random:从已设置过期的数据集中任意挑选数据淘汰
- allkeys-lru:从数据集中挑选最近最少使用的数据淘汰
(最常用的)
- allkeys-random:从数据集中任意挑选数据淘汰
- noenviction:禁止驱逐数据;可以保证数据不被丢失,这也是系统默认的一种淘汰策略
-
Redis持久化方式
- RDB持久化(默认):定时保存到磁盘上的RDB文件;RDB文件紧凑、体积小,恢复速度快,对性能影响小;但无法做到实时持久化
- AOF持久化:将Redis的操作日志以追加的方式写入文件;以日志的形式记录服务器处理的每一个写、删除操作,当服务器重启时会重新执行这些命令恢复原始数据;支持秒级持久化、兼容性好;文件大,恢复速度慢,对性能影响大
-
数据库和redis缓存一致性解决方案
- 一般情况下,先删缓存,再更新数据库;如果缓存删除失败,则直接返回;如果缓存删除成功,数据库更新失败,仍能保持数据一致
- 高并发情况下,删除缓存成功,这时去更新数据库,但数据库还没更新完,有一个查询请求过来,发现缓存里没有,就去数据库里查,此时查到的是旧数据,而且又被写入缓存,这样等数据库更新操作完成后,缓存和数据库数据不一致;此时可以用队列的去解决这个问题,当有数据更新请求时,先把它丢到队列里去,当更新完后在从队列里去除,如果在更新的过程中,遇到以上场景,先去缓存里看下有没有数据,如果没有,可以先去队列里看是否有相同数据在做更新,如果有也把查询的请求发送到队列里去,然后同步等待缓存更新完成
-
缓存穿透、缓存血崩、缓存击穿
-
缓存穿透
-
定义:查询不存在数据 ——不断请求缓存和数据库中都没有的数据,导致数据库压力过大,严重会击垮数据库
-
避免:
- 对查询结果为空的情况也进行缓存
- 接口层增加key的简单校验
- BloomFilter:利用高效的数据结构位数组和算法(K的hash散列函数)快速判断出你这个Key是否在数据库中存在,不存在你return就好了,存在你就去查了DB刷新KV再return
-
-
缓存血崩
-
定义:某一时刻发生大规模的缓存失效、导致系统崩溃的情况
-
避免:
- 不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀
- 使用 Hystrix进行限流 & 降级(二级缓存)
- 采用集群,降低服务宕机的概率
-
-
缓存击穿(缓存并发)
-
定义:在高并发的情况下,大量的请求同时查询同一个key时,此时这个key正好失效了,就会导致同一时间,这些请求都会去查询数据库
-
避免:
- 设置热点数据永远不过期(比如首页数据)
- 分布式互斥锁:只允许一个线程查询DB,然后回写到缓存
-
-
-
redis热点问题
-
问题:Redis中某个Key的访问频次远大于其他剩余的Key,导致该key的请求打到同一个节点上,压垮缓存服务,热点问题其实是
局部性问题
-
一般Redis单节点的查询性能在2W的QPS,MySQL单机的查询性能一般在4K的QPS
-
排查:依赖监控数据,监控Redis各个实例的CPU使用率、QPS 数据,如果你看到Redis集群中某些实例负载和QPS特别高,但其他实例负载很低,不用问肯定是出现热点问题了,接下来你需要做的就是找出具体的热点key,并且找出数据访问的来源
-
解决:
- 增加应用层Cache:比如GuavaCache,对于热点数据,采用本地缓存,这样热点数据请求在应用层内部就能消化,从而减少对redis的压力
- 增加数据副本:将热点key的请求做下拆分,在数据写入的时候,可以用不同的Key重复写10份,比如 XXX_KEY_01, XXX_KEY_02……XXX_KEY_10, 访问的时候在原始Key上随机拼接一个1-10之间的后缀即可,这样就能实现数据请求的分散,如果想让请求更分散,可以存储更多的副本
-
-
基于缓存(redis、memcached)的分布式锁
- 利用Redis的SETNX key value这个命令实现分布式锁
- 加锁:
stringRedisTemplate.opsForValue().setIfAbsent(lockKey, requestId, 30, TimeUnit.SECONDS);
- 解锁:判断当前解锁的竞争者id是否为锁的持有者,如果不是直接返回失败,如果是则删除key,如果删除成功,返回解锁成功,否则解锁失败;这里使用Lua脚本实现原子性,防止解锁不属于自己线程的锁:
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
- 是否可重入:以上实现的锁是不可重入的,如果需要实现可重入,在SET_IF_NOT_EXIST之后,再判断key对应的value是否为当前竞争者id,如果是返回加锁成功,否则失败
- 死锁问题:加锁时我们设置了key的超时,当超时后,如果还未解锁,则自动删除key达到解锁的目的。如果一个竞争者获取锁之后挂了,我们的锁服务最多也就在超时时间的这段时间之内不可用
自动续期
:锁超时时间的设置,设大设小都不合适,redisson的看门狗 Watch Dog机制,其实就是一个后台定时任务线程,获取锁成功之后,会将持有锁的线程放入到一个 RedissonLock.EXPIRATION_RENEWAL_MAP里面,然后每隔 10 秒 (internalLockLeaseTime / 3) 检查一下,如果客户端 还持有锁 key(判断客户端是否还持有 key,其实就是遍历 EXPIRATION_RENEWAL_MAP 里面线程 id 然后根据线程 id 去 Redis 中查,如果存在就会延长 key 的时间),那么就会不断的延长锁 key 的生存时间
-
Redis扩容方案
- 主从复制: 通过在集群中添加新的从服务器来扩容集群
- 哨兵机制: 通过在集群中添加新的哨兵服务器来扩容集群
- 集群模式: 通过在集群中添加新的节点来扩容集群
- 分片: 通过将数据分散到多个节点上来扩容集群
- 哈希槽: 通过将数据分布到多个节点上来扩容集群
-
lua脚本如何实现原子性的?
-
Lua脚本优势:
- 支持原子性操作:整体执行,不会被其他他请求插入
- 降低网络开销:将多个请求通过脚本形式一次发送到服务端,减少网络延时
- 脚本复用:脚本可以保存在服务端,被复用
-
redis专题
最新推荐文章于 2024-11-09 21:20:16 发布