资金安全
资金类交易主要的安全防范重点有防重、防错、防漏3个方面。
防重
防重设计主要为了避免产生重复数据,导致资金数据不一致。
产生问题的常见原因
- 前端多次提交
- 接口超时重试
- 消息重复消费
- 高并发场景
解决方式
- 请求签名机制
请求携带 timestamp 、 sign 、 nonce 三个参数;sign 由 timestamp、nonce、secret 组成,secret 通过 timestamp 和 nonce 通过非对称加密算法生成;保证请求的唯一性且请求数据(时间戳和 nonce)未被篡改。nonce 可通过布谷鸟过滤器验证该请求是否已经被提交过。 - 数据库唯一性验证
绝大数情况下,为了防止重复数据的产生,我们都会在表中加唯一索引,这是一个非常简单,并且有效的方案,如订单 id、用户 id 等。如 id 不唯一,可通过联合唯一索引,用多个字段唯一确定一条数据。通过这种方式,第一次请求数据可以插入成功。但后面的相同请求,插入数据时会抛出异常,表示唯一索引有冲突。 乐观锁
利用 CAS 机制,以数据库操作为例
1、查询数据得到版本号。
2、经过版本号去更新,版本号匹配则更新,版本号不匹配则不更新。-- 假如查询出的version为1 select version from table_name where userid = 10; -- 给用户的帐户加10 update table_name set money = money -10, version = version + 1 where userid = 10 and version = 1
悲观锁
悲观锁认为别人每次去拿数据都会修改这条数据,所以每次拿数据的时候,都会使数据处于锁定状态。(该方案影响接口性能)select * from test where user_id =123 and act_id='ac' for update
注意:在 MySQL 中,存储引擎必须用innodb,因为它才支持事务。同时锁表行记录条件一定要使用主键或者唯一建,不然会将整张表都被锁住。
- 分布式锁
执行方法时,先根据业务惟一的id获取分布式锁,获取成功,则执行,失败则不执行。分布式锁获取必须依赖外部系统,如 zookeeper、redis 等实现的分布式锁。 有限状态机
若是业务上须要修改订单状态,例如订单状态有:1-下单、2-已支付、3-完成、4-撤销等状态。设计时最好只支持状态的单向改变。这样在更新的时候就能够加上条件,屡次调用也只会执行一次。例如想把订单状态更新为完成状态,则以前的状态必须为已支付。update `order` set status=3 where id=123 and status=2;
- 防重表
增长一个防重表,业务惟一的id做为惟一索引,如订单号,当想针对订单作一系列操做时,能够向防重表中插入一条记录,插入成功,执行后续操做,插入失败,则不执行后续操做。本质上能够当作是基于MySQL实现的分布式锁。根据业务场景决定执行成功后,是否删除防重表中对应的数据。
防错
防错是指防止交易关键数据的记录错误或被篡改,如资金来源、资金去向、用户余额等,对应 STRIDE 威胁模型中的篡改和否认。
解决方式
数据签名
在资金数据记录中(用户余额表、用户流水表等),可以考虑对关键数据建立签名,在取用数据时校验签名,以防止关键数据被恶意或意外篡改(非预定的程序逻辑,如手动操作数据库等)。
外部依赖状态处理
外部依赖系统的返回值,需要确保每个返回值都有被覆盖。如明确成功、明确失败、非明确失败(接口返回超时,未收到结果)、未知返回值。非明确失败通过状态查询接口明确状态,明确失败及未知返回值需要将异常数据回滚。
冗余判断
涉及资金问题时,在资金处理链的每一个节点,都需要判断资金是否足够,如果不足,则不执行后续操作,直接返回失败,防止出现金额明显错误等情况。
对账机制
建立订单、流水、余额三要素的对账机制;对比资金的加减是否一致,防止消息队列消息丢失、处理异常带来的数据不一致的问题。
防漏
防漏是指防止数据的遗漏。主要防止在极端异常情况下,服务中断导致的订单丢失或退款流程异常终止。
对账机制
建立订单、流水、余额三要素的对账机制;对比资金的加减是否一致,防止消息队列消息丢失、处理异常带来的数据不一致的问题。
补单机制
当对账出现问题时,应当具备对遗漏的数据进行补单的机制。由于涉及到资金流转,是自动补单还是人工接入需要根据实际情况决策。
可重入机制
对于服务中断导致的遗漏情况,服务应该具备各阶段异常的重入能力。记录在资金处理链中每一步的处理上下文以及对应的结果,使得出现问题的的订单可以通过同步重试或者异步任务重试的方式继续处理成功。