2026-09-24 19:15:00
上一篇介绍幂等键留下一些问题,幂等键有了,如果两个请求同时到达,应该如何处理?
未仔细考虑过并发问题的代码可能也试图使用缓存进行存在性检验,但两个请求同时到达时,会发生下面的流程。
请求 A:检查 Redis,不存在
请求 B:检查 Redis,不存在
请求 A:执行兑换
请求 B:执行兑换
请求 A:写入幂等结果
请求 B:写入幂等结果
两个请求都通过了存在性验证,走到业务流程,兑换了两次,问题出在这里:
if !exists(key) {
result := doBusiness()
save(key, result)
}
exists 和 save 是两个独立操作。从单个请求的视角看,这段代码没有问题:
以并发视角出发,第一步和第三步之间则存在一个明显的时间窗口。
请求 A 检查完成后,请求 B 完全可以在请求 A 写入结果之前,也完成一次检查。于是两个请求都认为「我是第一次请求」。
所以面对并发问题,真正需要保证的是对于同一个 Idempotency Key,同一时刻只能有一个请求获得业务执行权。
Redis 的 SET ... NX 很适合完成这个动作。
例如:
SET idempotency:{key} processing NX EX 300
其中:
NX:Key 不存在时才写入;EX 300:设置 5 分钟过期时间;processing:表示该请求正在处理中。这个操作在 Redis 中是原子的,于是两个请求同时到达时:
请求 A:SET ... NX -> 成功
请求 B:SET ... NX -> 失败
只有请求 A 获得执行权,请求 B 不再进入业务流程。
伪代码大概变成:
// 1. 原子抢占执行权
ok := setNX(key, "PROCESSING", 5*time.Minute)
if !ok {
return handleDuplicateRequest(key)
}
result, err := doBusiness()
if err != nil {
// 只有能够确认业务没有产生副作用时,
// 才可以安全释放 PROCESSING。
//
// 如果业务结果处于“不确定”状态,
// 不能简单 DEL,否则重试可能导致业务再次执行。
return handleBusinessFailure(key, err)
}
// 2. 保存成功结果,并设置更长的结果保留时间
saveResultWithTTL(key, result, 24*time.Hour)
return result
相比之前:
if !exists(key) {
result := doBusiness()
save(key, result)
}
原来的 “先检查、再决定是否执行” 存在竞争窗口,而 SET ... NX 将 “检查是否不存在 + 抢占执行权” 合并成了一个原子操作。
如果只做到这里,又会马上遇到几个新的问题,假设请求 A 抢占成功:
SET key processing NX EX 300
然后开始执行业务。
这时请求 B 带着相同的 Idempotency Key 过来了,它发现 Processing Key 已经存在。
注意:这里的 Processing 状态和 SUCCESS 状态,数据 TTL 是不同的,Processing 根据业务可能只有几分钟,而产生结果的 SUCCESS 状态时间会长很多。
问题来了:
所以幂等记录至少需要区分 不存在、PROCESSING、SUCCESS
请求第一次到达时,通过 SET ... NX 原子地把状态从 “不存在” 变成 PROCESSING。
业务执行成功后,再把状态更新为 SUCCESS,并保存原请求对应的响应结果。
例如:
{
"status": "success",
"http_status": 200,
"response": {
"code": 0,
"message": "success"
}
}
这样后续相同请求再次到达时,就不需要重新执行业务,可以直接返回第一次请求的结果。
假设请求 A 仍然处于 PROCESSING:
请求 A:PROCESSING
请求 B:再次请求
请求 B 此时有几种处理方式。
一种方式是立即返回:
HTTP/1.1 409 Conflict
例如:
{
"code": "request_in_progress",
"message": "A request with the same Idempotency-Key is still being processed"
}
客户端稍后重试即可。另一种方案是服务端短暂等待,请求 A 执行完成后,把 A 的结果直接返回给 B。
例如等待几十到几百毫秒,并轮询 Redis:
B -> 发现 PROCESSING
-> 等待
-> 查询
-> 等待
-> 查询
-> 发现 SUCCESS
-> 返回 A 的结果
这种方式客户端体验更好,但服务端实现也更复杂,而且会占用连接和计算资源。
实际项目中,更推荐这样判断:
短业务:可短暂等待
长业务:直接返回 processing/conflict,让客户端重试
前面的:
SET key processing NX EX 300
必须设置 TTL,因为服务可能在设置完 PROCESSING 后的处理过程中崩溃,如果缓存永不过期,那么这个 Idempotency Key 就永久处于处理中,没人知道它是不是还在运行。
之后所有重试都会被拒绝,本质上也是一种故障恢复机制。
然后就会出现新的问题:如果业务执行时间超过了 PROCESSING 的 TTL 怎么办?
例如 TTL 是 5 分钟:
00:00 请求 A 获得执行权
00:05 Redis Key 过期
00:06 请求 B 到达,重新 SET ... NX 成功
00:07 请求 A 仍然在执行业务
此时 A、B 又同时开始执行了。
所以 TTL 不能简单地拍脑袋设置。
对于执行时间可控的普通 HTTP API,可以让 TTL 明显大于业务正常执行时间。
对于耗时可能非常长的任务,则需要考虑:
本文使用 SET ... NX,只是为了原子地确定:谁是这个 Idempotency Key 对应请求的第一次执行者。
还有一个更加重要的问题。
即使 SET ... NX 本身是原子的,也不意味着整个业务流程是原子的。
例如兑换业务:
1. Redis 抢占 Idempotency Key
2. 数据库事务:扣减积分 + 创建兑换记录
3. 数据库 COMMIT 成功
4. Redis 保存 SUCCESS
假设数据库事务已经 COMMIT 成功,但服务在 Redis 保存 SUCCESS 之前崩溃,此时可能出现:
Redis:PROCESSING
数据库:兑换已经成功
客户端:没有收到成功响应
过一段时间 PROCESSING 过期,请求重新执行。
如果业务本身没有其他保护机制,就可能再次扣积分。
所以 Idempotency Key 能解决的是 HTTP 请求级别的重复提交问题,搭配 SET ... NX 能看住服务的大门,内部系统的业务 ‘幂等建设’ 仍需要仔细考量服务可能在任何阶段挂掉后的恢复机制,数据库层增加最终约束进行兜底。
下一篇,仍是关于 Idempotency Key ,还需要继续学习解决一个问题:
如果两个请求使用了相同的 Idempotency Key,但请求参数却不一样,应该怎么办?
2026-09-23 10:17:23
2026 年 9 月 22 日 Anthropic 发布 Claude Opus 5.5,同时赠送了一张重置卡(引入用户自主点击重置卡)。
同一天,OpenAI 发布 GPT-6 Sol 和 GPT-6 Luna 模型,也赠送了一张重置卡。
Yes 工程师提前过中秋!
2026-09-22 09:19:23
早上骑车路过一个施工现场,远远就看到有个人站在渣土车上 ‘观察’,直直的站着,两脚就踩着车斗的两个边框上(比图中更加夸张)。我猜是为了指导挖掘机填土,(如果有)安全员,看了肯定直呼哇塞。
2026-09-15 17:01:23
我表述想更新 Tag Name,过了一会儿它已经把测试、正式环境都更新好了。
过于‘智能’,问题是我从未配置过生产环境的操作流程、地址和密钥,以至于我看到生产环境已经生效,最后才怀疑到是它干的...

