{ "topic": "lua-bucketing-sync", "type": "code_reading", "schema_version": "1.0.0", "generated": "2026-09-09T21:42:00+08:00", "questions": [ { "id": "cr-001", "type": "code_reading", "difficulty": 3, "tags": [ "lua-bucketing-sync", "lua" ], "question": "分析以下点赞 Lua 脚本,回答子问题。", "explanation": "本题考查对 Redis Lua 脚本原子操作细节的理解,包括 Key 设计和空值处理。", "code": "local tempThumbKey = KEYS[1]\nlocal userThumbKey = KEYS[2]\nlocal userId = ARGV[1]\nlocal blogId = ARGV[2]\n\nif redis.call('HEXISTS', userThumbKey, blogId) == 1 then\n return -1\nend\nlocal hashKey = userId .. ':' .. blogId\nlocal oldNumber = tonumber(redis.call('HGET', tempThumbKey, hashKey) or 0)\nlocal newNumber = oldNumber + 1\nredis.call('HSET', tempThumbKey, hashKey, newNumber)\nredis.call('HSET', userThumbKey, blogId, 1)\nreturn 1", "language": "lua", "sub_questions": [ { "index": 1, "type": "single_choice", "question": "脚本中 hashKey 的拼接格式是什么?这样设计的目的是什么?", "options": { "A": "userId:blogId — 将临时计数按用户维度隔离,避免不同用户对同一博客的计数互相覆盖", "B": "blogId:userId — 方便按博客维度批量查询", "C": "userId — 简化 Key 结构,牺牲并发性", "D": "tempThumbKey:userId — 组合键防止碰撞" }, "answer": "A", "explanation": "hashKey = userId .. ':' .. blogId,使用 '用户ID:博客ID' 格式。tempThumbKey 是按时间片分桶的 Hash,桶内需要区分不同用户的计数。以 'userId:blogId' 为 field,既能保证不同用户对同一博客的计数互不干扰,也便于后续落库时按用户维度解析。" }, { "index": 2, "type": "short_answer", "question": "这段脚本的 'or 0' 在 HGET 返回值处理中起什么作用?如果去掉会有什么风险?", "explanation": "or 0 是 Lua 的短路求值语法,HGET 不存在时返回 nil,nil or 0 得到 0,确保 tonumber 正常运行。", "answer": "or 0 的作用是在 HGET 返回 nil(field 不存在)时提供默认值 0,确保 tonumber 不会收到 nil 参数。如果去掉,当用户第一次对某博客点赞时 HGET 返回 nil,tonumber(nil) 会报错导致整个 Lua 脚本执行失败,点赞功能不可用。", "keywords": [ "or 0", "默认值", "nil", "tonumber", "第一次点赞" ], "scoring_rubric": "答出 nil 处理/默认值给定给 2 分;答出 tonumber 对 nil 会报错给 2 分;答出会导致脚本执行失败/点赞不可用给 1 分。满分 5 分。" } ] }, { "id": "cr-002", "type": "code_reading", "difficulty": 4, "tags": [ "lua-bucketing-sync", "sync-job" ], "question": "分析以下定时落库核心逻辑,回答子问题。", "explanation": "本题考查定时落库任务的时间片计算逻辑、边界条件处理以及 Spring 事务管理对 Redis+DB 双写的影响。", "code": "@Scheduled(fixedRate = 10000)\n@Transactional(rollbackFor = Exception.class)\npublic void run() {\n DateTime nowDate = DateUtil.date();\n int second = (DateUtil.second(nowDate) / 10 - 1) * 10;\n if (second == -10) {\n second = 50;\n nowDate = DateUtil.offsetMinute(nowDate, -1);\n }\n String date = DateUtil.format(nowDate, \"HH:mm:\") + second;\n syncThumb2DBByDate(date);\n}", "language": "java", "sub_questions": [ { "index": 1, "type": "single_choice", "question": "假设当前时间是 10:00:05,run() 方法计算出的 date 值是什么?", "options": { "A": "10:00:00", "B": "09:59:50", "C": "10:00:10", "D": "09:59:00" }, "answer": "B", "explanation": "second = (5 / 10 - 1) * 10 = (0 - 1) * 10 = -10。触发边界处理:second = 50, nowDate 前推一分钟为 09:59:xx。date = '09:59:' + 50 = '09:59:50'。这处理的是上一分钟最后一个 10 秒桶(09:59:50~09:59:59)的数据。" }, { "index": 2, "type": "single_choice", "question": "方法上的 @Transactional 注解对落库操作意味着什么?如果 syncThumb2DBByDate 中批量写入 DB 成功但批量删除 Redis Key 失败,会发生什么?", "options": { "A": "事务会回滚,DB 写入被撤销,Redis Key 保留,后续补偿任务会重新同步", "B": "事务只回滚 DB 操作,Redis 操作不受事务管理所以不受影响", "C": "事务会覆盖 Redis 操作,导致 Redis 数据也被回滚", "D": "不会发生异常,因为 Redis 删除在虚拟线程中执行" }, "answer": "A", "explanation": "@Transactional 注解使得 syncThumb2DBByDate 方法在同一个事务中执行。如果 DB 操作成功但 Redis Key 删除失败(抛异常),Spring 事务管理器会回滚 DB 的所有写入。Redis 删除使用虚拟线程异步执行,不受 Spring 事务管理——如果 Redis 删除在同步块中失败,事务回滚确保 DB 一致性;如果 Redis 删除在异步块中失败,事务已完成提交,Redis Key 残留但数据已写入 DB,补偿任务会再次处理(可能产生少量重复写入但不会丢数据)。" } ] }, { "id": "cr-003", "type": "code_reading", "difficulty": 4, "tags": [ "lua-bucketing-sync", "sync-job", "lua" ], "question": "对比点赞和取消点赞两个 Lua 脚本,分析取消点赞的实现逻辑。", "explanation": "本题考查对两个互逆 Lua 脚本的对比分析能力,包括幂等性保障和落库阶段的数据处理。", "code": "-- 取消点赞 Lua 脚本\nif redis.call('HEXISTS', userThumbKey, blogId) ~= 1 then\n return -1\nend\nlocal hashKey = userId .. ':' .. blogId\nlocal oldNumber = tonumber(redis.call('HGET', tempThumbKey, hashKey) or 0)\nlocal newNumber = oldNumber - 1\nredis.call('HSET', tempThumbKey, hashKey, newNumber)\nredis.call('HDEL', userThumbKey, blogId)\nreturn 1\n\n-- 点赞 Lua 脚本\nif redis.call('HEXISTS', userThumbKey, blogId) == 1 then\n return -1\nend\nlocal hashKey = userId .. ':' .. blogId\nlocal oldNumber = tonumber(redis.call('HGET', tempThumbKey, hashKey) or 0)\nlocal newNumber = oldNumber + 1\nredis.call('HSET', tempThumbKey, hashKey, newNumber)\nredis.call('HSET', userThumbKey, blogId, 1)\nreturn 1", "language": "lua", "sub_questions": [ { "index": 1, "type": "single_choice", "question": "取消点赞脚本中计数可能变为负数(oldNumber=0 时 newNumber=-1),这在落库阶段是如何处理的?", "options": { "A": "负数表示取消点赞,落库时执行数据库中的 DELETE 操作", "B": "负数会被忽略,不做任何处理", "C": "负数表示取消操作,落库时执行数据库中的反向扣减(如 thumb_count - 1)", "D": "脚本中已有保护逻辑,计数不会为负" }, "answer": "C", "explanation": "落库阶段会区分正数(新增点赞)和负数(取消点赞)。正数对应数据库 INSERT,负数对应数据库 DELETE(取消用户点赞记录)或反向更新(blog 的 thumb_count 减去绝对值)。脚本本身不做负数保护,因为如果用户之前通过临时 Key 累加了计数但尚未落库,此时取消点赞,计数变为负值是正确的语义——表示'撤销之前的一次点赞'。" }, { "index": 2, "type": "short_answer", "question": "两个脚本都先检查 HEXISTS 再做后续操作。请解释这两个脚本中 HEXISTS 检查条件的区别(== 1 和 ~= 1),以及各自保证了什么语义。", "explanation": "两个脚本通过 HEXISTS 前置条件检查实现互斥语义,配合 Lua 原子性保证幂等操作。", "answer": "点赞脚本检查 HEXISTS == 1(已存在),如果存在直接返回 -1,保证幂等性——用户已赞过的博客不会重复计数。取消点赞脚本检查 HEXISTS ~= 1(不存在),如果不存在直接返回 -1,保证幂等性——用户未赞过的博客不能执行取消操作。两个脚本通过前置条件检查,在 Lua 原子性保证下实现了操作的互斥语义。", "keywords": [ "== 1", "~= 1", "幂等性", "HEXISTS", "互斥", "原子性" ], "scoring_rubric": "答出 == 1 表示'已赞则拒绝'给 2 分;答出 ~= 1 表示'未赞则拒绝'给 2 分;答出两者共同保证幂等性/互斥语义给 1 分。满分 5 分。" } ] } ] }