Redis 命令及数据类型 -- JSON

摘要

JSON vs String

  • Redis JSON存储数据的性能更高。

    • Redis JSON 底层其实是以一种高效的二进制的格式存储。
    • 相比简单的文本格式,二进制格式进行 JOSN 格式读写的性能更高,也更节省内存。
    • 根据官网的性能测试报告,使用 Redis JSON 读写 JSON数据,性能已经能够媲美 MongoDB 以及 ElasticSearch 等传统 NoSQL 数据库。
  • Redis JSON 使用树状结构来存储JSON。

    • 这种存储方式可以快速访问子元素。
    • 与传统的文本存储方案相比,树状存储结构能够更高效的执行查询操作。
  • 与Redis生态集成度高。

    • 作为Redis的扩展模块,Redis JSON 和Redis的其他功能和工具无缝集成。
    • 这意味着开发者可以继续使用TTL、Redis事务、发布/订阅、Lua脚本等功能。

JSONPath

  • JSONPath 是一种用于查询和修改 JSON 数据的语法。

  • JSONPath 语法

语法元素 中文说明 示例
$ 根节点(最外层 JSON 元素),JSONPath 的起点 $$.user.name
. 访问子字段(对象属性访问) $.user.age
[] 子元素选择器(字段名或数组索引) $['user']['name']
.. 递归下降,遍历所有层级的节点 $..id
* 通配符,匹配当前层级的所有元素 $.users[*]
[index] 数组下标访问(支持负索引) $.scores[0]
[a,b,c] 联合选择,返回多个指定元素 $.scores[0,2,4]
[start:end:step] 数组切片(起始索引 : 结束索引 : 步长) $.scores[1:5:2]
[*] 选择数组中的所有元素 $.items[*]
[:] 选择数组中的所有元素(切片写法) $.items[:]
?() 过滤表达式,用于数组或对象的条件筛选 $.users[?(@.age>=18)]
() 脚本表达式,用于计算或复杂逻辑判断 $.items[?((@.a+@.b)>10)]
@ 当前元素引用,常用于过滤或脚本表达式中 @.price<100
  • 数组切片 [start:end:step] 详解

写法 含义
[3:] 从索引 3 到末尾
[:8] 从头到索引 7
[:8:2] 从头到 7,每隔 2 个取一个
[::] 等价于 [:],取全部
[*] 推荐写法,取全部

默认值规则:
start 默认 0
end 默认数组末尾
step 默认 1

  • 过滤表达式 ?() 支持的运算符

比较运算符,实测中仅支持数字比较,且注意两边不能有空格

运算符 含义
== 等于
!= 不等于
< 小于
<= 小于等于
> 大于
>= 大于等于
=~ 正则匹配

逻辑运算符

运算符 含义
&&
||
() 分组优先级
  • Redis 的 RedisJSON 模块(提供 JSON 操作功能)支持的 JSONPath 语法是受限的子集,尤其是:

    • ❌ 不支持复杂表达式
    • ❌ 不支持嵌套数组条件(@.items[*].xxx)
    • ⚠️ 对 ?() 的支持非常有限
  • 如果需要复杂的查询推荐使用 RediSearch 模块,这个我们后面的章节会介绍。

JSON 命令

  • JSON 命令 汇总

分类 命令
基础读写 SET / MSET / GET / MGET
数组操作 ARRAPPEND / ARRINSERT / ARRPOP / ARRLEN / ARRINDEX / ARRTRIM
对象操作 OBJKEYS / OBJLEN / TYPE
数值运算 NUMINCRBY / NUMMULTBY / TOGGLE
字符串 STRAPPEND / STRLEN
删除清空 DEL / FORGET / CLEAR
合并输出 MERGE / RESP
调试诊断 DEBUG / DEBUG MEMORY

一、基础读写类(CRUD)

