15、Redis Stack扩展功能实战

 、了解Redis产品

⽬前,在Redis的官⽹上, 可以看到Redis已经包含了多个产品。

其中, Redis CloudRedis的云服务, Redis InsightRedis官⽅推出的图形化客户端 。解决了Redis客户端群 龙⽆⾸的囧境。

Redis本身,也已经划分成了⼏个版本 Redis OSS就是我们之前⽤的Redis Redis Stack可以理解为是 Redis加上—系列的扩展产品 Redis EnterpriseRedis的企业版。

这次我们就—起来体验—下Redis Stack的扩展功能。

  申请RedisCloud实例 

 

Redis Stack可以在我们之前安装的Redis服务上, ⾃⾏下载安装新的扩展模块 。在⽬前阶段,在RedisCloud 可以申请—个免费的RedisStack实例,快速体验Redis Stack的功能。

Redis官⽹的右上角,就有Redis Cloud的登录链接 。⽬前Redis Cloud提供了多种第三⽅登录的⽅式,可以 选择合适的⽅式注册账号。

注册登录后, Redis Cloud就会分配—个免费的Redis实例 。提供了Redis Stack功能⽀持。

 接下来使⽤命令⾏ ,就可以连上这个Redis实例

 

三、Redis Stack体验

1 RedisStack有哪些扩展?

⽬前Redis的官⽹上, 单独构建了Redis的指令⻚⾯ 。在这个⻚⾯可以直接搜索相关的功能。

 

另外, 在redis-cli客户端也可以使⽤ module list指令查看当前Redis服务中有哪些扩展。

Redis Stack的这些扩展功能,也可以⼿动添加到⾃⼰的Redis服务中 。但是通常并不是必须的 。我们可以使⽤ Redis Cloud上的实例先完整体验—下,再考虑要不要使⽤这些扩展。

接下来找⼏个⽐较常见的扩展模块,体验—下。

2 Redis JSON

(1)Redis JSON是什么

RedisJSONRedis的—个扩展模块, 它提供了对JSON数据的原⽣⽀持 。通过RedisJSON ,我们可以将JSON 数据直接存储在Redis中,并利⽤丰富的命令集进⾏⾼效的查询和操作 。RedisJSON不仅简化了数据处理的流 程, 还⼤幅提升了处理JSON数据的性能。 

(2)Redis JSON有什么⽤

Redis JSON的常⽤指令,在官⽹的Commands页⾯搜索JSON组就能看到

--在Redis服务端, 这些扩展指令并没有严格分组,⽽是都放在—个叫做module的组

redis-17998.c295.ap-southeast-1-1.ec2.redns.redis-cloud.com:17998> help JSON.SET

JSON.SET (null)
summary: (null)
group: module

 Redis JSON模块为Redis添加了JSON数据类型的⽀持,并且对JSON数据提供了快速进⾏增 、删 、改 、查的操作。

-- 设置—个JSON数据
JSON.SET user $ '{"name":"loulan","age":18} '
## key是user, value就是—个JSON数据 。其中$表示JSON数据的根节点。
-- 查询JSON数据
JSON.GET user
-- 查询JSON对象的name属性 JSON.GET user $.name
-- 查看数据类型
JSON.TYPE user    -- object
JSON.TYPE user $.name   --- string
JSON.TYPE user $.age    --- integer
--修改JSON数据  年龄加2
JSON.NUMINCRBY user $.age 2
-- 添加新的字段
JSON.SET user $.address '{"city": "Changsha", "country": "China"} ' NX ## NX 表示只有当address字段不存在的时候才进⾏设置。
-- 在JSON数组中添加元素
JSON.SET user $.hobbies ' ["reading"] '
JSON.ARRAPPEND user $.hobbies '"swimming" ' -- 查看JSON对象中key的个数
JSON.OBJLEN user $.address


-- 查看user对象的所有key
JSON.OBJKEYS user

-- 删除JSON中的key
JSON.DEL user $.address

(3)Redis JSON的优势

JSON是现代应⽤程序中经常⽤到的—种数据类型 。很多时候,就算没有Redis JSON插件,我们也会采⽤JSON格式来缓存复杂的数据类型 。⽐如在分布式场景下做⽤户登录功能,我们就可以将⽤户信息以JSON字符 串的形式保存到Redis中,来代替单体应⽤中的Session ,从⽽实现统—的登录状态管理 。这些数据使⽤Redis   JSON插件来管理,就显得顺理成章了。

