diff --git a/notes/pics/feed-push-pull.png b/notes/pics/feed-push-pull.png index f6f75da7..5d89fb32 100644 Binary files a/notes/pics/feed-push-pull.png and b/notes/pics/feed-push-pull.png differ diff --git a/notes/系统设计基础.md b/notes/系统设计基础.md index 8d30358b..ddd909c1 100644 --- a/notes/系统设计基础.md +++ b/notes/系统设计基础.md @@ -302,7 +302,7 @@ return 0 -- 可以发送 同一个手机号 1 秒内请求 5 次,只有第 1 次发送了短信;把冷却时间改为 1 秒后连续请求 12 次,第 11、12 次因为当天的次数用完被拒绝;同一个 IP 给 30 个手机号发验证码,从第 21 个开始被拒绝(Redis 7.4 实测,见[附录 A20](#a20-接口防刷))。按小时、按天计数是固定窗口,窗口边界附近可能短时间内放过两倍的请求,需要更精确时改用滑动窗口,见 [限流算法](#1-限流算法)。 -验证码本身也要防止被猜中。6 位数字只有 100 万种,不限制输错次数时,在本机直接调用校验脚本逐个尝试,第 835770 次猜中,只用了 8 秒(见附录 A20);通过网络调用接口会慢一些,但按每秒 1000 次计算,平均约 8 分钟也能猜中。所以校验时: +验证码本身也要防止被猜中。6 位数字只有 100 万种,不限制输错次数时,在服务器所在的机器上直接调用校验逻辑逐个尝试,第 835770 次猜中,只用了 8 秒(见附录 A20);通过网络调用接口会慢一些,但按每秒 1000 次计算,平均约 8 分钟也能猜中。所以校验时: - 验证码保存在 Redis 中,5 分钟后过期,多个服务实例都能读到; - 输对后立即删除,一个验证码只能用一次; @@ -310,7 +310,7 @@ return 0 -- 可以发送 加上发送的限制,一个手机号每天最多能猜 50 次,猜中的概率只有 50 / 106 = 0.005%。 -开放给合作方的接口还要防止请求被篡改或者重放。重放指的是把截获的合法请求原样再发一次,例如把一次退款请求重复提交。做法是双方事先约定一个密钥,调用方把请求的关键字段和时间戳、随机数(nonce)拼在一起,用密钥计算 HMAC 签名: +开放给合作方的接口还要防止请求被篡改或者重放。重放指的是把截获的合法请求原样再发一次,例如把一次退款请求重复提交。做法是双方事先约定一个密钥,调用方把请求的方法、路径、查询参数、请求体和时间戳、随机数(nonce)拼在一起(漏掉的部分可以被改掉而签名不变),用密钥计算 HMAC 签名: ```text 签名原文:POST\n/api/refund\n1793000000\nn-8f3a2c\n{"order": 1024, "amount": 3900} @@ -321,9 +321,9 @@ return 0 -- 可以发送 - 时间戳与当前时间相差不超过 5 分钟,很久以前的请求直接拒绝; - 用同样的密钥重新计算签名并比较,请求中任何一个字段被改过都对不上; -- 执行 SET nonce:n-8f3a2c 1 NX EX 600 记录随机数,失败说明这个请求已经处理过。随机数保存的时间要覆盖时间戳允许的范围,否则过期后又能重放。 +- 执行 SET nonce:n-8f3a2c 1 NX EX 600 记录随机数,失败说明这个请求已经处理过。随机数保存的时间要覆盖时间戳允许的范围,前后各 5 分钟就保存 10 分钟,否则过期后又能重放。多个调用方时键名带上调用方的标识,避免随机数相互冲突。 -原样重放的请求因为随机数已经用过被拒绝,改掉金额的请求签名对不上,10 分钟后再重放则时间戳超出了范围(见附录 A20)。需要注意的是,签名只适合密钥保存在服务端的调用,例如开放平台和支付回调;写在 App 或网页中的密钥可以被逆向取出,只能增加刷接口的成本。 +原样重放的请求因为随机数已经用过被拒绝,改掉金额的请求签名对不上,10 分钟后再重放则时间戳超出了范围(见附录 A20)。随机数只能拦住原样的重放:调用方超时后换一个随机数重试,签名层看到的是新请求,退款这类操作仍要按业务单号做[幂等](分布式.md#九幂等)。需要注意的是,签名只适合密钥保存在服务端的调用,例如开放平台和支付回调;写在 App 或网页中的密钥可以被逆向取出,只能增加刷接口的成本。 识别出的刷接口来源,还可以在网关中加入黑名单,例如 SET ban:ip:203.0.113.7 1 EX 3600,一小时内直接拒绝它的请求。规模更大的系统会接入风控,根据设备指纹、操作轨迹等判断请求是否来自机器,可疑时要求验证码。图形和滑块验证码能被打码平台破解,同样只能提高成本,所以要和限频、黑名单一起使用。 @@ -496,7 +496,7 @@ WHERE id = 70001 AND user_id = 1001 AND status = 0 AND valid_end > NOW(); 拆分的时机有两种: - 预先拆分:发红包时就拆成 10 份,用 RPUSH 放入 Redis 列表,抢的时候 LPOP 一份。抢的路径只有一次弹出,金额在发的时候就已确定,可以随红包一起落库备查;代价是每一份都要占用内存,份数多时更明显; -- 实时计算:Redis 中只保存剩余金额和剩余个数,抢的时候在 Lua 脚本中按二倍均值法算出本份金额,再更新这两个值。占用的内存固定,但每次领取都要在脚本中计算。Redis 7 中脚本的随机数生成器每次调用都会随机设置种子,可以直接调用 math.random。 +- 实时计算:Redis 中只保存剩余金额和剩余个数,抢的时候在 Lua 脚本中按二倍均值法算出本份金额,再更新这两个值。占用的内存固定,但每次领取都要在脚本中计算。Redis 7 中脚本可以直接调用 math.random。需要注意的是,随机数生成器只在启动时设置固定的初值,之后各次调用依次取同一个序列中的值,重启后序列从头开始(Redis 7.4 实测,见[附录 A26](#a26-脚本中的-mathrandom));对结果的不可预测性有要求时,由应用生成随机数作为参数传入。 两种做法用的是同一个拆分方法,金额的分布相同,在 Lua 脚本中执行时也都不会超发。红包的份数通常不多,预先拆分的实现更简单。 @@ -537,7 +537,7 @@ WHERE id = 8001 AND remain_num > 0 AND remain_amount >= 276; 应用在脚本执行成功之后、消息发出之前宕机时,这条领取只记录在 Redis 中。因此红包抢完或过期后,要用 Redis 中的领取记录与数据库对账,补发缺失的消息。例如每条消息都投递两次、其中一个用户的消息全部丢失:18 条消息插入 9 条、冲突 9 条,对账补发后剩余金额为 0、领取记录 10 条,合计正好 100 元(MySQL 8.0 实测,见[附录 A16](#a16-并发抢红包与异步落库))。 -红包一般有有效期,例如 24 小时。到期后先完成对账,再把数据库中的剩余金额退回给发送者。 +红包一般有有效期,例如 24 小时。到期后先在 Redis 中停止领取(例如删除金额列表,之后的脚本 LPOP 取不到金额,直接返回已抢完),再对账并补发缺失的消息,确认这些领取都已写入数据库后,把数据库中的剩余金额退回给发送者。顺序反过来时,对账之后仍可能有人抢到,退回的金额就多了。 ## 七、海量数据处理 @@ -582,7 +582,7 @@ WHERE id = 8001 AND remain_num > 0 AND remain_amount >= 276; ## 八、短链系统 -短链系统把很长的网址换成很短的网址,例如把 https://shop.example.com/item/1024?from=campaign 换成 https://s.example/6LAze,常用于短信和社交媒体。用户访问短链时,服务返回一个跳转,浏览器再去访问原来的长链接。 +短链系统把很长的网址换成很短的网址,例如把 `https://shop.example.com/item/1024?from=campaign` 换成 `https://s.example/6LAze`,常用于短信和社交媒体。用户访问短链时,服务返回一个跳转,浏览器再去访问原来的长链接。

@@ -889,7 +889,7 @@ S3 的完成请求要求列出每个分片的分片号和 ETag,服务端按分 分片本身不是对象,只有完成请求之后才生成对象,分片也随之消失。因此合并是分片上传的最后一步。在 S3 中,合并由存储服务按分片号组合,应用不需要再下载、拼接并重新上传一次。 -MinIO 兼容 S3 的分片上传接口,它的限制与 S3 一致:分片号必须在 1 到 10000 之间,分片小于 5 MB 时返回 EntityTooSmall 错误。应用侧的流程与上文相同,区别在于分片由 MinIO 保存,完成时由 MinIO 组合成对象。如果希望大文件不经过应用服务器,也可以让客户端用预签名 URL 直接上传到对象存储,应用服务只保存上传状态。 +MinIO 兼容 S3 的分片上传接口,分片号和最小分片的限制与 S3 一致:分片号必须在 1 到 10000 之间,除最后一片外每片至少 5 MiB,否则完成上传时返回 EntityTooSmall 错误;单个分片最大 5 TiB,比 S3 的 5 GiB 大。应用侧的流程与上文相同,区别在于分片由 MinIO 保存,完成时由 MinIO 组合成对象。如果希望大文件不经过应用服务器,也可以让客户端用预签名 URL 直接上传到对象存储,应用服务只保存上传状态。 ## 十二、权限系统 @@ -1030,7 +1030,8 @@ SELECT id, title FROM sys_order ORDER BY id; 常见做法是在 Mapper 或 DAO 层用注解标记需要过滤的查询,由 AOP 或 MyBatis 拦截器在执行前把范围条件追加到 WHERE 中,业务代码不需要关心范围。这种做法需要注意以下几点: - 条件的值必须通过参数绑定传入,不能拼接字符串; -- 多表关联或子查询中,条件要加在正确的表别名上,拦截器需要能正确解析 SQL; +- 多表关联或子查询中,条件要加在正确的表别名上,原条件还要先加括号。例如 status = 1 OR status = 2 直接拼上 AND dept_id = 20,AND 的优先级高于 OR,其它部门 status = 1 的订单也会返回,所以拦截器要用 SQL 解析器改写,而不是拼接字符串; +- MyBatis 拦截器要在 Executor.query 中、计算缓存键之前改写 SQL。在 StatementHandler.prepare 中改写时,缓存键仍按原 SQL 和参数计算,开启二级缓存后,另一个部门的用户会命中前一个用户的查询结果; - 分页的总数(COUNT)也要带同一条件。张三的订单列表只有 2 条,不带条件的 COUNT(*) 却返回 4,分页会显示错误的总数; - 修改和删除同样要带范围条件。张三修改订单 2 时,执行 UPDATE sys_order SET amount = 1 WHERE id = 2 AND creator_id = 1001,影响行数为 0,应用据此拒绝请求。这是为了防止不安全的直接对象引用(Insecure Direct Object Reference, IDOR),即用户只改动 URL 中的 ID 就访问到别人的数据; - 部门有层级时,DEPT 的范围还应包括下级部门,可以在部门表中保存祖先路径,或使用闭包表; @@ -1468,12 +1469,12 @@ OBJECT ENCODING inbox:0 -> listpack ### A4 MySQL 主键点查的并发扫描 -环境:MySQL 8.0.44,通过 unix socket 连接;Apple M1 Pro(10 核),macOS 15.8.1;客户端 mysqlslap 与服务端在同一台机器上。表 `t1_item` 有 10 万行,主键为 `id BIGINT`,表大小约 5.5 MB,小于 128 MB 的 InnoDB 缓冲池,预热后查询都在内存中完成。 +环境:MySQL 8.0.44,通过 unix socket 连接;Apple M1 Pro(10 核),macOS 15.8.1;客户端 mysqlslap 与服务端在同一台机器上。表 `item` 有 10 万行,主键为 `id BIGINT`,表大小约 5.5 MB,小于 128 MB 的 InnoDB 缓冲池,预热后查询都在内存中完成。 查询为 2000 条不同主键的语句循环执行,形如: ```sql -SELECT name, price, stock FROM t1_item WHERE id = 48213; +SELECT name, price, stock FROM item WHERE id = 48213; ``` 命令(每个并发数执行 3 次,每次 10 万条查询): @@ -1494,7 +1495,7 @@ mysqlslap --user=root --socket= --create-schema=demo \ | 64 | 0.557 | 179476 | 0.357 | | 128 | 0.522 | 191510 | 0.668 | -吞吐量为实际执行条数除以耗时。mysqlslap 的 `--number-of-queries` 指每个客户端约执行的条数([mysqlslap 手册](https://dev.mysql.com/doc/refman/8.0/en/mysqlslap.html)),64 和 128 个连接时取整后实际分别执行 99968 条。平均响应时间为耗时除以每个客户端的条数,即并发数 ÷ 吞吐量,包含客户端自身的开销。32 个连接的单次耗时在 0.557 至 0.927 秒之间波动。 +吞吐量为实际执行条数除以耗时。mysqlslap 的 `--number-of-queries` 是全部客户端合计的条数,按连接数向下取整平分(手册写作每个客户端的上限,与源码 mysqlslap.cc 中按连接数相除的实现不一致),64 和 128 个连接时实际分别执行 99968 条。平均响应时间为耗时除以每个客户端的条数,即并发数 ÷ 吞吐量,包含客户端自身的开销。32 个连接的单次耗时在 0.557 至 0.927 秒之间波动。 原始输出(并发数 16 的一段): @@ -1509,10 +1510,10 @@ Benchmark ### A5 热点行与分散行的 UPDATE -环境同[附录 A4](#a4-mysql-主键点查的并发扫描)。表 `t1_stock` 有 100 行(`sku_id` 为 1 至 100,`sold` 为计数)。热点负载为: +环境同[附录 A4](#a4-mysql-主键点查的并发扫描)。表 `sku_stock` 有 100 行(`sku_id` 为 1 至 100,`sold` 为计数)。热点负载为: ```sql -UPDATE t1_stock SET sold = sold + 1 WHERE sku_id = 1; +UPDATE sku_stock SET sold = sold + 1 WHERE sku_id = 1; ``` 分散负载为 2000 条语句,`sku_id` 为 1 至 100 之间的随机数。两者都是自动提交,每个并发数执行 20000 条,3 次平均。服务端的 `innodb_flush_log_at_trx_commit` 和 `sync_binlog` 均为默认值 1,每次提交都要刷盘。 @@ -1567,7 +1568,7 @@ print(params / 1e9, kv_per_token) ### A9 分片上传的最小实现与实测 -环境:Apple M1 Pro,macOS 15.8.1;Python 3.13.7,标准库 http.server 和 hashlib,pymysql 2.2.8;MySQL 8.0.44,demo 库中三张表 t2_upload、t2_part、t2_file。测试文件为 300 MiB(314572800 字节),由固定种子的伪随机数生成,分片 5 MiB,共 60 片。 +环境:Apple M1 Pro,macOS 15.8.1;Python 3.13.7,标准库 http.server 和 hashlib,pymysql 2.2.8;MySQL 8.0.44,demo 库中三张表 upload_task、upload_part、file_meta。测试文件为 300 MiB(314572800 字节),由固定种子的伪随机数生成,分片 5 MiB,共 60 片。 服务端的分片接收和合并(核心片段,省略建表、路由和部分错误分支): @@ -1583,18 +1584,18 @@ def put_part(self, uid, n): if not buf: # 客户端中途断开 break f.write(buf); h.update(buf); md5.update(buf); got += len(buf) - expect = q("SELECT expect_sha256 FROM t2_part WHERE upload_id=%s AND part_no=%s", + expect = q("SELECT expect_sha256 FROM upload_part WHERE upload_id=%s AND part_no=%s", (uid, n), 'one') if got != length or h.hexdigest() != expect['expect_sha256']: os.remove(tmp) return self.send(400 if got != length else 409, {'error': 'part rejected'}) os.replace(tmp, part_path(uid, n)) # 同号分片被覆盖 - q("UPDATE t2_part SET size=%s, etag=%s WHERE upload_id=%s AND part_no=%s", + q("UPDATE upload_part SET size=%s, etag=%s WHERE upload_id=%s AND part_no=%s", (got, md5.hexdigest(), uid, n)) self.send(200, {'part': n, 'etag': md5.hexdigest()}) # 服务端:完成上传。条件更新保证只有一个请求进入合并,合并时流式计算整文件哈希 -if q("UPDATE t2_upload SET status='completing' WHERE id=%s AND status='uploading'", (uid,)) == 0: +if q("UPDATE upload_task SET status='completing' WHERE id=%s AND status='uploading'", (uid,)) == 0: return self.send(409, {'error': 'being completed or aborted'}) # 此处省略检查缺片、逐片核对哈希的代码 h = hashlib.sha256() @@ -1747,10 +1748,10 @@ $ 客户端 status e355c1f1002e409d upload directory exists after abort: False == E1 数据库状态 -t2_upload by status: (('aborted', 1), ('done', 2)) -t2_file rows: 2 -t2_part (upload_id, rows, uploaded): ('762fa9bd16a44d31', 20, Decimal('20')) -t2_part (upload_id, rows, uploaded): ('b80db1c51e31407b', 60, Decimal('60')) +upload_task by status: (('aborted', 1), ('done', 2)) +file_meta rows: 2 +upload_part (upload_id, rows, uploaded): ('762fa9bd16a44d31', 20, Decimal('20')) +upload_part (upload_id, rows, uploaded): ('b80db1c51e31407b', 60, Decimal('60')) == 服务端日志 listening on 49696 @@ -1782,7 +1783,7 @@ SHA-256 扫描 300 MiB 实测 0.31 s,换算 2 GiB 约 2.12 s ### A11 权限判断与执行计划 -环境:MySQL 8.0.44,macOS(Apple M1 Pro),库 t3_rbac,客户端字符集 utf8mb4。判断查询与正文相同,五个用例只替换 user_id、请求方法和路径。原始输出如下: +环境:MySQL 8.0.44,macOS(Apple M1 Pro),库 rbac,客户端字符集 utf8mb4。判断查询与正文相同,五个用例只替换 user_id、请求方法和路径。原始输出如下: ```text +--------------------------------+---------+ @@ -1795,13 +1796,13 @@ SHA-256 扫描 300 MiB 实测 0.31 s,换算 2 GiB 约 2.12 s | 1005 GET /api/orders (no role) | 0 | +--------------------------------+---------+ -+----+-------------+-------+------------+--------+-----------------------+---------------+---------+--------------------------+------+----------+-------------+ -| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | -+----+-------------+-------+------------+--------+-----------------------+---------------+---------+--------------------------+------+----------+-------------+ -| 1 | SIMPLE | p | NULL | const | PRIMARY,uk_method_url | uk_method_url | 1056 | const,const | 1 | 100.00 | NULL | -| 1 | SIMPLE | ur | NULL | ref | PRIMARY,idx_role_id | PRIMARY | 8 | const | 1 | 100.00 | Using index | -| 1 | SIMPLE | rp | NULL | eq_ref | PRIMARY,idx_perm_id | PRIMARY | 16 | t3_rbac.ur.role_id,const | 1 | 100.00 | Using index | -+----+-------------+-------+------------+--------+-----------------------+---------------+---------+--------------------------+------+----------+-------------+ ++----+-------------+-------+------------+--------+-----------------------+---------------+---------+-----------------------+------+----------+-------------+ +| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | ++----+-------------+-------+------------+--------+-----------------------+---------------+---------+-----------------------+------+----------+-------------+ +| 1 | SIMPLE | p | NULL | const | PRIMARY,uk_method_url | uk_method_url | 1056 | const,const | 1 | 100.00 | NULL | +| 1 | SIMPLE | ur | NULL | ref | PRIMARY,idx_role_id | PRIMARY | 8 | const | 1 | 100.00 | Using index | +| 1 | SIMPLE | rp | NULL | eq_ref | PRIMARY,idx_perm_id | PRIMARY | 16 | rbac.ur.role_id,const | 1 | 100.00 | Using index | ++----+-------------+-------+------------+--------+-----------------------+---------------+---------+-----------------------+------+----------+-------------+ ``` ### A12 数据范围、分页总数与写操作 @@ -1947,7 +1948,7 @@ return 0 10400 ms 拒绝 剩余令牌 0.0 ``` -内存用 MEMORY USAGE SAMPLES 0 测量:写入 1000 条记录的 ZSET(成员形如 req-1000000,分值为毫秒时间戳)为 102016 字节,多次运行在 101888~102384 字节之间;固定窗口的计数器为 56 字节,令牌桶的哈希表为 104 字节。 +内存用 MEMORY USAGE \ SAMPLES 0 测量:写入 1000 条记录的 ZSET(成员形如 req-1000000,分值为毫秒时间戳)为 102016 字节,多次运行在 101888~102384 字节之间;固定窗口的计数器为 56 字节,令牌桶的哈希表为 104 字节。 单个键的吞吐量用 redis-benchmark 测量(本机回环,50 个连接,每项 200000 次,同一个键),令牌桶脚本先用 SCRIPT LOAD 加载: @@ -2258,40 +2259,40 @@ ignoreExceptions(BizException):状态 CLOSED,失败 0 次,失败率 -1%, ### A19 防止超卖的几种写法 -环境:MySQL 8.0.44(默认的可重复读),Python 3 + PyMySQL,Apple M1 Pro。表 d5_goods(id, stock, version) 中商品 1001 的库存为 100,d5_order 的主键为 (user_id, goods_id)。每种写法用 64 个线程,每个用户请求 1 次,每个线程使用自己的连接(自动提交): +环境:MySQL 8.0.44(默认的可重复读),Python 3 + PyMySQL,Apple M1 Pro。表 goods(id, stock, version) 中商品 1001 的库存为 100,goods_order 的主键为 (user_id, goods_id)。每种写法用 64 个线程,每个用户请求 1 次,每个线程使用自己的连接(自动提交): ```python def naive(uid): # 先查询,大于 0 再扣减 - cur.execute("SELECT stock FROM d5_goods WHERE id = 1001") + cur.execute("SELECT stock FROM goods WHERE id = 1001") if cur.fetchone()[0] <= 0: return 'soldout' - cur.execute("UPDATE d5_goods SET stock = stock - 1 WHERE id = 1001") - cur.execute("INSERT INTO d5_order VALUES (%s, 1001)", (uid,)) + cur.execute("UPDATE goods SET stock = stock - 1 WHERE id = 1001") + cur.execute("INSERT INTO goods_order VALUES (%s, 1001)", (uid,)) return 'ok' def pessimistic(uid): # SELECT ... FOR UPDATE c.begin() - cur.execute("SELECT stock FROM d5_goods WHERE id = 1001 FOR UPDATE") + cur.execute("SELECT stock FROM goods WHERE id = 1001 FOR UPDATE") if cur.fetchone()[0] <= 0: c.rollback(); return 'soldout' - cur.execute("UPDATE d5_goods SET stock = stock - 1 WHERE id = 1001") - cur.execute("INSERT INTO d5_order VALUES (%s, 1001)", (uid,)) + cur.execute("UPDATE goods SET stock = stock - 1 WHERE id = 1001") + cur.execute("INSERT INTO goods_order VALUES (%s, 1001)", (uid,)) c.commit(); return 'ok' def optimistic(uid, retries=0): # 版本号,冲突时最多重试 retries 次 for _ in range(retries + 1): - cur.execute("SELECT stock, version FROM d5_goods WHERE id = 1001") + cur.execute("SELECT stock, version FROM goods WHERE id = 1001") stock, ver = cur.fetchone() if stock <= 0: return 'soldout' - n = cur.execute("UPDATE d5_goods SET stock = stock - 1, version = version + 1 " + n = cur.execute("UPDATE goods SET stock = stock - 1, version = version + 1 " "WHERE id = 1001 AND version = %s", (ver,)) if n == 1: - cur.execute("INSERT INTO d5_order VALUES (%s, 1001)", (uid,)) + cur.execute("INSERT INTO goods_order VALUES (%s, 1001)", (uid,)) return 'ok' return 'conflict' def conditional(uid): # 条件更新 - n = cur.execute("UPDATE d5_goods SET stock = stock - 1 WHERE id = 1001 AND stock > 0") + n = cur.execute("UPDATE goods SET stock = stock - 1 WHERE id = 1001 AND stock > 0") if n == 0: return 'soldout' - cur.execute("INSERT INTO d5_order VALUES (%s, 1001)", (uid,)) + cur.execute("INSERT INTO goods_order VALUES (%s, 1001)", (uid,)) return 'ok' ``` @@ -2683,6 +2684,22 @@ WHERE o.pay_txn IS NOT NULL AND o.channel = 'wxpay' +--------------+----------+-------------+--------------+---------+------------------------+ ``` +### A26 脚本中的 math.random + +环境:Redis 7.4.1,Apple M1 Pro,macOS,默认配置。连续调用三次同一个脚本,重启 Redis 后再调用三次: + +```bash +for i in 1 2 3; do redis-cli eval "return math.random(1,1000000)" 0; done +redis-cli shutdown nosave # 重新启动 redis-server 后再执行上一行 +``` + +```text +重启前:396465 840486 353337 +重启后:396465 840486 353337 +``` + +三次调用的结果不同,说明不是每次调用都把种子重设为同一个值;重启后得到同样的序列,说明初值是固定的。源码中 math.random 被替换为 redisLrand48,只有脚本调用 math.randomseed 时才重设种子(src/rand.c、src/script_lua.c)。 + ## 参考资料 - [Redis: Scripting with Lua](https://redis.io/docs/latest/develop/programmability/eval-intro/)