命令 作用 关键参数说明 示例
JSON.SET 设置或更新某个 path 的值 key:键名
path:JSONPath(如 $$.a.b
value:JSON 值
JSON.SET user:1 $ '{"name":"Tom","age":18}'
JSON.MSET 批量设置多个 key (key path value)... JSON.MSET k1 $ '{"a":1}' k2 $.b 2
JSON.GET 获取一个 key 的 JSON 值 key
path...:可多个
JSON.GET user:1 $.name $.age
JSON.MGET 从多个 key 获取同一路径 key... path JSON.MGET k1 k2 $.a
  • 说明:

    • JSON.GET 返回 字符串化 JSON
    • JSON.MGET 返回数组,对应多个 key

二、数组操作类(Array)

命令 作用 参数含义 示例
JSON.ARRAPPEND 向数组末尾追加元素 key path value... JSON.ARRAPPEND k $.tags "redis" "json"
JSON.ARRINSERT 在指定索引插入元素 key path index value... JSON.ARRINSERT k $.tags 1 "nosql"
JSON.ARRPOP 删除并返回指定索引元素 key path [index] JSON.ARRPOP k $.tags -1
JSON.ARRLEN 获取数组长度 key path JSON.ARRLEN k $.tags
JSON.ARRINDEX 查找元素索引 key path value JSON.ARRINDEX k $.tags "redis"
JSON.ARRTRIM 截取数组区间 key path start stop JSON.ARRTRIM k $.scores 0 9
  • 说明:

    • 索引支持负数(-1 表示最后一个)
    • ARRTRIM 类似 LTRIM

三、对象操作类(Object)

命令 作用 参数说明 示例
JSON.OBJKEYS 获取对象的所有 key key path JSON.OBJKEYS k $.user
JSON.OBJLEN 获取对象字段数量 key path JSON.OBJLEN k $.user
JSON.TYPE 返回 path 对应值类型 key path JSON.TYPE k $.user.name
  • JSON.TYPE 返回类型包括:object / array / string / number / boolean / null

四、数值运算类(Numeric)

命令 作用 参数说明 示例
JSON.NUMINCRBY 数值自增 key path number JSON.NUMINCRBY k $.count 1
JSON.NUMMULTBY 数值乘法 key path number JSON.NUMMULTBY k $.price 0.8
JSON.TOGGLE 布尔值取反 key path JSON.TOGGLE k $.enabled
  • 说明:

    • 保证 原子性
    • 常用于计数、开关状态

五、字符串操作类(String)

命令 作用 参数说明 示例
JSON.STRAPPEND 追加字符串 key path value JSON.STRAPPEND k $.msg " world"
JSON.STRLEN 字符串长度 key path JSON.STRLEN k $.msg

六、删除 / 清空类

命令 作用 参数说明 示例
JSON.DEL 删除指定 path key path JSON.DEL k $.user.age
JSON.FORGET 等价 JSON.DEL 同上 JSON.FORGET k $.tmp
JSON.CLEAR 清空值 key path JSON.CLEAR k $.list
  • JSON.CLEAR 行为说明:

    • 数组 → []
    • 对象 → {}
    • 数值 → 0

七、合并与高级操作

命令 作用 参数说明 示例
JSON.MERGE 合并 JSON(RFC7396) key path value JSON.MERGE k $ '{"a":2,"b":3}'
JSON.RESP RESP 格式返回 key path JSON.RESP k $.user
  • 说明:

    • MERGE 支持字段覆盖、删除、扩展
    • RESP 适合客户端直接解析

八、调试与诊断类(Debug)

命令 作用 参数说明 示例
JSON.DEBUG 调试命令容器 子命令 JSON.DEBUG MEMORY k
JSON.DEBUG MEMORY 查看 JSON 占用内存 key JSON.DEBUG MEMORY user:1

应用示例

  • 场景选用:电商用户画像 + 订单系统(结构复杂、字段多、非常适合 JSONPath)

1
2
3
4
5
6
7
我们要存储一个用户的完整画像:
基本信息
地址列表
订单列表(包含商品、价格、数量)
标签(偏好)
账户状态
统计信息
  • SpringBoot暂时没有支持RedisJSON,你可以编写Lua脚本来实现相应的功能,另外 Redissson已经提供了对JSON的支持,但是实际使用中发现还有一些Bug,下面结合命令给出代码示例。

1
2
3
4
5
6
<!-- 引入 Redisson ,这里要注意,现在最新版是 4.0.0,需要 springboot 4.x -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.52.0</version>
</dependency>
  • 创建完整 JSON 文档(JSON.SET)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# 创建 JSON 文档,实际执行中不支持多行,这里只是为了看着清楚些,实际运行时要将其改成一行
JSON.SET user:10001 $ '{
"profile": {
"name": "Alice",
"age": 28,
"vip": true
},
"tags": ["vip", "electronics", "promotion"],
"addresses": [
{ "city": "Beijing", "zip": "100000" },
{ "city": "Shanghai", "zip": "200000" }
],
"orders": [
{
"orderId": "O1001",
"amount": 199.99,
"status": "PAID",
"items": [
{ "sku": "SKU-1", "price": 99.99, "qty": 1 },
{ "sku": "SKU-2", "price": 100.00, "qty": 1 }
]
},
{
"orderId": "O1002",
"amount": 59.90,
"status": "CREATED",
"items": [
{ "sku": "SKU-3", "price": 59.90, "qty": 1 }
]
}
],
"stats": {
"loginCount": 10,
"balance": 300.5
}
}'

