第2章 容错设计基本原则:优雅降级、幂等性、原子性、超时与重试机制、熔断与限流在密码改造中的应用
做密码改造这几年,我最大的感受就是:再完美的改造方案也扛不住网络抖动、服务雪崩和人为失误。你想想看,加密机突然挂了、密钥库连接超时、老系统缓存了旧加密结果——哪一样都让人头疼。所以这一章我重点聊聊容错设计的五个基本功:优雅降级、幂等性、原子性、超时与重试、熔断与限流。这五个东西我用在了密码改造的每个环节,效果还不错。
优雅降级
说白了就是系统扛不住了,先给用户一个保底的加密服务。比如核心加密机超时,不要直接甩500,而是临时切到本地缓存密钥做一次降级签名,虽然强度低了点,但业务还能跑。我在改造一个支付系统时遇到过,上游加密服务挂了,结果下游所有交易报错。后来我加了一层降级开关:默认走硬件加密,失败后降级为软件加密(纯CPU算,但允许业务继续)。
- 降级要分等级:L1正常→L2降密级(比如从SM4降为AES-128,但业务能接受)→L3降功能(只返回缓存结果)
- 必须设置降级阈值:连续失败3次才触发,避免误判
- 降级后要自动尝试恢复,别一直卡在低水位
幂等性
密码改造里最容易被忽略的就是幂等。举个例子:加密请求因为网络超时,客户端重试了一次,结果密文多存了一份,业务对账对不上。我习惯在请求里带一个全局唯一ID(例如reqId),服务端收到先查表,如果ID已存在且结果相同,直接返回旧结果,不再重复执行加密。
// 幂等处理伪代码
if redis.exists(reqId):
return redis.get(reqId) // 直接返回之前的结果
else:
ciphertext = encrypt(plaintext, key)
redis.setex(reqId, ciphertext, 86400) // 存储一天
return ciphertext
你想想看,如果一次加密操作被重试了5次,每次都要重新生成密钥,那密钥管理系统迟早被拖垮。幂等既是容错,也是省钱。
原子性
密码改造经常涉及多步操作:更新密钥、加密数据、更新元数据。这三步必须要么全做,要么都不做。我见过一个事故:密钥更新成功,但数据加密了一半服务挂了,结果新密钥下既有旧密文也有新密文,恢复时根本分不清。原子性可以用本地事务或分布式事务保证,但密码改造场景我更推荐“先写日志再执行”的模式。
- 在数据库里插入一条操作日志(状态:待执行)
- 执行加密/密钥变更操作
- 更新日志状态为成功(或失败)
- 如果第2步失败,回滚:比如删除新密钥,或者恢复旧备份
超时与重试机制
密码服务对外部依赖很敏感,比如调用KMS(密钥管理服务)或者HSM(硬件安全模块),响应时间通常不可控。我建议每个外部调用都要设置超时,并且区分读和写:读操作超时短(比如2秒),写操作稍微长点(5秒),但绝不能无限等。
| 操作类型 | 推荐超时 | 重试策略 | 最大重试次数 |
|---|---|---|---|
| 读取密钥 | 2s | 立即重试一次+指数退避 | 3 |
| 加密/解密 | 5s | 指数退避(1s 2s 4s) | 3 |
| 密钥生成 | 10s | 不重试(幂等性靠请求ID保证) | 0 |
熔断与限流
熔断和限流是一对好兄弟,一个保护上游,一个保护下游。在密码改造中,熔断通常用在调用下游加密服务时——如果连续10次超时或报错,就打开熔断开关,直接返回降级结果,不再请求下游。限流则管住自己:比如每秒最多处理1000次加密请求,超过的直接返回429,防止把CPU和内存耗尽。
我一般结合实现:用滑动窗口统计失败率,达到阈值就熔断30秒。30秒后进入半开状态,试探一个请求,如果成功则关闭熔断,否则继续熔断。
这五个原则在密码改造中不是孤立用的。比如一次加密请求:先限流挡住洪峰→设置超时→超时后重试两次(带退避)→重试失败触发熔断→熔断打开后走优雅降级→降级过程中保持幂等性和原子性。你想想看,这套组合拳下来,系统是不是稳多了?