feat: add ThumbUP 项目 — 70 questions across 5 subtopics (cache-protection, two-level-cache, lua-bucketing-sync, heavykeeper-topk, distributed-lock)
Deploy Examination / deploy (push) Successful in 20s

This commit is contained in:
2026-09-09 21:59:46 +08:00
parent 3447dbd4ee
commit 85710ded73
21 changed files with 2222 additions and 0 deletions
@@ -0,0 +1,137 @@
{
"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 分。"
}
]
}
]
}