# 也可以使用单行模式一点点组装
JSON.SET user:10001 $ '{}'
JSON.SET user:10001 $.profile '{"name":"Alice","age":28,"vip":true}'
JSON.SET user:10001 $.tags '["vip","electronics","promotion"]'
JSON.SET user:10001 $.addresses '[{"city":"Beijing","zip":"100000"},{"city":"Shanghai","zip":"200000"}]'
JSON.SET user:10001 $.orders '[{"orderId":"O1001","amount":199.99,"status":"PAID","items":[{"sku":"SKU-1","price":99.99,"qty":1},{"sku":"SKU-2","price":100,"qty":1}]},{"orderId":"O1002","amount":59.90,"status":"CREATED","items":[{"sku":"SKU-3","price":59.90,"qty":1}]}]'
JSON.SET user:10001 $.stats '{"loginCount":10,"balance":300.5}'

# 查看类型
127.0.0.1:6379> type user:10001
ReJSON-RL
  • 通用代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Autowired
private RedissonClient redissonClient;

String userStr = """
{
"profile": {
"name": "Alice",
"age": 28,
"vip": true
},
"tags": ["vip", "electronics", "promotion"],
"addresses": [
{ "city": "Beijing", "zip": "100000" },
{ "city": "Shanghai", "zip": "200000" }
],
"orders": [
{
"orderId": "O1001",
"amount": 199.99,
"status": "PAID",
"items": [
{ "sku": "SKU-1", "price": 99.99, "qty": 1 },
{ "sku": "SKU-2", "price": 100.00, "qty": 1 }
]
},
{
"orderId": "O1002",
"amount": 59.90,
"status": "CREATED",
"items": [
{ "sku": "SKU-3", "price": 59.90, "qty": 1 }
]
}
],
"stats": {
"loginCount": 10,
"balance": 300.5
}
}
""";
ObjectMapper objectMapper = new ObjectMapper();
RJsonBucket<User> bucket = redissonClient.getJsonBucket("user:10001", new JacksonCodec<>(User.class));
1
2
User user = objectMapper.readValue(userStr, User.class);
bucket.set(user);

JSONPath 查询示例

  • 1️⃣ 基础路径访问

1
2
3
4
5
6
7
8
9
10
# 获取全部数据
127.0.0.1:6379> JSON.GET user:10001

# 获取用户信息
127.0.0.1:6379> JSON.GET user:10001 $.profile
"[{\"name\":\"Alice\",\"age\":28,\"vip\":true}]"

# 获取用户名
127.0.0.1:6379> JSON.GET user:10001 $.profile.name
"[\"Alice\"]"
1
2
3
4
5
6
7
8
9
User user = bucket.get(new JacksonCodec<>(new TypeReference<User>() {}));

User.ProfileBean profile = bucket.get(new JacksonCodec<>(new TypeReference<User.ProfileBean>() {}),"profile");

String name = bucket.get(new JacksonCodec<>(new TypeReference<String>() {}),"profile.name");

// 加上 $. 前缀,返回值必须是 List<T>
List<String> names = bucket.get(new JacksonCodec<>(new TypeReference<List<String>>() {}),"$.profile.name");
System.out.println(names.get(0));
  • 2️⃣ 数组访问 & 通配符

1
2
3
4
5
6
7
# 获取所有订单ID
127.0.0.1:6379> JSON.GET user:10001 $.orders[*].orderId
"[\"O1001\",\"O1002\"]"

# 获取所有地址的城市
127.0.0.1:6379> JSON.GET user:10001 $.addresses[*].city
"[\"Beijing\",\"Shanghai\"]"
1
2
3
List<String> orderIds = bucket.get(new JacksonCodec<>(new TypeReference<List<String>>() {}),"$.orders[*].orderId");

List<String> citys = bucket.get(new JacksonCodec<>(new TypeReference<List<String>>() {}),"$.addresses[*].city");
  • 3️⃣ 数组下标与切片

