redisvalue最⼤值_Redis基础知识整理
Redis安装和使⽤
使⽤Docker安装Redis
docker run --name redis -p 6379:6379 --restart always -d redis
使⽤redis-cli执⾏redis命令
docker exec -it redis redis-cli
Redis思维导图
Redis的整体结构
单线程
Redis使⽤⼀个线程来处理所有的客户端请求,使⽤多路复⽤来达到⾼性能。多个 socket 可能会并发产⽣不同的操作,每个操作对应不同的⽂件事件,但是 IO 多路复⽤程序会监听多个 socket,会将 socket 产⽣的事件放⼊队列中排队,事件分派器每次从队列中取出⼀个事件,把该事件交给对应的事件处理器进⾏处理。
⼏种数据结构
Redis不是⼀个简单的key-value存储。
键相关知识点
Redis过期键的删除策略是定期删除+惰性删除。
定期删除使⽤的是贪⼼策略,它每秒会进⾏ 10 次过期扫描,此配置可在 f 进⾏配置,默认值是 hz 10,Redis 会随机抽取 20 个值,删除这 20 个键中过期的键,如果过期 key 的⽐例超过 25% ,重复执⾏此流程。需要预防⼤量的缓存在同⼀时刻⼀起过期,简单的解决⽅案就是在过期时间的基础上添加⼀个指定范围的随机数。
惰性删除是指查的时候如果键过期,就把键删掉。
lazy free 特性是 Redis 4.0 新增的⼀个⾮常使⽤的功能,它可以理解为延迟删除。意思是在删除的时候提供异步延时释放键值的功能,把键值释放操作放在 BIO(Background I/O) 单独的⼦线程处理中,以减少删除删除对 Redis 主线程的阻塞,可以有效地避免删除 big key 时带来的性能和可⽤性问题。
值相关知识点
Redis的value⽀持不同数据结构类型的值,以下是Redis value⽀持的数据结构的列表。
字符串
字符串的最⼤值是512M。
SET命令⽀持同时设置nx和px属性,因此可以⽤于单实例的分布式锁。
INCR命令可以将字符串键存储的整数值加上1,可以作为ID⽣成器
MSET和MGET命令代替多条SET和GET命令只需要⼀次⽹络通信,从⽽有效地减少程序执⾏多个设置和获取操作时的时间。
APPEND命令在键不存在时执⾏设置操作,在键存在时执⾏追加操作,可以⽤于追加存储⼀段时间内的⽇志。
列表
基于链表实现,内部数据结构是quickList
LPUSH命令从列表的左边添加数据,RPOP命令从列表的右边获取并且移除数据。因此可以⽤作消息队列。
LRANGE命令可以根据索引获取列表的数据,因此可以⽤作记录⽤户最新发布的内容ID。
哈希
底层以hashtable和ziplist实现
hash算法是MurmurHash算法,MurmurHash 是⼀种⾮加密型哈希函数,适⽤于⼀般的哈希检索操作。
扩容和缩容是根据负载因⼦的值进⾏判断的,负载因⼦=哈希表中已有元素和哈希桶数的⽐值。当负载因⼦⼤于1时,可能会扩容,如果⼤于1⼩于5并且没有进⾏
bgsave/bdrewrite操作会扩容,⼤于5就会⽴刻扩容。当负载因⼦⼩于0.1就会缩容。扩容和缩容时桶的数量都是指数变化的,并且会新建⼀个哈希表⽤于扩容和缩容。
哈希适合⽤来存储短⽹址ID与⽬标⽹址之间的映射。
集合
底层是intset或者hashtable(hashtable实现时,hashtable中key为集合的元素,value为null)
集合是⽆序的,并且没有重复数据。
SADD命令向集合添加新元素。
Redis提供的命令还可以对集合执⾏⼀些其他操作,⽐如测试给定元素是否已经存在,执⾏多个集合之间的交集、并集或差集等等。
有序集合
底层以ziplist或skiplist+hashtable来实现。
有序集合的每个元素都由⼀个成员和⼀个与成员相关联的分值组成,其中成员以字符串⽅式存储,⽽
分值则以64位双精度浮点数格式存储。同⼀个有序集合不能存储相同的成员,但不同成员的分值却可以是相同的。
有序集合是Redis提供的所有数据结构中最为灵活的⼀种,它可以以多种不同的⽅式获取数据,⽐如根据成员获取分值、根据分值获取成员、根据成员的排名获取成员、根据指定的分值范围获取多个成员等。
使⽤zrange和zrevrange命令可以获取升序或者降序的排⾏榜,使⽤zrank(正序)和zrevrank(从⼤到⼩)可以获取排名,使⽤zscore命令可以获取成员的分值,使⽤zincrby命令可以对有序集合中指定成员的分值执⾏⾃增操作或⾃减操作。
位图
位图不是实际的数据类型,⽽是在字符串类型上定义的⼀组⾯向位的操作。由于字符串是⼆进制安全的blob,其最⼤长度为512mb,因此适合设置为2的32次⽅个不同的位。
位图最⼤的优点之⼀是,它们在存储信息时通常可以极⼤地节省空间。例如,在不同⽤户由递增的⽤户id表⽰的系统中,仅使⽤512mb的内存就可以记住40亿⽤户的单个⽐特信息(例如,知道⽤户是否想要接收时事通讯)。
使⽤SETBIT和GETBIT可以在常量时间内设置和获取某⼀个位的值。使⽤BITCOUNT命令可以统计位图中值为1的⼆进制位数量。
HyperLogLog
HyperLogLog是⼀种概率数据结构,对于⼀个给定的集合,HyperLogLog可以计算出这个集合的近似基数:近似基数并⾮集合的实际基数,它可能会⽐实际的基数⼩⼀点或者⼤⼀点,但是估算基数和实际基数之间的误差会处于⼀个合理的范围之内,因此那些不需要知道实际基数或者因为条件限制⽽⽆法计算出实际基数的程序就可以把这个近似基数当作集合的基数来使⽤。
使⽤PFADD命令可以对给定的⼀个或多个集合元素进⾏计数, 如果已经计数返回0,没有计数返回1,因此可以⽤于检测重复信息。使⽤PFCOUNT命令可以为集合计算出的近似基数。使⽤PFMERGE命令可以对多个给定的HyperLogLog执⾏并集计算,然后把计算得出的并集HyperLogLog保存到指定的键中,因此可以对多个HyperLogLog实现的唯⼀计数器执⾏并集计算,从⽽实现每周/⽉度/年度计数器。
地理坐标
可以将经纬度格式的地理坐标存储到Redis中,并对这些坐标执⾏距离计算、范围查等操作。
Streams
Redis流是使⽤Redis实现消息队列应⽤的最佳选择。流是⼀个包含零个或任意多个流元素的有序队列,队列中的每个元素都包含⼀个ID和任意多个键值对,这些元素会根据ID的⼤⼩在流中有序地进⾏排列。当⽤户将符号*⽤作id参数的值时,Redis将⾃动为新添加的元素⽣成⼀个可⽤的新ID。
流元素的ID由毫秒时间(millisecond)和顺序编号(sequcen number)两部分组成,其中使⽤UNIX时间戳表⽰的毫秒时间⽤于标识与元素相关联的时间,⽽以0为起始值的顺序编号则⽤于区分同⼀时间内产⽣的多个不同元素。因为毫秒时间和顺序编号都使⽤64位的⾮负整数表⽰,所以整个流ID的总长为128位,⽽Redis在接受流ID输⼊以及展⽰流ID的时候都会使⽤连字符-分割这两个部分。通过将元素ID与时间进⾏关联,并强制要求新元素的ID必须⼤于旧元素的ID, Redis从逻辑上将流变成了⼀种只执⾏追加操作(append only)的数据结构,这种特性对于使⽤流实现消息队列和事件系统的⽤户来说是⾮常重要的:⽤户可以确信,新的消息和事件只会出现在已有消息和事件之后,就像现实世界⾥新事件总是发⽣在已有事件之后⼀样,⼀切都是有序进⾏的。
Redis流的消费者组(consumer group)允许⽤户将⼀个流从逻辑上划分为多个不同的流,并让消费者组属下的消费者去处理组中的消息。
⼀条消费者组消息从出现到处理完毕,需要经历以下阶段:不存在;未递送;待处理;已确认。
使⽤XADD命令可以将⼀个带有指定ID以及包含指定键值对的元素追加到流的末尾。
使⽤XDEL命令删除消息,这⾥的删除仅仅是设置了标志位,不影响消息总长度。
使⽤XRANGE命令可以获取消息列表,会⾃动过滤已经删除的消息。
使⽤XLEN命令消息长度。
Redis通信⽅式
请求响应
交互⽅式是将⼀个命令发送到服务器,等服务器执⾏完这个命令并将结果返回给客户端之后,再执⾏下⼀个命令。
pipeline
允许客户端把任意多条Redis命令请求打包在⼀起,然后⼀次性地将它们全部发送给服务器,⽽服务器则会在流⽔线包含的所有命令请求都处理完毕之后,⼀次性地将它们的执⾏结果全部返回给客户端。可以提⾼整个交互的性能。
事务
当EXEC中有⼀条请求执⾏失败时,后续请求继续执⾏,只在返回客户端的array型响应中标记这条出错的结果,由客户端的应⽤程序决定如何恢复,Redis⾃⾝不包含回滚机制(执⾏到⼀半的批量操作必须继续执⾏完)。回滚机制的缺失使得Redis的事务实现极⼤地简化:⽆须为事务引⼊数据版本机制,⽆须为每个操作引⼊逆向操作。所以严格地
讲,Redis的事务并不是⼀致的。
lua脚本
Lua脚本是以原⼦的⽅式执⾏的。
每⼀个提交到服务器端的lua脚本都会在服务器端的lua_script map中常驻,除⾮显式通过FLUSH命令清理;script在实例的主备间可通过script重放和cmd重放两种⽅式实现复制;之前执⾏过的script后续可直接通过它的sha指定⽽不⽤再向服务器端发送⼀遍script内容。
发布订阅
发布订阅的交互⽅式是⼀个客户端触发,多个客户端被动接收,通过服务器的中转。
使⽤PUBLISH命令可以将⼀条消息发送⾄给定频道。
使⽤SUBSCRIBE命令可以让客户端订阅给定的⼀个或多个频道。
发布与订阅虽然拥有将消息传递给多个客户端的能⼒,并且也拥有相应的阻塞弹出原语,但发布与订阅的“发送即忘(f ire andforget)”策略会导致离线的客户端丢失消息,所以它是⽆法实现可靠的消息队列的。如果 Redis 停机重启,PubSub 的消息是不会持久化的,毕竟 Redis 宕机就相当于⼀个消费者都没有,所有的消息都会被直接丢弃。
持久化
AOF和RDB,以及混合持久化⽅式
RDB是以全量的⽅式,持久化Redis的状态到rdb⽂件中,有save和bgsave两种⽅式,save和普通的redis命令执⾏⽅式⼀样。bgsave另起⼀个⼦线程的⽅式来异步的持久化。RDB 可能会导致⼀定时间内的数据丢失。
AOF是以增量的⽅式,持久化写命令到AOF⽂件⾥,有三种Always,Every Second和NO三种刷磁盘的策略。有rewrite机制优化AOF⽂件的⼤⼩。AOF 由于⽂件较⼤则会影响 Redis 的启动速度。
混合持久化⽅式,Redis 4.0 之后新增的⽅式,混合持久化是结合了 RDB 和 AOF 的优点,在写⼊的时候,先把当前的数据以 RDB 的形式写⼊⽂件的开头,再将后续的操作命令以 AOF 的格式存⼊⽂件,这样既能保证 Redis 重启时的速度,⼜能减低数据丢失的风险。
内存策略
当内存超过maxmemory限定时,触发主动清理策略,⼀共有⼋种淘汰策略
LRU淘汰
volatile-lru:从设置过期时间的数据集(server.db[i].expires)中挑选出最近最少使⽤的数据淘汰。没有设置过期时间的key不会被淘汰,这样就可以在增加内存空间的同时保证需要持久化的数据不会丢失。
allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使⽤的数据淘汰,该策略要淘汰的key⾯向的是全体key集合,⽽⾮过期的key集合。
TTL淘汰
volatile-ttl:除了淘汰机制采⽤LRU,策略基本上与volatile-lru相似,从设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰,ttl值越⼤越优先被淘汰。
随机淘汰
volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰。当内存达到限制⽆法写⼊⾮过期时间的数据集时,可以通过该淘汰策略在主键空间中随机移除某个key。
allkeys-random:从数据集(server.db[i].dict)中选择任意数据淘汰。
禁⽌淘汰
no-enviction:禁⽌驱逐数据,也就是当内存不⾜以容纳新⼊数据时,新写⼊操作就会报错,请求可以继续进⾏,线上任务也不能持续进⾏,采⽤no-enviction策略可以保证数据不被丢失,这也是系统默认的⼀种淘汰策略。
LFU淘汰
在 Redis 4.0 版本中⼜新增了 2 种淘汰策略:
1. volatile-lfu:淘汰所有设置了过期时间的键值中,最少使⽤的键值;
2. allkeys-lfu:淘汰整个键值中最少使⽤的键值。
分布式
主从复制
SYNC命令和PSYNC命令
Slave发送SYNC命令,Master借助BGSAVE命令⽣成快照信息,发送给Slave。发送期间的写命令存放在backlog, 在快照发送完成之后发送。之后写命令都实时发送给Slave。
在 Redis 2.8 中引⼊了 PSYNC 命令来代替 SYNC,它具有两种模式:
1. 全量复制: ⽤于初次复制或其他⽆法进⾏部分复制的情况,将主节点中的所有数据都发送给从节点,是⼀个⾮常重型的操作;
2. 部分复制: ⽤于⽹络中断等情况后的复制,只将 中断期间主节点执⾏的写命令 发送给从节点,与全量复制相⽐更加⾼效。需要注意 的是,如果⽹络中断时间过长,导致
主节点没有能够完整地保存中断期间执⾏的写命令,则⽆法进⾏部分复制,仍使⽤全量复制;
部分复制的原理主要是靠主从节点分别维护⼀个 复制偏移量,有了这个偏移量之后断线重连之后⼀⽐较,之后就可以仅仅把从服务器断线之后缺失的这部分数据(写命令)给补回来了。
哨兵
多个哨兵节点通过pubsub来进⾏交互形成集保证⾼可⽤,哨兵节点和Master节点通过定期⼼跳,当⼤于等于配置的Quorum的哨兵节点判断Master节点是否挂断,然后通过类似Raft协议选举主哨兵节点,最后根据条件选择⼀个Slave节点作为主节点。
集
Redis集通过分⽚来进⾏数据共享,并提供复制和故障转移功能。
集节点之间通过CLUSTER MEET命令进⾏握⼿,通过Gossip协议病毒式传播形成集。
Redis集通过分⽚的⽅式来保存数据库中的键值对:集的整个数据库被分为16384个槽(slot),数据库中的每个键都属于这16384个槽的其中⼀个,集中的每个节点可以处理0个或最多16384个槽。当数据库中的16384个槽都有节点在处理时,集处于上线状态(ok);相反地,如果数据库中有任何⼀个槽没有得到处理,那么集处于下线状态(fail)。计算公式:slot = CRC16(key) & 16383,每⼀个节点负责维护⼀部分槽以及槽所映射的键值数据。
集节点CLUSTER ADDSLOTS命令指定⾃⼰负责的槽信息,节点会通过发送消息告知集中的其他节点,⾃⼰⽬前正在负责处理哪些槽。
Redis集中的节点分为主节点(master)和从节点(slave),其中主节点⽤于处理槽,⽽从节点则⽤于复制某个主节点,并在被复制的主节点下线时,代替下线主节点继续处理命令请求。
当⼀个从节点发现⾃⼰正在复制的主节点进⼊了已下线状态时,从节点将开始对下线主节点进⾏故障转移。⾸先集中的所有主节点会选举⼀个从节点,选择算法都是基于Raft 算法,随机时间,配置纪元,过半选举成功。然后从节点成为主节点,并且指派已下线主节点的槽信息给⾃⼰。接着像集⼴播PONG消息。然后开始处理请求,完成故障转移。
集⾥的每个节点默认每隔⼀秒钟就会从已知节点列表中随机选出五个节点,然后对这五个节点中最长时间没有发送过PING消息的节点发送PING消息,以此来检测被选中的节点是否在线。除此之外,如果节点A最后⼀次收到节点B发送的PONG消息的时间,距离当前时间已经超过了节点A的cluster-node-timeout选项设置时长的⼀半,那么节点A也会向节点B发送PING消息,这可以防⽌节点A因为长时间没有随机选中节点B作为PING消息的发送对象⽽导致对节点B的信息更新滞后。
当节点接收到⼀个PUBLISH命令时,节点会执⾏这个命令,并向集⼴播⼀条PUBLISH消息,所有接收到这条PUBLISH消息的节点都会执⾏相同的PUBLISH命令。
集环境下,Lua脚本的所有 key,必须在 1 个 slot 上,否则直接返回 error。所有 key 都应该由 KEYS 数组来传递,redis.call/pcall ⾥⾯调⽤的 redis 命令,key 的位置,必须是 KEYS array, 否则直
接返回 error。
分布式锁
1. set命令同时设置NX属性和过期时间原⼦性获取锁,通过锁key和锁的随机值作为参数,使⽤lua脚本原⼦性判断值是否相等来删除锁,防⽌由于某个线程超时导致别的线
程的锁被误删除,从⽽导致锁失效。redis支持的数据结构
2. redission单实例锁会⾃动续期,可以更好的避免锁失效的问题, redission使⽤Lua脚本实现加锁和解锁,锁是可重⼊的,set命令的锁是不可重⼊的。
3. RedLock实现:顺序向五个节点请求加锁,根据⼀定的超时时间来推断是不是跳过该节点,三个节点加锁成功并且花费时间⼩于锁的有效期,认定加锁成功。
缓存实战问题
缓存雪崩
缓存同⼀时间⼤⾯积的失效,所以,后⾯的请求都会落到数据库上,造成数据库短时间内承受⼤量请求⽽崩掉。
解决的⽅案:
事前:尽量保证整个 redis 集的⾼可⽤性,发现机器宕机尽快补上。选择合适的内存淘汰策略。
事中:本地ehcache缓存 + hystrix限流&降级,避免MySQL崩掉
事后:利⽤ redis 持久化机制保存的数据尽快恢复缓存
缓存穿透
⼤量请求的 key 不存在,缓存中没有,导致请求直接到了数据库上,根本没有经过缓存这⼀层。
解决的⽅案:
1. 缓存⽆效 key : 如果缓存和数据库都查不到某个 key 的数据就写⼀个到 redis 中去并设置过期时间,⼀般设置5分钟。
2. 布隆过滤器:把所有可能存在的请求的值都存放在布隆过滤器中,当⽤户请求过来,会先判断⽤户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参
数错误信息给客户端,存在的话才会⾛下⾯的流程。
Cache-Aside pattern
失效:应⽤程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中;
命中:应⽤程序从cache中取数据,取到后返回;
更新:先把数据存到数据库中,成功后,再让缓存失效;
给缓存设有效时间可以保证最终⼀致性。
问题1:先更新数据库,再删除缓存。如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据就出现了不⼀致。
解决思路:先删除缓存,再更新数据库。如果数据库更新失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不⼀致。因为读的时候缓存没有,所以去读了数据库中
的旧数据,然后更新到缓存中。。
问题2:上亿流量⾼并发场景下数据发⽣了变更,先删除了缓存,然后要去修改数据库,此时还没修改。⼀个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改
前的旧数据,放到了缓存中。随后数据变更的程序完成了数据库的修改。数据库和缓存中的数据不⼀样了。
解决⽅案1:更新数据的时候,根据数据的唯⼀标识,将操作路由之后,发送到⼀个 jvm 内部队列中。读取数据的时候,如果发现数据不在缓存中,那么将重新执⾏“读取数据
+更新缓存”的操作,根据唯⼀标识路由之后,也发送到同⼀个 jvm 内部队列中。⼀个队列对应⼀个⼯作线程,每个⼯作线程串⾏拿到对应的操作,然后⼀条⼀条的执⾏。这样
的话,⼀个数据变更的操作,先删除缓存,然后再去更新数据库,但是还没完成更新。此时如果⼀个读请求过来,没有读到缓存,那么可以先将缓存更新的请求发送到队列中,
此时会在队列中积压,然后同步等待缓存更新完成。
解决⽅案2:采⽤双延时删除策略。在主从同步的延时时间基础上,加⼏百ms。
缓存删除失败的场景,可以通过消息队列进⾏重试。
参考衔接
1. wwwblogs/zhengcheng-java/p/11451507.html
2. 深⼊分布式缓存:从原理到实践第8章
3. Redis使⽤⼿册
4. Redis设计与实现
5. mp.weixin.qq/s/FxOwmWCGagL5pIdLBkr82A
6. mp.weixin.qq/s/-fk-cEIo3iDCUSwT_l8d2w
7. github/doocs/advanced-java/blob/master/docs/high-concurrency/redis-consistence.md
8. gitee/SnailClimb/JavaGuide/blob/master/docs/database/Redis/redis-
collection/Redis(9)%E2%80%94%E2%80%94%E9%9B%86%E7%BE%A4%E5%85%A5%E9%97%A8%E5%AE%9E%E8%B7%B5%E6%95%99%E7%A8%8B.md
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论