2、密码同步核心原理:同步流程、状态机设计、数据模型抽象
密码同步,说白了就是让两个端——比方说DLP终端和服务器——的密码状态保持一致。但我刚接触这活儿时,以为就是个“A把密码发给B,B存下来”的事。嗯,后来发现水挺深。
我经手的第一个同步项目就踩了坑:终端改了密码,服务器没收到确认,结果两边各走各的,用户被锁了一整天。今天咱们就聊聊同步的核心原理,把流程、状态机、数据模型这三个点掰开揉碎。
2.1 同步流程:从操作到生效的四步舞
从一个密码修改操作到两端真正同步,我习惯把它拆成四个阶段。你看这像不像一支四人舞?
| 阶段 | 动作 | 责任方 | 典型耗时 |
|---|---|---|---|
| 触发 | 用户修改密码,或管理员重置 | 认证模块 | <10ms |
| 传送 | 旧密码 + 新密码 打包成同步消息 | 同步代理 | 50~200ms |
| 验证 | 服务端校验消息完整性、防重放 | 密码服务 | 20~50ms |
| 回写 | 更新密码库,返回确认 | 存储层 | 10~100ms |
我在项目里遇到过一个问题:传送阶段的网络抖动导致消息重复发送,服务端连续收到两条相同的同步请求。结果呢?密码被覆盖了两次,看起来一样,但状态机乱了。后来我们加了幂等校验——每条消息带唯一ID,服务端去重——才算解决。
避坑指南:我曾经因为没处理好网络超时,让终端误以为同步失败,反复重试,最后把服务端打成了“假死”。解决方案是引入指数退避重试 + 最大重试次数上限。
2.2 状态机设计:每个密码都有自己的生命周期
你想想看,一个密码从创建到生效,中间经历了多少种状态?如果不用状态机管着,代码会乱成一锅粥。
我个人习惯用四个核心状态描述一次同步任务:
- PENDING:密码已修改,等待同步
- IN_FLIGHT:同步消息已发出,等待服务端确认
- COMMITTED:服务端已确认,密码生效
- FAILED:同步失败,需要处理
为什么是这四个?少了不够用,多了又复杂。我以前试过把“服务端收到但未入库”单独作为一个状态,后来发现跟IN_FLIGHT区分不开——你根本不知道是网络丢了还是入库慢了。简化后清晰多了。
记住一个关键点:状态变迁只能由确认事件触发,不能靠超时猜测。比如收到服务端的COMMIT_ACK才能从IN_FLIGHT转到COMMITTED,别用定时器瞎猜。
状态机还有个隐藏作用:防死锁。我记得有次线上事故,密码同步卡在IN_FLIGHT状态十多分钟,因为服务端重启后丢失了上下文。后来我们加了个“兜底定时器”:超过30秒还收不到确认,状态自动切到FAILED,然后走手工恢复流程。
2.3 数据模型抽象:给密码找一个合适的“容器”
数据模型该怎么设计?我见过直接把密码明文扔进同步队列的,也见过把整个用户表都同步过去的。都不好。
我建议抽象成三个独立的数据结构,各司其职:
- PasswordDelta:描述一次密码变更的“增量”,包含旧哈希、新哈希、时间戳、操作者
- SyncTask:描述一次同步任务,含状态、重试次数、消息ID、目标节点
- PasswordRecord:最终在服务端存储的密码记录,包含当前哈希、盐值、历史版本(可选)
技巧:PasswordDelta里的旧哈希不是必须的,但加上它可以在服务端做“版本校验”:如果服务端当前哈希与旧哈希不符,说明中间有人插队修改,直接拒绝本次同步。这个设计在多人管理同一账号时特别管用。
来看看一个简化版的PasswordDelta和SyncTask定义:
// PasswordDelta
{
"userId": "u_10086",
"oldHash": "sha256:abc...",
"newHash": "sha256:xyz...",
"timestamp": 1712500000,
"operator": "user"
}
// SyncTask
{
"taskId": "sync_20250315_001",
"deltaId": "delta_20250315_001",
"status": "IN_FLIGHT",
"retries": 0,
"maxRetries": 3,
"targets": ["server_a", "server_b"]
}
你可能会问:为什么要把旧哈希和新哈希放一起?直接传新哈希不行吗?我一开始也这么想,结果在一次双机热备切换时,服务端A的密码被改了一次,服务端B还没同步,又改了一次。A传过去的新哈希跟B当前的哈希对不上,但B不知道A的旧版本是什么——导致数据直接覆盖。有了旧哈希,B就能判断:“哦,我当前的版本跟你要改的那个版本不一样,我拒绝。”
2.4 一张图看懂密码同步核心
下面这张图把流程、状态机、数据模型串起来了。核心逻辑是:终端触发→构建密码增量→封装同步任务→状态机流转→服务端校验→更新记录。
图中三个关键点,我提一下:
- 同步流程串起构建Delta和SyncTask两步前置工作
- 状态机用PENDING→IN_FLIGHT→COMMITTED/FAILED管理生命周期
- 每个状态变迁都绑定一个明确的事件,没有“猜状态”的灰色地带
2.5 几点实战建议
讲了这么多,最后浓缩成三条我自己的血泪教训:
- 同步消息一定要带唯一ID。我见过太多因为消息重复导致的状态错乱,加上UUID之后问题自愈。
- 状态机不要设计成闭环。千万别搞“失败后自动重试回到PENDING”这种无限循环,设个最大次数,超出后直接告警人工介入。
- 数据模型尽量小。别往同步消息里塞用户姓名、部门信息,只要密码相关的字段。同步链路上每多一个字节,出错的概率就高一分。
对了,还有一个细节:密码哈希的算法一定要带盐,而且盐值要随新密码一起同步过去。服务端生成的新盐和终端生成的旧盐不一样?那也得保留终端生成的盐,否则用户下次登录时算出来的哈希对不上。