并且Redis JSON插件相⽐⽤string管理这种JSON数据, 还能带来—些很明显的优势。

  • .  Redis JSON存储数据的性能更⾼ Redis JSON底层其实是以—种⾼效的⼆进制的格式存储 。相⽐简单的⽂本格式,⼆进制格式进⾏JOSN格式读写的性能更⾼ ,也更节省内存 。根据官⽹的性能测试报告,使⽤  Redis JSON读写JSON数据,性能已经能够媲美MongoDB以及ElasticSearch等传统NoSQL数据库。
  • .  Redis JSON使⽤树状结构来存储JSON 。这种存储⽅式可以快速访问⼦元素 。与传统的⽂本存储⽅案相  ,树状存储结构能够更⾼效的执⾏查询操作。
  • .  Redis⽣态集成度⾼ 。作为Redis的扩展模块, Redis JSONRedis的其他功能和⼯具⽆缝集成 。这意味 着开发者可以继续使⽤TTL Redis事务 、发布/订阅 Lua脚本等功能。

3 Search And Query 

Redis中存储的数据⽐较多时,搜索Redis中的数据是—件⽐较麻烦的事情 。通常使⽤的 keys * 这样的指令,在⽣产环境—般都是直接禁⽤的, 因为这样会产⽣严重的线程阻塞,影响其他的读写操作。

如何快速搜索Redis中的数据(主要是key)呢? Redis中原⽣提供了Scan指令, 另外在Redis Stack中也增加了 Search And Query模块。

(1)传统Scan搜索

Scan指令的官⽅介绍:SCAN | Docs

Scan指令的基础思想就是每次只返回想要查询的—部分结果数据,然后通过迭代的⽅式,逐步返回完整数据。 scan指令的基础使⽤⽅式:

 SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]

这⼏个核⼼参数介绍如下:

        ·cursor: 游标 。代表每次迭代返回的偏移量 。通常—次查询,cursor0开始,然后scan令会返回下—次 迭代的起始偏移量 。⽤户可以⽤这个返回值作cursor ,继续迭代下—批 。直到cursor返回0 ,表示所有数据都过滤完成了。

        ·pattern: 匹配字符串 。⽤来匹配要查询的key  例如 user* 表示以user开头的字符串。

        ·count:数字,表示每次迭代多少条数据。

        ·type是key的类型, ⽐如可以指定string ,set,zset等。

 另外,针对不同key类型, 还有—些不同的指令 。 ⽐如 SSCAN针对Set类型 HSCAN针对HASH类型。 ZSCAN针对ZSet类型。

-- 准备数据
eval I for i = 1,30,1 do redis.call("SET","k"..tostring(i),"v"..tostring(i)) end I 0

--简单按照cursor过滤所有key。
scan 0
1) 18      ## 下—次迭代的cursor
....
scan 18
1) 21
....
scan 21
1) 0       ## 返回0表述所有数据过滤完成
....


-- 按照patern过滤  查询所有k开头的key
scan 0 MATCH k*
1) 18
...
scan 18 MATCH k*
1) 21
...
scan 21 MATCH k*
1) 0

-- 设置迭代次数
scan 0 MATCH k* count 20
1) 21
...
scan 21 MATCH k* count 20
1) 0

 

(2)Search And Query搜索

传统的SCAN搜索⽅式, 只能简单的过滤Key 。如果想要做—些复杂的搜索,就⼒不从⼼了。

⽐如在电商场景中,我们通常会⽤Redis来缓存商品信息,但是如果要做按品牌 、型号 、价格等等各种条件过滤商品的场景, Redis就不够⽤了 。以往我们会选择将商品数据导⼊到MongoDB或者ElasticSearch这样的搜索引擎进⾏复杂过滤。

Redis提供了RedisSearch插件,基本就可以认为是ElasticSearch这类搜索引擎的平替 。⼤部分ES能够实现 的搜索功能,在Redis⾥就能直接进⾏ 。这样就极⼤的减少了数据迁移带来的麻烦。

既然要做搜索,那就需要有能够⽀持搜索的数据结构 Redis的哪些数据结构能够⽀持结构化查询呢?只有 HASHJSON

 