1
2
3
4
5
6
7
8
9
10
11
# 获取第一个订单
127.0.0.1:6379> JSON.GET user:10001 $.orders[0]
"[{\"orderId\":\"O1001\",\"amount\":199.99,\"status\":\"PAID\",\"items\":[{\"sku\":\"SKU-1\",\"price\":99.99,\"qty\":1},{\"sku\":\"SKU-2\",\"price\":100,\"qty\":1}]}]"

# 获取第一个和第二个订单,切片逻辑是 [start:end], start <= x < end ,end不写默认为数组末尾
127.0.0.1:6379> JSON.GET user:10001 $.orders[0:2]
"[{\"orderId\":\"O1001\",\"amount\":199.99,\"status\":\"PAID\",\"items\":[{\"sku\":\"SKU-1\",\"price\":99.99,\"qty\":1},{\"sku\":\"SKU-2\",\"price\":100,\"qty\":1}]},{\"orderId\":\"O1002\",\"amount\":59.9,\"status\":\"CREATED\",\"items\":[{\"sku\":\"SKU-3\",\"price\":59.9,\"qty\":1}]}]"

# 获取所有订单,这里等同于 $.orders[*]
127.0.0.1:6379> JSON.GET user:10001 $.orders[:]
"[{\"orderId\":\"O1001\",\"amount\":199.99,\"status\":\"PAID\",\"items\":[{\"sku\":\"SKU-1\",\"price\":99.99,\"qty\":1},{\"sku\":\"SKU-2\",\"price\":100,\"qty\":1}]},{\"orderId\":\"O1002\",\"amount\":59.9,\"status\":\"CREATED\",\"items\":[{\"sku\":\"SKU-3\",\"price\":59.9,\"qty\":1}]}]"
1
2
3
4
5
List<OrdersBean> orders = bucket.get(new JacksonCodec<>(new TypeReference<List<OrdersBean>>() {}),"$.orders[0]");

orders = bucket.get(new JacksonCodec<>(new TypeReference<List<OrdersBean>>() {}),"$.orders[0:2]");

orders = bucket.get(new JacksonCodec<>(new TypeReference<List<OrdersBean>>() {}),"$.orders[:]");
  • 4️⃣ 递归查询(…)

1
2
3
# 👉 找出所有商品价格(不管在哪一层)
127.0.0.1:6379> JSON.GET user:10001 $..price
"[99.99,100,59.9]"
1
List<Double> prices = bucket.get(new JacksonCodec<>(new TypeReference<List<Double>>() {}),"$..price");
  • 5️⃣ 条件过滤(JSONPath 核心能力)

✅ 支持的查询

1
2
3
4
5
6
7
8
9
10
11
# 👉 找出所有订单金额大于100的订单,✅ 数字比较
127.0.0.1:6379> JSON.GET user:10001 $.orders[?(@.amount>100)]
"[{\"orderId\":\"O1001\",\"amount\":199.99,\"status\":\"PAID\",\"items\":[{\"sku\":\"SKU-1\",\"price\":99.99,\"qty\":1},{\"sku\":\"SKU-2\",\"price\":100,\"qty\":1}]}]"

# 👉 找出所有订单中第一个商品价格大于50的订单,✅ 数组条件
127.0.0.1:6379> JSON.GET user:10001 $.orders[?(@.items[0].price>50)]
"[{\"orderId\":\"O1001\",\"amount\":199.99,\"status\":\"PAID\",\"items\":[{\"sku\":\"SKU-1\",\"price\":99.99,\"qty\":1},{\"sku\":\"SKU-2\",\"price\":100,\"qty\":1}]},{\"orderId\":\"O1002\",\"amount\":59.9,\"status\":\"CREATED\",\"items\":[{\"sku\":\"SKU-3\",\"price\":59.9,\"qty\":1}]}]"

# 👉 找出所有订单金额大于100并且订单中第一个商品价格大于50的订单,✅ 逻辑运算符
127.0.0.1:6379> JSON.GET user:10001 $.orders[?(@.amount>100)&&(@.items[0].price>50)]
"[{\"orderId\":\"O1001\",\"amount\":199.99,\"status\":\"PAID\",\"items\":[{\"sku\":\"SKU-1\",\"price\":99.99,\"qty\":1},{\"sku\":\"SKU-2\",\"price\":100,\"qty\":1}]}]"
1
2
3
4
5
List<OrdersBean> orders = bucket.get(new JacksonCodec<>(new TypeReference<List<OrdersBean>>() {}),"$.orders[?(@.amount>100)]");