听说过很多 Agent 删数据库、删代码库、删工作区的... 第一次遇到这种翻我命令历史找线上环境地址和 token 自动操作不打招呼的,开眼界了 🤷♂️
吓得我赶紧添加了一段约束添加到 AGENTS.md 防一手儿这老小子。
## 外部环境与凭据安全
- **Agent 禁止操作 production**:不得连接、查询或修改线上 API、数据库、缓存、
主机、集群、云资源、管理后台、部署或任务系统;只读、dry-run、用户已登录或
用户提供凭据均不例外。
- 即使工具支持 production,Agent 也只能生成并本地验证命令、SQL、脚本和操作步骤,
由用户手动执行;不得通过 shell、浏览器、子 Agent 或其他方式间接代为调用。
- 外部操作前必须明确环境和实际 host;无法证明是本地、测试或明确允许的隔离环境时,
一律按 production 处理并拒绝执行。不得从文件名、历史命令或相似域名猜测环境。
- 缺少凭据、登录状态或环境信息属于合法阻塞条件。不得为完成任务而主动读取、搜索、
解析或恢复 shell history、密码管理器、系统钥匙串、浏览器存储、SSH 身份、
CLI 认证文件、日志、剪贴板或进程参数中的凭据。
- 泛化授权(如“调用接口”“修改数据”“使用已有登录”)不构成凭据来源授权。
非 production 操作只能使用用户明确指定的凭据来源或工具约定的标准认证链。
- 不得读取、打印、复制、导出、持久化或跨任务复用秘密值;配置检查只报告
“已设置 / 未设置 / 可用 / 不可用”。
- 可能触达 production 的工具必须要求显式提供环境、host 和 apply 参数,
不得自动发现凭据或目标环境;工具能力不代表 Agent 获得执行权限。
2026-09-15 11:45:00
早在几年前,Web 端就支持了单位代扣代缴员工个人所得税,当时参考小红书上的教程,就一直沿用 Windows 客户端的方式缴纳,其实 Web 端缴纳更加方便。
本文是对《非京籍个体户缴纳社保(十):新增并缴纳个人所得税-工资薪金》 一文的补充。
地址:https://etax.chinatax.gov.cn
登录后可以看到 Tab 有「单位办税」的选项,点击进入,在综合所得申报内,可以看到正常工资薪金所得。(员工信息需要重新录入)
而后查看申报信息是否准确,这里刚看到的时候有些奇怪,“应纳税额” 和 “已缴税额” 在客户端好像没这样显示。
不过是正确的(如果暂时不考虑个人承担的社保、公积金,每个月就是:工资收入 6000 − 基本减除费用 5000 − 赡养老人专项附加扣除 950 = 应纳税所得额 50 元(50 × 3% = 1.50 元)
到 2026 年 8 月:
累计收入:6000 × 8 = 48000
累计减除费用:5000 × 8 = 40000
累计赡养老人扣除:950 × 8 = 7600
累计应纳税所得额 = 48000 − 40000 − 7600 = 400 元
累计应纳税额 = 400 × 3% = 12 元
前 7 月已缴:1.5 × 7 = 10.50 元
8 月应缴:12 − 10.50 = 1.50 元
跟之前缴纳数额都是一致的,确认没问题后继续缴款。
选择银联缴纳的方式,缴税款即可。Web 端功能更丰富,查询统计模块支持 “单位申报记录查询” 和 “个人扣缴明细查询”
在单位申报记录查询页面,可以看到有显示 “扣缴客户端web版”,之前的记录都是 “客户端”
2026-09-10 12:05:16
自 2026 年 7 月起,北京市职工基本养老、失业、工伤及职工基本医疗保险(含生育)的月缴费基数上限和下限分别为:
相比 2025 年,下限由 7162 元上调至 7270 元,增加 108 元,涨幅约 1.51%;上限由 35811 元上调至 36348 元,增加 537 元,涨幅约 1.50%。
按 7270 元缴费基数,个体工商户以企业身份为经营者缴纳社保,承担单位和个人两部分费用明细如下:
作为对比,2025 年社保基数调整后,个体工商户最低缴费由 2540.42 元/月上升至 2667.27 元/月(上涨 126.85 元/月)。
数据来源