--清空数据
flushall
-- 创建—个产品的索引
FT.CREATE productIndex ON JSON SCHEMA $.name AS name TEXT $.price AS price NUMERIC
## 索引为productIndex.
## ON JSON 表示  这个索引会基于JSON数据构建, 需要RedisJSON模块的⽀持 。默认是ON HASH 表示检索所有HASH格 式的数据
## SCHEMA表示根据哪些字段建⽴索引 。  字段名  AS 索引字段名  数据类型  这样的组合 。如果是JSON格式, 字段名⽤$.
路径表示
-- 模拟—批产品信息
JSON.SET phone:1 $ I{"id":1,"name":"HUAWEI 1","description":"HUAWEI PHONE
1","price":1999} I
JSON.SET phone:2 $ I{"id":2,"name":"HUAWEI 2","description":"HUAWEI PHONE
2","price":2999} I
JSON.SET phone:3 $ I{"id":3,"name":"HUAWEI 3","description":"HUAWEI PHONE
3","price":3999} I
JSON.SET phone:4 $ I{"id":4,"name":"HUAWEI 4","description":"HUAWEI PHONE
4","price":4999} I
JSON.SET phone:5 $ I{"id":5,"name":"HUAWEI 5","description":"HUAWEI PHONE
5","price":5999} I
JSON.SET phone:6 $ I{"id":6,"name":"HUAWEI 6","description":"HUAWEI PHONE
6","price":6999} I
JSON.SET phone:7 $ I{"id":7,"name":"HUAWEI 7","description":"HUAWEI PHONE
7","price":7999} I
JSON.SET phone:8 $ I{"id":8,"name":"HUAWEI 8","description":"HUAWEI PHONE
8","price":8999} I
JSON.SET phone:9 $ I{"id":9,"name":"HUAWEI 9","description":"HUAWEI PHONE
9","price":9999} I
JSON.SET phone:10 $ I{"id":10,"name":"HUAWEI 10","description":"HUAWEI PHONE
10","price":19999} I

## 如果是ON HASH , 可以直接通过索引添加数据
## FT.ADD productIndex I product:1 I  1.0 FIELDS "id" 1 "name" "HUAWEI1" "description"
"HUAWEI PHONE 1" "PRICE" 3999
## 数据的key 是producr:1
## FIELDS 数据 。  按照  key value 的格式组织
-- 查看索引状态
FT.INFO productIndex

-- 搜索产品
## 搜索条件 : name包含  HUAWEI,   price在1000到5000之间 。返回id和name
FT.SEARCH productIndex "@name:HUAWEI @price: [1000 5000]" RETURN 2 id name
## 查询条件构建, 参⻅官⽹  https://redis.io/docs/latest/develop/interact/search-and- query/query/

4 Bloom Filter

(1)布隆过滤器是什么

—句话解释:—种快速检索—个元素是否在—个海量集合中的算法。

⽐如现在有—个签到活动,要求每个⽤户只能签到—次, 重复签到⽆效 。这种常⻅的需求怎么做?如果不考虑 数量级,那么⾮常简单 。把所有签到的⽤户ID存到—个集合⾥ 。签到前到集合⾥检查—下⽤户ID有没有就可以 了。

但是,如果你要⾯对的是淘宝的海量⽤户信息呢?这个集合得要多⼤?在—个海量集合⾥检索—个数据, 是不是很慢?这就需要—个更节省空间同时更⾼效的算法,能够在海量数据集合中快速判断—个元素存不存在 。这 就可以⽤布隆过滤器。

布隆过滤器的使⽤场景⾮常多, 最典型的应⽤是作为缓存数据的前端过滤缓存 。快速⽐如,在淘宝这种海量⽤ 户的登录场景,也可以⽤布隆过滤器,快速判断⽤户输⼊的⽤户名是不是存在 。如果⽤户名不存在,那么就可以直接拒绝,不再需要去数据库⾥查了 。这样就可以防⽌⼤部分⽆效数据查询,屏蔽很多恶意的请求。

布隆过滤器使⽤—个很⻓的⼆进制位数组和—系列哈希函数来保存元素 。优点是⾮常节省空间,并且查询时间 也⾮常快 。缺点是有—定的误失败概率以及⽆法删除元素 ,也⽆法给元素计数。

  • · 位数组( Bit Array):布隆过滤器使⽤—个⻓度固定的位数组来存储数据 。每个位置只占⽤—个⽐特( 0 1), 初始时所有位都设置为0 。位数组的⻓度和哈希函数的数量决定了过滤器的误报率和容量。
  •  · 哈希函数集合:布隆过滤器使⽤多个哈希函数,每个函数都会将输⼊数据映射到位数组的—个不同位置  哈希函数的选择对过滤器的性能有很⼤影响,理想的哈希函数应该具有良好的散列性,使得不同的输⼊尽 可能均匀地映射到位数组的不同位置。

 

布隆过滤器判断—个元素不在集合中,那么这个元素肯定不在集合中 。但是,布隆过滤器判断—个元素在集合 中,那么这个元素有可能不在集合中 。

(2)Guava的布隆过滤器示例

布隆过滤器中,将—个原本不在集合中的元素判断成为在集合中, 这就是误判 。⽽误判率是布隆过滤器—个很 重要的控制指标。

在算法实现时,误判率是可以通过设定更复杂的哈希函数组合以及做更⼤的位数组来进⾏控制的 。所以,在布 隆过滤器的初始化过程中,通常只需要指定过滤器的容量和误判率,就⾜够了。


<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>33.1.0-j re</version>
</dependency>

 使⽤Guava提供的布隆过滤器实现

 

public static void main(String [] args) {
BloomFilter<String> bloomFilter =
BloomFilter.create(Funnels.stringFunnel(StandardCharsets.UTF_8),10000,0.01);
//把  A~Z 放⼊布隆过滤器
for (int i = 64; i <= 90 ; i++) {
bloomFilter.put(String.valueOf((char) i));
}
System.out.println(bloomFilter.mightContain("A")); //true
System.out.println(bloomFilter.mightContain("a")); //false
}

 

(3)RedisBloomFilter使⽤示例

布隆过滤器是⽤的⼆进制数组来保存数据,所以, RedisBitMap数据结构天⽣就⾮常适合做—个分布式的布 隆过滤器底层存储 。只是算法还是需要⾃⼰实现 。有很多企业实际上也是这么做的。

现在Redis提供了BloomFilter模块后, BloomFilter的使⽤⻔槛就更低了。

 

-- 创建—个key为bf的布隆过滤器, 容错率0.01, 容量1000。NONSCALING 表示不扩容 。如果这个过滤器⾥的数据满 了, 就直接报错
BF.RESERVE bf 0.01 1000 NONSCALING
-- 添加元素
BF.ADD bf A
.....
-- 批量添加元素
BF.MADD bf B C D E F G H I

-- 如果bf不存在, 就创建—个key为bf的过滤器。
BF.INSERT bf CAPACITY 1000 ERROR 0.01 ITEMS hello
-- 查看容量
BF.CARD bf
-- 判断元素是否在过滤器中
## 返回值0表示不在, 1表示在
BF.EXISTS bf a
-- 批量判断
BF.MEXISTS bf A a B b
-- 查看布隆过滤器状态
BF.INFO bf
# 依次迭代布隆过滤器中的位数组
BF.SCANDUMP bf 0
## 和SCAN指令使⽤很像, 返回当前访问到的数据和下—次迭代的起点 。  当下次迭代起点为0表示数据已经全部迭代完 成。
## 主要是可以配合BF.LOADCHUNK 进⾏备份。

5 Cuckoo Filter

(1)CuckooFilter是什么?

布隆过滤器最⼤的问题是⽆法删除数据 。因此,后续诞⽣了很多布隆过滤器的改进版本 Cuckoo Filter 布⾕鸟 过滤器就是其中—种。

相⽐于布隆过滤器, Cuckoo Filter可以删除数据 。⽽且基于相同的集合和误报率, Cuckoo Filter通常占⽤空间 更少 。相对的, 算法实现也就更复杂。

不过他同样有误判率 。即有可能将—个不在集合中的元素错误的判断成在集合中 。布隆过滤器的误报率通过调 整位数组的⼤⼩和哈希函数来控制,⽽CuckooFilter的误报率受指纹⼤⼩和桶⼤⼩控制。

 

BUSKETSIZE ,表示每个桶Busket中存放的元素个数 Cuckoo Filter的数组⾥存的不是位,⽽是桶busket ,每个桶⾥可以存放多个数据 。同—个桶中存放的数据越多, 空间利⽤率更⾼ ,相应的误判率也就 越⾼ ,性能也更慢 RedisCuckooFilter实现中, BUSKETSIZE应该是—个在1255之间的整数,默认   BUSKETSIZE2

Busket中并不实际保存数据本身,⽽是保存数据的指纹(可以认为是压缩后的数据, 实际上是数据对象 的⼏个低位数据) 。指纹越⼩, HASH冲突造成误判的⼏率就越⼩ 。这个参数的调整⽐较复杂, Redis    CuckooFilter中不⽀持调整这个参数。

(2)CuckooFilter使⽤示例 

-- 创建默认值
## 容量1000, 这个是必填参数 。后⾯⼏个都是可选参数 。这⾥填的⼏个就是Redis中的CuckooFilter的默认值
## BUSKETSIZE越⼤, 空间利⽤率更⾼ ,但是误判率也更⾼ ,性能更差
## MAXITARATIONS越⼩ ,性能越好 。如果设置越⼤, 空间利⽤率就越好。
## EXPANSION 是指空间扩容的⽐例。
CF.RESERVE cf 1000 BUSKETSIZE 2 MAXITERATIONS 20 EXPANSION 1

 他使⽤ ,和布隆过滤器差不多 。就是多了个CF.DEL删除元素的指令。

 四、Redis Stack补充

1 、⼿动安装Redis扩展模块

Redis Stack的这些扩展模块除了在Redis Cloud上直接使⽤外,也可以⼿动集成到⾃⼰的Redis服务当中。

登录Redis Cloud 进⼊下载中⼼ ,可以⼿动下载对应的扩展模块 。下载时注意选择对应的Redis版本以及操作系统。

 

这是—个最简单的下载途径 。另外更建议的⽅式, 是去下载这些扩展模块的源码,然后编译, 安装。

 下载后获取以.so为后缀的扩展⽂件 。上传到服务器后,在Redis的配置⽂件中加载这个扩展模块

 


# Load modules at startup. If the server is not able to load modules
# it will abort. It is possible to use multiple loadmodule directives.
#
loadmodule /root/my redis/redisbloom.so

然后重启Redis服务,就可以使⽤客户端,登录Redis后查看扩展模块的加载情况。

127.0.0.1:6379> MODULE LIST

1.
1.  "name"
2.  "bf"
3.  "ver"
4.  (integer) 20612
5.  "path"
6.  "/root/my redis/redisbloom.so"
7.  "args"
8.  (empty array)

注意:如果模块加载错误,那么Redis服务启动会失败的 。这时要去查看⽇志逐步排查问题。

例如,如果redis bloom.so⽂件在Linux服务器上,没有添加可执⾏x 权限,那么Redis就会启动失败

27425:M 18 Jun 2024 14:07:29.670 # Module /root/my redis/redisbloom.so failed to load: It does not have execute permissions.

27425:M 18 Jun 2024 14:07:29.670 # Can 't load module from /root/my redis/redisbloom.so: server aborting

2 Java客户端调⽤扩展模块

这些扩展模块⽬前阶段都还是⽐较新的功能, 需要⼿动进⾏扩展 。所以⽬前Java的—些客户端⼯具都还没有集  成这些功能 。⼤部分情况下, 只能通过lua脚本⼿动调⽤这些扩展功 。但是由于在客户端⽆法确定服务端是否 安装了对应的扩展模块,所以,在写lua脚本调⽤时,—定要注意处理好各种各样的异常情况。

例如, 以布隆过滤器为例

@SpringBootTest
@RunWith(SpringRunner.class)  public class RedisStackTest {

@Resource
RedisTemplate<String,Object> redisTemplate;
List<String> keys = List.of("a-bf");
@Test
public void createBloomFilter(){
if( !redisTemplate.hasKey("a-bf")){
try{
String createFilterScriptText= """
return redis.call( 'BF.RESERVE ', KEYS [1], '0.01 ', '1000 ', 'NONSCALING ') """
DefaultRedisScript<String> redisScript = new DefaultRedisScript<>
(createFilterScriptText, String.class);
String execute = redisTemplate.execute(redisScript,keys);
System.out.println("CREATE BF:"+execute);
}catch (Exception e){
//               e.printStackTrace();
System.out.println("COMMAND NOT SUPPORT");
}
}else{
System.out.println("BF KEY is already exists");
}
}

@Test
public void addData(){
if( !redisTemplate.hasKey("a-bf")){
System.out.println("BF KEY is not exists");
}else{
try{
String [] args = new String []{"A","B","C","D","E","F","G"};
String addDataScriptText= """
for i,arg in ipairs(ARGV) do
local addRes = redis.call( 'BF.ADD ',KEYS [1],arg)
end
return 'OK '
"""
DefaultRedisScript<String> redisScript = new DefaultRedisScript<>
(addDataScriptText, String.class);
System.out.println("ADDDATA BF:"+redisTemplate.execute(redisScript,  keys,
args));
}catch (Exception e){
//                e.printStackTrace();
System.out.println("COMMAND NOT SUPPORTED");
}
}
}

@Test
public void checkData(){
if( !redisTemplate.hasKey("a-bf")){
System.out.println("BF KEY is not exists");
}else{
String [] args = new String []{"A","B","C","D","E","F","G"};
String checkDataScriptText= """

local checkRes = redis.call( 'BF.EXISTS ',KEYS [1],ARGV [1]) return checkRes
"""
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>
(checkDataScriptText, Long.class);
try{
Long res = redisTemplate.execute(redisScript, keys, args);
if(1L == res){
System.out.println("KEY EXISTS");
} else if (0L==res) {
System.out.println("KEY NOT EXISTS");
}else{
System.out.println("ERROR");
}
}catch (Exception e){
//                e.printStackTrace();
System.out.println("COMMAND NOT SUPPORTED");
}
}
}
}

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值