orders = bucket.get(new JacksonCodec<>(new TypeReference<List<OrdersBean>>() {}),"$.orders[?(@.items[0].price>50)]");

orders = bucket.get(new JacksonCodec<>(new TypeReference<List<OrdersBean>>() {}),"$.orders[?(@.amount>100)&&(@.items[0].price>50)]");

❌ 不支持的查询

1
2
3
4
5
6
7
8
# 👉 找出所有已支付订单,❌ 不支持字符串的比较
JSON.GET user:10001 $.orders[?(@.status=="PAID")]
# 👉 找出所有已支付订单,❌ 不支持正则匹配
JSON.GET user:10001 $.orders[?(@.status=~"PAID")]

# 👉 找出所有订单中商品价格大于50的订单,这个就不准,❌ 不支持嵌套数组条件(@.items[*].xxx)
127.0.0.1:6379> JSON.GET user:10001 $.orders[?(@.items[*].price>50)]
"[{\"orderId\":\"O1002\",\"amount\":59.9,\"status\":\"CREATED\",\"items\":[{\"sku\":\"SKU-3\",\"price\":59.9,\"qty\":1}]}]"
  • 6️⃣ 修改数据(JSON.SET / JSON.NUMINCRBY)

1
2
3
4
5
6
# 👉 修改用户名
127.0.0.1:6379> JSON.SET user:10001 $.profile.name '"Alice Zhang"'
OK
# 👉 用户登录次数 +1
127.0.0.1:6379> JSON.NUMINCRBY user:10001 $.stats.loginCount 1
"[11]" # 返回修改后的值
1
2
3
4
bucket.set("$.profile.name", "Alice Zhang");
// 注意如下方法会增加成功,但是会抛异常,应该是Redisson的bug
// Caused by: java.lang.NumberFormatException: For input string: "[11]"
bucket.incrementAndGet("$.stats.loginCount", 1);
  • 7️⃣ 数组操作(ARRAPPEND / ARRINSERT)

1
2
3
4
5
6
7
8
9
10
11
# 👉 添加一个订单
127.0.0.1:6379> JSON.ARRAPPEND user:10001 $.orders '{"orderId": "O1003","amount": 399,"status": "PAID","items": [{ "sku": "SKU-9", "price": 399, "qty": 1 }]}'
1) (integer) 3 # 返回添加后的长度

# 👉 在 tags 的最后添加一个标签
127.0.0.1:6379> JSON.ARRAPPEND user:10001 $.tags '"newTag"'
1) (integer) 4 # 添加后的长度

# 👉 在 tags 中的指定位置插入一个标签
127.0.0.1:6379> JSON.ARRINSERT user:10001 $.tags 1 '"newTag2"'
1) (integer) 5 # 插入后的长度
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
OrdersBean ordersBean = OrdersBean.builder()
.orderId("O1003")
.amount(399)
.status("CREATED")
.items(List.of(
OrdersBean.ItemsBean.builder()
.sku("SKU-9")
.qty(1)
.price(399).build()
)).build();
// 添加一个订单,返回添加后的长度
long num = bucket.arrayAppend("$.orders", ordersBean);

num = bucket.arrayAppend("$.tags", "newTag");

// 插入成功,但是会抛异常,应该是Redisson的bug
num = bucket.arrayInsert("$.tags", 1, "newTag2");
  • 8️⃣ 删除 & 清理操作(JSON.DEL & JSON.CLEAR)

1
2
3
4
5
6
7
# 👉 删除用户年龄,$.profile.age 不存在了
127.0.0.1:6379> JSON.DEL user:10001 $.profile.age
(integer) 1 # 删除成功,返回0,表示路径不存在

# 👉 清空用户所有标签,$.tags 保留,只是变成了 [] 空数组
127.0.0.1:6379> JSON.CLEAR user:10001 $.tags
(integer) 1 # 清空成功,返回0,表示路径不存在或已经清空
1
2
3
long delete = bucket.delete("$.profile.age");

long clear = bucket.clear("$.tags");
  • 9️⃣ 统计数组长度

1
2
3
# 👉 统计用户订单的长度
127.0.0.1:6379> JSON.ARRLEN user:10001 $.orders
1) (integer) 3
1
2
// 会抛异常,应该是Redisson的bug
long length = bucket.arraySize("$.orders");

🔟 查看 JSON 占用内存

1
2
127.0.0.1:6379> JSON.DEBUG MEMORY user:10001
(integer) 1896 # 占用内存大小,单位字节
1
long sizeInMemory = bucket.sizeInMemory();