Redis不仅仅是一个简单的key-value内存数据库,Redis官网对自身的定义是“数据结构服务器”。通过用心设计各种数据结构类型的数据存储,可以实现部分的数据查询功能。因为在Redis的设计中,key是一切,对于Redis是可见的,而value对于Redis来说就是一个字节数组,Redis并不知道你的value中存储的是什么,所以要想实现比如
‘select from users where userlocation="shanghai"’
这样的查询,在Redis是没办法通过value进行比较得出结果的。但是可以通过不同的数据结构类型来做到这一点。比如如下的数据定义
users:1 {name:Jack,age:28,location:shanghai}
users:2 {name:Frank,age:30,location:beijing}
users:location:shanghai [1]
其中users:1 users:2 分别定义了两个用户信息,通过Redis中的hash数据结构,而users:location:shanghai 记录了所有上海的用户id,通过集合数据结构实现。这样通过两次简单的Redis命令调用就可以实现我们上面的查询。
Jedis jedis = jedisPoolgetResource();
Set<String> shanghaiIDs = jedissmembers("users:location:shanghai");
//遍历该set
//
//通过hgetall获取对应的user信息
jedishgetAll("users:" + shanghaiIDs[0]);
通过诸如以上的设计,可以实现简单的条件查询。但是这样的问题也很多,首先需要多维护一个ID索引的集合,其次对于一些复杂查询无能为力(当然也不能期望Redis实现像关系数据库那样的查询,Redis不是干这的)。
但是Redis26集成了Lua脚本,可以通过eval命令,直接在RedisServer环境中执行Lua脚本,并且可以在Lua脚本中调用Redis命令。其实,就是说可以让你用Lua这种脚本语言,对Redis中存储的key value进行 *** 作,这个意义就大了,甚至可以将你们系统所需的各种业务写成一个个lua脚本,提前加载进入Redis,然后对于请求的响应,只需要调用一个个lua脚本就行。当然这样说有点夸张,但是意思就是这样的。
比如,现在我们要实现一个‘所有age大于28岁的user’这样一个查询,那么通过以下的Lua脚本就可以实现
public static final String SCRIPT =
"local resultKeys={};"
+ "for k,v in ipairs(KEYS) do "
+ " local tmp = rediscall('hget', v, 'age');"
+ " if tmp > ARGV[1] then "
+ " tableinsert(resultKeys,v);"
+ " end;"
+ "end;"
+ "return resultKeys;";
执行脚本代码
Jedis jedis = jedisPoolgetResource();
jedisauth(auth);
List<String> keys = ArraysasList(allUserKeys);
List<String> args = new ArrayList<>();
argsadd("28");
List<String> resultKeys = (List<String>)jedisevalsha(funcKey, keys, args);
return resultKeys;
注意,以上的代码中使用的是evalsha命令,该命令参数的不是直接Lua脚本字符串,而是提前已经加载到Redis中的函数的一个SHA索引,通过以下的代码将系统中所有需要执行的函数提前加载到Redis中,我们的系统维护一个函数哈希表,后续需要实现什么功能,就从函数表中获取对应功能的SHA索引,通过evalsha调用就行。
String shaFuncKey = jedisscriptLoad(SCRIPT);//加载脚本,获取sha索引
funcTableput(funcName_age, shaFuncKey);//添加到函数表中
通过以上的方法,便可以使较为复杂的查询放到Redis中去执行,提高效率。
当同时满足以下条件时,使用ziplist编码:
SpringBoot—实现n秒内出现x个异常报警
思路:
借助Redis的zSet集合,score存储的是异常时的时间戳,获取一定时间范围内的set集合。判断set个数是否满足条件,若满足条件则触发报警;
注意点:
相关API:
Redis实现延迟队列方法介绍
基于Redis实现DelayQueue延迟队列设计方案
相关API:
SpringBoot2x—使用Redis的bitmap实现布隆过滤器(Guava中BF算法)
布隆过滤器: 是专门用来检测集合中是否存在特定元素的数据结构。
存在误差率: 即将不在集合的元素误判在集合中。
所以布隆过滤器适合查询准确度要求没这么苛刻,但是对时间、空间效率比较高的场景。
实现方式:Redis实现布隆过滤器——借鉴Guava的BF算法:
SpringBoot2x中使用Redis的bitmap结构(工具类)
注意:bitmap使用存在风险,若仅仅计算hash值,会导致bitmap占用空间过大。一般需要对hash值进行取余处理。
根据Redis是否存在key,判断锁是否被获取;
锁应该是一个对象,记录持有锁的线程信息、当前重入次数。所以应该使用Redis的Hash结构来存储锁对象。
31 网络波动造成释放锁失败怎么解决?
需要为锁加上超时时间;
32 任务未执行完毕时,锁由于超时时间被释放?
线程一旦加锁成功,可以启动一个后台线程,每隔多少秒检查一次,如果线程还持有锁,可以不断延长锁的生存时间。
主从切换时,从服务器上没有加锁信息,导致多个客户端同时加锁。
list结构底层是ziplist/quicklist(可看着一个双端队列)。常用命令:
使用list作为对象的缓存池。通过rpush放入对象,通过lpop取出对象。
若是阻塞取,可以使用blpop命令实现。
Redis和Lua脚本(实现令牌桶限流)
数据结构选择hash。
hash里面维护:最后放入令牌时间、当前桶内令牌量、桶内最大数量、令牌放置速度(元数据)。
被动式维护:
命令:incr原子累加;
对一段固定时间窗口内的请求进行计数,如果请求数超过了阈值,则舍弃该请求;如果没有达到设定的阈值,则接受该请求,且计数加1。当窗口时间结束,重置计数器为0。
优点:实现简单,容易理解;
缺点:流量曲线可能不够平滑,有“突刺现象”。
1 一段时间内(不超过时间窗口)系统服务不可用。 比如窗口大小1s,限流为100,恰好某个窗口第1ms来了100个请求,然后2ms-999ms请求都会被拒绝。这段时间用户会感觉系统服务不可用(即不够平滑)。
2 窗口切换时可能会出现两倍于阈值流量的请求。 比如窗口大小1s,限流大小100,然后在某个窗口的第999ms有100个请求,窗口前期没有请求。所以这100个请求都会通过。然后下一个窗口的第1ms又来100个请求,然后全部通过。其实也是1ms内通过的200个请求。
命令:Redis的incr命令
是对固定窗口计数器的优化,解决的是切换窗口两倍阈值流量的场景。
具体解决方案是:将限流窗口分为多个小的限流窗口,各个限流窗口分别计数。当前时间大于窗口最大时间时,将头部的小窗口数据舍弃,尾部新增小窗口来处理新请求。
优点:本质上是对固定窗口的优化
常见的Redis集群架构是三主三从的结构,为了保证数据分片,redis采用了Hash槽的概念,即:
常见的三主三从结构,将solt平均分到三个节点上
如果存入一个值,按照redis cluster哈希槽的 算法 : CRC16('key')384 = 6782。 那么就会把这个key 的存储分配到 B 上了。同样,当我连接(A,B,C)任何一个节点想获取'key'这个key时,也会这样的算法,然后内部跳转到B节点上获取数据
新增一个节点D,redis cluster的这种做法是从各个节点的前面各拿取一部分slot到D上,会变成这样:
同样删除一个节点也是类似,移动完成后就可以删除这个节点了。
Redis的Hash槽分配不是 一致性Hash ,一致性Hash是成一个hash环,当节点加入或者失效的时候,在环上顺时针找到对应节点。而Redis集群属于手动分配 线性Hash槽 ,需要手动指定,并且尽量做到各个节点solt平均分配。
而至于为什么Redis没有采用一致性Hash,因为如果一个节点失效,把数据转移到下一个节点,容易造成缓存雪崩,而采用hash槽+副本节点失效的时候从节点自动接替,不易造成雪崩。
Redis 集群中内置了 16384 个哈希槽,当需要在 Redis 集群中放置一个 key-value
时,redis 先对 key 使用 crc16 算法算出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,redis 会根据节点数量大致均等的将哈希槽映射到不同的节点。Redis 集群没有使用一致性hash, 而是引入了哈希槽的概念。
Redis 集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽集群的每个节点负责一部分hash槽。这种结构很容易添加或者删除节点,并且无论是添加删除或者修改某一个节点,都不会造成集群不可用的状态。使用哈希槽的好处就在于可以方便的添加或移除节点。当需要增加节点时,只需要把其他节点的某些哈希槽挪到新节点就可以了;当需要移除节点时,只需要把移除节点上的哈希槽挪到其他节点就行了;在这一点上,我们以后新增或移除节点的时候不用先停掉所有的 redis 服务。
"用了哈希槽的概念,而没有用一致性哈希算法,不都是哈希么?这样做的原因是为什么呢?
"Redis Cluster是自己做的crc16的简单hash算法,没有用一致性hash。Redis的作者认为它的crc16(key) mod 16384的效果已经不错了,虽然没有一致性hash灵活,但实现很简单,节点增删时处理起来也很方便。"为了动态增删节点的时候,不至于丢失数据么?"节点增删时不丢失数据和hash算法没什么关系,不丢失数据要求的是一份数据有多个副本。“还有集群总共有2的14次方,16384个哈希槽,那么每一个哈希槽中存的key 和 value是什么?”当你往Redis Cluster中加入一个Key时,会根据crc16(key) mod 16384计算这个key应该分布到哪个hash slot中,一个hash slot中会有很多key和value。你可以理解成表的分区,使用单节点时的redis时只有一个表,所有的key都放在这个表里;改用Redis Cluster以后会自动为你生成16384个分区表,你insert数据时会根据上面的简单算法来决定你的key应该存在哪个分区,每个分区里有很多key。
ziplist 编码的哈希对象使用压缩列表作为底层实现, 每当有新的键值对要加入到哈希对象时, 程序会先将保存了键的压缩列表节点推入到压缩列表表尾, 然后再将保存了值的压缩列表节点推入到压缩列表表尾, 因此:
保存了同一键值对的两个节点总是紧挨在一起, 保存键的节点在前, 保存值的节点在后;
先添加到哈希对象中的键值对会被放在压缩列表的表头方向, 而后来添加到哈希对象中的键值对会被放在压缩列表的表尾方向。
举个例子, 如果我们执行以下 HSET 命令, 那么服务器将创建一个列表对象作为 profile 键的值:
另一方面, hashtable 编码的哈希对象使用字典作为底层实现, 哈希对象中的每个键值对都使用一个字典键值对来保存:
Redis 的字典使用哈希表作为底层实现, 一个哈希表里面可以有多个哈希表节点, 而每个哈希表节点就保存了字典中的一个键值对。
Redis 字典所使用的哈希表由 dicth/dictht 结构定义:
table 属性是一个数组, 数组中的每个元素都是一个指向 dicth/dictEntry 结构的指针, 每个 dictEntry 结构保存着一个键值对。
size 属性记录了哈希表的大小, 也即是 table 数组的大小, 而 used 属性则记录了哈希表目前已有节点(键值对)的数量。
sizemask 属性的值总是等于 size - 1 , 这个属性和哈希值一起决定一个键应该被放到 table 数组的哪个索引上面。
图 4-1 展示了一个大小为 4 的空哈希表 (没有包含任何键值对)。
哈希表节点使用 dictEntry 结构表示, 每个 dictEntry 结构都保存着一个键值对:
key 属性保存着键值对中的键, 而 v 属性则保存着键值对中的值, 其中键值对的值可以是一个指针, 或者是一个 uint64_t 整数, 又或者是一个 int64_t 整数。
next 属性是指向另一个哈希表节点的指针, 这个指针可以将多个哈希值相同的键值对连接在一次, 以此来解决键冲突(collision)的问题。
举个例子, 图 4-2 就展示了如何通过 next 指针, 将两个索引值相同的键 k1 和 k0 连接在一起。
Redis 中的字典由 dicth/dict 结构表示:
type 属性和 privdata 属性是针对不同类型的键值对, 为创建多态字典而设置的:
ht 属性是一个包含两个项的数组, 数组中的每个项都是一个 dictht 哈希表, 一般情况下, 字典只使用 ht[0] 哈希表, ht[1] 哈希表只会在对 ht[0] 哈希表进行 rehash 时使用。
除了 ht[1] 之外, 另一个和 rehash 有关的属性就是 rehashidx : 它记录了 rehash 目前的进度, 如果目前没有在进行 rehash , 那么它的值为 -1 。
图 4-3 展示了一个普通状态下(没有进行 rehash)的字典:
在Redis中,由于它对实时性要求更高,因此使用了渐进式rehash
当有新键值对添加到Redis字典时,有可能会触发rehash。Redis中处理哈希碰撞的方法与Java一样,都是采用链表法,整个哈希表的性能则依赖于它的大小size和它已经保存节点数量used的比率。
比率在1:1时,哈希表的性能最好,如果节点数量比哈希表大小大很多的话,则整个哈希表就退化成多个链表,其性能优势全无。
上图的哈希表,平均每次失败查找需要访问5个节点。为了保持高效性能,在不修改键值对情况下,
需要进行rehash,目标是将ratio比率维持在1:1左右。
Ratio = Used / Size
rehash触发条件:
rehash执行过程:
Redis哈希为了避免整个rehash过程中服务被阻塞,采用了渐进式的rehash,即rehash程序激活后,并不是
马上执行直到完成,而是分多次,渐进式(incremental)的完成。同时,为了保证并发安全,在执行rehash
中间执行添加时,新的节点会直接添加到ht[1]而不是ht[0], 这样保证了数据的完整性与安全性。
另一方面,哈希的Rehash在还提供了创新的(相对于Java HashMap)收缩(shrink)字典,当可用节点远远
大于已用节点的时候,rehash会自动进行收缩,具体过程与上面类似以保证比率始终高效使用。
当哈希对象可以同时满足以下两个条件时, 哈希对象使用 ziplist 编码:
Redis基于Request/Response 协议。通常情况下一次请求的步骤如下
1 客户端通过tcp 协议发送指令到服务端,等待redis服务端返回结果
2 redis服务端执行命令,返回响应到客户端。
客户端发送请求到接受到服务端返回的响应被称为一次RTT(Round Trip Time) 在不考虑redis 服务端处理耗时的情况下,数据传输的耗时是最要的开销。且redis client 必须等待上一次响应结果才能发送下一个指令,会长时间处于阻塞的状态。
为解决提升reids服务端的吞吐量,redis提供了如下几种解决方案。
redis实现了HMGET/HMSET/MSET/MGET等一系列批量指令。区别与普通指令如GET, MGET可以一次性发送key数组,然后redis服务端一次性返回结果数组。
很明显这种方式可以使用一次RTT处理完多个指令。这种方式的在性能上是最好的,缺点在于
1 指令类型必须一致,批量指令依赖于Redis的实现,有些指令如setbit 没有批量实现的,就无法使用这种方案。
2 不能混合指令发送,需要发送的指令必须在一次请求中确定。灵活性比pipeline差。
pipelining 是Request/Reponse协议的一种实现方式。客户端连续发送请求,而不用等待上一次的response返回。最终通过一次读取之前所有的Response。Redis服务支持这种实现方式。
我们通过redis-benchmark对比下普通cs模型和pipleline的性能。
从上面测试结果可以看出pipleline的性能远高于普通的请求方式。
1 pipeline的RTT交互次数,从而减少延迟,在网络延迟比较大的环境下。吞吐量提高会特别明显。
2 redis客户端/服务端减少了sys_call调用次数,减少了用户态到内核态的切换开销。
3 redis客户端发送无需等待上一次response请求结果,减少了阻塞时间。
Pipelining is not just a way in order to reduce the latency cost due to the round trip time, it actually improves by a huge amount the total operations you can perform per second in a given Redis server This is the result of the fact that, without using pipelining, serving each command is very cheap from the point of view of accessing the data structures and producing the reply, but it is very costly from the point of view of doing the socket I/O This involves calling the read() and write() syscall, that means going from user land to kernel land The context switch is a huge speed penalty
Redis-Bloomfilter在100版本时, *** 作redis是基于标准的request/response模式, 但是Bloomfilter 在精度和预计插入数量大的情况,需要做多次hash *** 作。这样一次bloomfilter *** 作需要进行几十次redis setbit/getbit。这种情况下会严重影响Bloomfilter的吞吐量。由于setbit/getbit 不支持批量 *** 作,所以采用pipeline来优化redis的性能开销。具体可以参考 >
以下内容来源官网:
>
以上就是关于hash类型的redis怎样实现联合查询全部的内容,包括:hash类型的redis怎样实现联合查询、Redis使用bitmap、zset、hash、list等结构完成骚 *** 作、Redis - 集群Hash槽分配等相关内容解答,如果想了解更多相关内容,可以关注我们,你们的支持是我们更新的动力!
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)