Files
examination/topics/thumbup/lua-bucketing-sync/code_reading.json
T

137 lines
9.0 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"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 分。"
}
]
}
]
}