30
30
2) " JavaGuide"
31
31
```
32
32
33
- [ ` MULTI ` ] ( https://redis.io/commands/multi ) 命令后可以输入多个命令,Redis 不会立即执行这些命令,而是将它们放到队列,当调用了 [ ` EXEC ` ] ( https://redis.io/commands/exec ) 命令后,再执行所有的命令。
33
+ [ ` MULTI ` ] ( https://redis.io/commands/multi ) 命令后可以输入多个命令,Redis 不会立即执行这些命令,而是将它们放到队列,当调用了 [ ` EXEC ` ] ( https://redis.io/commands/exec ) 命令后,再执行所有的命令。
34
34
35
35
这个过程是这样的:
36
36
@@ -122,7 +122,7 @@ Redis 官网相关介绍 [https://redis.io/topics/transactions](https://redis.io
122
122
123
123
![ Redis 事务] ( https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/database/redis/redis-transactions.png )
124
124
125
- ### Redis 支持原子性吗 ?
125
+ ### Redis 事务支持原子性吗 ?
126
126
127
127
Redis 的事务和我们平时理解的关系型数据库的事务不同。我们知道事务具有四大特性: ** 1. 原子性** ,** 2. 隔离性** ,** 3. 持久性** ,** 4. 一致性** 。
128
128
@@ -131,7 +131,7 @@ Redis 的事务和我们平时理解的关系型数据库的事务不同。我
131
131
3 . ** 持久性(Durability):** 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
132
132
4 . ** 一致性(Consistency):** 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的;
133
133
134
- Redis 事务在运行错误的情况下,除了执行过程中出现错误的命令外,其他命令都能正常执行。并且,Redis 是不支持回滚 (roll back)操作的。因此,Redis 事务其实是不满足原子性的(而且不满足持久性)。
134
+ Redis 事务在运行错误的情况下,除了执行过程中出现错误的命令外,其他命令都能正常执行。并且,Redis 事务是不支持回滚 (roll back)操作的。因此,Redis 事务其实是不满足原子性的(而且不满足持久性)。
135
135
136
136
Redis 官网也解释了自己为啥不支持回滚。简单来说就是 Redis 开发者们觉得没必要支持回滚,这样更简单便捷并且性能更好。Redis 开发者觉得即使命令执行错误也应该在开发过程中就被发现而不是生产过程中。
137
137
@@ -154,7 +154,9 @@ Redis 从 2.6 版本开始支持执行 Lua 脚本,它的功能和事务非常
154
154
155
155
一段 Lua 脚本可以视作一条命令执行,一段 Lua 脚本执行过程中不会有其他脚本或 Redis 命令同时执行,保证了操作不会被其他指令插入或打扰。
156
156
157
- 如果 Lua 脚本运行时出错并中途结束,出错之后的命令是不会被执行的。并且,出错之前执行的命令是无法被撤销的。因此,严格来说,通过 Lua 脚本来批量执行 Redis 命令也是不满足原子性的。
157
+ 不过,如果 Lua 脚本运行时出错并中途结束,出错之后的命令是不会被执行的。并且,出错之前执行的命令是无法被撤销的,无法实现类似关系型数据库执行失败可以回滚的那种原子性效果。因此, ** 严格来说的话,通过 Lua 脚本来批量执行 Redis 命令实际也是不完全满足原子性的。**
158
+
159
+ 如果想要让 Lua 脚本中的命令全部执行,必须保证语句语法和命令都是对的。
158
160
159
161
另外,Redis 7.0 新增了 [ Redis functions] ( https://redis.io/docs/manual/programmability/functions-intro/ ) 特性,你可以将 Redis functions 看作是比 Lua 更强大的脚本。
160
162
@@ -226,6 +228,64 @@ Biggest string found '"ballcat:oauth:refresh_auth:f6cdb384-9a9d-4f2f-af01-dc3f28
226
228
227
229
个人建议不管是否开启 lazy-free,我们都尽量给 key 设置随机过期时间。
228
230
231
+ # ## 使用批量操作减少网络传输
232
+
233
+ 一个 Redis 命令的执行可以简化为以下 4 步:
234
+
235
+ 1. 发送命令
236
+ 2. 命令排队
237
+ 3. 命令执行
238
+ 4. 返回结果
239
+
240
+ 其中,第 1 步和第 4 步耗费时间之和称为 ** Round Trip Time (RTT,往返时间)** ,也就是数据在网络上传输的时间。
241
+
242
+ ** 使用批量操作可以减少网络传输次数,进而有效减小网络开销,大幅减少 RTT。**
243
+
244
+ # ### 原生批量操作命令
245
+
246
+ Redis 中有一些原生支持批量操作的命令,比如:
247
+
248
+ - ` mget` (获取一个或多个指定 key 的值)、` mset` (设置一个或多个指定 key 的值)、
249
+ - ` hmget` (获取指定哈希表中一个或者多个指定字段的值)、` hmset` (同时将一个或多个 field-value 对设置到指定哈希表中)、
250
+ - ` sadd` (向指定集合添加一个或多个元素)
251
+ - ......
252
+
253
+ 不过,在 Redis 官方提供的分片集群解决方案 Redis Cluster 下,使用这些原生批量操作命令可能会存在一些小问题需要解决。就比如说 ` mget` 无法保证所有的 key 都在同一个 ** hash slot** (哈希槽)上,` mget` 可能还是需要多次网络传输,原子操作也无法保证了。不过,相较于非批量操作,还是可以节省不少网络传输次数。
254
+
255
+ 整个步骤的简化版如下(通常由 Redis 客户端实现,无需我们自己再手动实现):
256
+
257
+ 1. 找到 key 对应的所有 hash slot;
258
+ 2. 分别向对应的 Redis 节点发起 ` mget` 请求获取数据;
259
+ 3. 等待所有请求执行结束,重新组装结果数据,保持跟入参 key 的顺序一致,然后返回结果。
260
+
261
+ 如果想要解决这个多次网络传输的问题,比较常用的办法是自己维护 key 与 slot 的关系。不过这样不太灵活,虽然带来了性能提升,但同样让系统复杂性提升。
262
+
263
+ > Redis Cluster 并没有使用一致性哈希,采用的是 ** 哈希槽分区** ,每一个键值对都属于一个 ** hash slot** (哈希槽) 。当客户端发送命令请求的时候,需要先根据 key 通过上面的计算公示找到的对应的哈希槽,然后再查询哈希槽和节点的映射关系,即可找到目标 Redis 节点。
264
+ >
265
+ > 我在 [Redis 集群详解(付费)](https://javaguide.cn/database/redis/redis-cluster.html) 这篇文章中详细介绍了 Redis Cluster 这部分的内容,感兴趣地可以看看。
266
+
267
+ # ### pipeline
268
+
269
+ 对于不支持批量操作的命令,我们可以利用 ** pipeline(流水线)** 将一批 Redis 命令封装成一组,这些 Redis 命令会被一次性提交到 Redis 服务器,只需要一次网络传输。不过,需要注意控制一次批量操作的 ** 元素个数** (例如 500 以内,实际也和元素字节数有关),避免网络传输的数据量过大。
270
+
271
+ 与` mget` 、` mset` 等原生批量操作命令一样,pipeline 同样在 Redis Cluster 上使用会存在一些小问题。原因类似,无法保证所有的 key 都在同一个 ** hash slot** (哈希槽)上。如果想要使用的话,客户端需要自己维护 key 与 slot 的关系。
272
+
273
+ 原生批量操作命令和 pipeline 的是有区别的,使用的时候需要注意:
274
+
275
+ - 原生批量操作命令是原子操作,pipeline 是非原子操作;
276
+ - pipeline 可以打包不同的命令,原生批量操作命令不可以;
277
+ - 原生批量操作命令是 Redis 服务端支持实现的,而 pipeline 需要服务端和客户端的共同实现。
278
+
279
+ 另外,pipeline 不适用于执行顺序有依赖关系的一批命令。就比如说,你需要将前一个命令的结果给后续的命令使用,pipeline 就没办法满足你的需求了。对于这种需求,我们可以使用 ** Lua 脚本** 。
280
+
281
+ # ### Lua 脚本
282
+
283
+ Lua 脚本同样支持批量操作多条命令。一段 Lua 脚本可以视作一条命令执行,可以看作是原子操作。一段 Lua 脚本执行过程中不会有其他脚本或 Redis 命令同时执行,保证了操作不会被其他指令插入或打扰,这是 pipeline 所不具备的。
284
+
285
+ 并且,Lua 脚本中支持一些简单的逻辑处理比如使用命令读取值并在 Lua 脚本中进行处理,这同样是 pipeline 所不具备的。
286
+
287
+ 不过, Redis Cluster 下 Lua 脚本的原子操作也无法保证了,原因同样是无法保证所有的 key 都在同一个 ** hash slot** (哈希槽)上。
288
+
229
289
# # Redis 生产问题
230
290
231
291
# ## 缓存穿透
@@ -391,6 +451,20 @@ Cache Aside Pattern 中遇到写请求是这样的:更新 DB,然后直接删
391
451
392
452
** 参考答案** :[Redis 集群详解(付费)](https://javaguide.cn/database/redis/redis-cluster.html)。
393
453
454
+ # # Redis 使用规范
455
+
456
+ 实际使用 Redis 的过程中,我们尽量要准守一些常见的规范,比如:
457
+
458
+ 1. 使用连接池:避免频繁创建关闭客户端连接。
459
+ 2. 尽量不使用 O(n)指令,使用 O(N)命令时要关注 N 的数量 :例如 ` hgetall` 、` lrange` 、` smembers` 、` zrange` 、` sinter` 、` sunion` 命令并非不能使用,但是需要明确 N 的值。有遍历的需求可以使用 ` hscan` 、` sscan` 、` zscan` 代替。
460
+ 3. 使用批量操作减少网络传输 :原生批量操作命令(比如 ` mget` 、` mset` 等等)、pipeline、Lua 脚本。
461
+ 4. 尽量不适用 Redis 事务:Redis 事务实现的功能比较鸡肋,可以使用 Lua 脚本代替。
462
+ 5. 禁止长时间开启 monitor:对性能影响比较大。
463
+ 6. 控制 key 的生命周期:避免 Redis 中存放了太多不经常被访问的数据。
464
+ 7. ......
465
+
466
+ 相关文章推荐 :[阿里云 Redis 开发规范](https://developer.aliyun.com/article/531067) 。
467
+
394
468
# # 参考
395
469
396
470
- 《Redis 开发与运维》
0 commit comments