金融应用资金处理安全十问

1.死锁问题

投资文章奖励时,是企业账户给个人账户转账的模式,不同的文章奖励用户的顺序可能不一样,如果多线程执行如下逻辑
1)文章A:企业账户 -> 个人A,企业账户 ->个人B
2)文章B:企业账户 -> 个人B,企业账户 ->个人A
会有死锁吗?

实验

account表中,set autocommit=off; 账户1给2转,同时2给1转,几乎同时按下commit;会有deadlock吗?

实验结果是:

解决:

mysql自动第一条成功,第二条回滚了,接口报错,换个交易id重新上送即可

事务越小越好,批量处理顺序要高度一致(比如张同学的游戏业务调用我们的批量接口,我就告诉他,按账户字母顺序调用。各个调用若都顺序一致,就不会有死锁了)


2.防重问题

上游应用主动提出:这个接口能否增加一个交易id xx,由调用者传入,当id xx的转账交易第一次成功后,假如第二次再次传入id xx调用,则不进行真正的转账,而是返回交易成功
再这样做的目的是防止重复交易

解决:

我们要增加存储这个交易id(其实就是类似工行的渠道事件编号),重复交易具有了幂等性

通证的实战应用

    1. 新增用户过程中,如果调用方uuid上送重复了,返回的报文是一模一样的
    1. 转账过程中,如果上送的eventId重复了,会拒绝转账第二次

3.多更问题

多线程更新账户余额,注意这个问题和死锁问题不同,死锁问题关键是两个人一个占A要B,另一个占B要A;而多线程更新账户余额是两个人都是更新A,但是没协调好,取了脏数据,所以用乐观锁


update table1 set balance = balance - 1

where uid='............'

and balance - 1 >= 0   //查看余额是否足够,如果返回为0,说明余额不足,提示“余额不足”,同时balance也不会减去1(因为不符合update的条件)

解决:

关键是操作的同时进行余额检查,比较简洁方便,程序判断如果更新记录为0,发现是有问题,报告前端处理

扣费例子:

多并发扣费时如何保持余额一致性

1.select balance from tb_account where uid=100;

2.程序判断balance的值是否足够抵扣。

3.update tb_account set balance = balance - 28.00, update_time = sysdate() where uid=100;
通常情况下,这种余额判断方法在高并发且不加锁的情况下是非常不可靠的 如下是放在一个事务中,比较可靠


create procedure proc_account_balance_dec ( in_money decimal(8,2), in_uid bigint, OUT status int )  
BEGIN  

DECLARE from_account_balance decimal(8,2);  

START TRANSACTION;  

SELECT balance INTO from_account_balance FROM tb_account  
    WHERE uid = in_uid FOR UPDATE;  

IF from_account_balance>=in_money THEN   
    UPDATE tb_account SET balance = balance - in_money , update_time = sysdate()  
        WHERE uid = in_uid;  
    COMMIT;  

    SET status=1;  
ELSE   
    ROLLBACK;  
    SET status=0;  
END IF;  

END;

另外有网易项目实战的例子 TransferImpl.java 中,update更新完成后要获取余额写入另外的表


@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT, timeout = 60, rollbackFor = Exception.class)
public int transfer(List<Transfer> transfers, TransferEvent te) throws Exception {
      //略去部分代码
      accountDao.updateBalanceByAccountId(value, uid, accountId);
      // MySQL Innodb使用MVCC,必须要在update语句之后读取,update语句中包含当前读,会加x锁,其他事务不能更新当前这条记录
      account = accountDao.selectByIds(uid, accountId);
      t.setBalance(BigDecimal.ZERO);

这种情况下,获取最新余额的动作一定要在锁中,防止脏读。


4.热点问题

热点指热点账户,目前我们user表是分开两张表,但是account表是一张表,其实user表是读多写少,而account表是读少写多,account应该分两张才是,

部分企业账户是热点账户,通过分表(分库分表的雏形),减低了事务冲突的概率

  • 现在通证的方案就是去热点的方案,但是提升有限,哪怕是在100批量情况下,也只是从1700提升到2200

  • 还有一个是网易金融的方案,transfer_his表中也有余额字段,最新余额+ merkle树 但是存在同时读余额,然后2条记录处理后余额为脏(这个问题可以用乐观锁解决,具体是利用DDB的BF字段来做,具体看 通证和人民币架构再优化:更新余额从update方式变成insert方式.md)

因为热点问题而影响业务设计的实例

通证的transfer_his 表结构,其均衡字段都是uid字段,那么涉及批量业务时,一个企业就有好多条记录,影响数据分布的均衡,

这个问题,方案是:

  • B2B 和 C2C,是两边都记录

  • B2C 和 C2B,是只记录C端的,作为中心端的企业,其转账记录是不记录的

但是目前人民币的transfer表,是两边都记录的,我看了下 电商和内容的两个账户,转账记录占了一半,这个还是问题,后续要考虑数据分布严重不均衡的情况


5.负数问题

如果是业务需求要求有负数,看看具体场景,对信用额度的控制,及后续处理等等

但是如果是技术性失误造成,则要解决的,我们定义Decimal(38,18)是有可能是负数的,晚间要扫描并报警修复

目前通证和人民币对此处理不同:

  • 通证,因为企业余额是异步更新的,所以企业余额允许为负,最多10W负数额度

  • 人民币,所有账号不能为负


6.精度问题

我们目前应该不存在这个问题了,因为比起银行的数据存储,我们采用了超大精度Decimal(38,18)我们会精确到一个聪,但是注意,这个只能加减,不能乘除,产品谈一个倍数需求时,必须谈精度问题

通证和人民币实例

我们采用AOP的方式,在 java 类中统一处理了精度

目前人民币实际可以存储6位小数,对外接口统一是2位 见 Const.java 中; 而通证是18位,对外提供5位


public static int MAX_AMOUNT_SCALE = 2;

7.溢出问题

java中int32位,最大金额是如下,对部分日常业务是够了,但是某些变态的统计,比如n年的累加,是不够的


int max=2147483647 int min=-2147483648

long max=9223372036854775807  long min=-9223372036854775808



如上long的最大值也不能涵盖uint256这个交易所常用的数据类型,如果定义用long,必然存在溢出

著名的金额溢出事件: 美链攻击事件,uint溢出之祸.md

我们采用了超大容量Decimal(38,18)来一定程度上规避这个问题


8.对账问题

  • 逐笔核对

  • 总分核对

注意,不能只对成功的交易(因为有未知的情况),比如银行只对自己表中成功的交易,可能有笔银行失败,对方未知,需要针对成功、失败、未知***全量对账***


9.资源释放问题

比如 lock.release 这样的语句要在什么地方释放?

注意,如果是如下类型的语句,则可能发生因前面语句有异常,资源不能释放的问题


lock.lock();

bussinessLogic();  //一旦这句发生异常,异常抛出,就无法执行锁的释放了

lock.release();

如下链接是个实际的例子,提现业务,各个应用服务器通过抢zk锁来获得执行异常后补提现的功能,因为异常导致锁不能释放,进而影响提现

实际例子,异常导致问题,修复了lock释放语句


10.认证授权问题

虽然都是内网应用,但是也存在被攻击的可能,人民币的9月24日版本主要就是针对ak/sk做

有了ak/sk可以认证发起交易的应用服务器是正确的

不过还是不能验证是否是对应的人发起(对应人发起的,要靠支付密码;平常小额,要用免密规则)

发布了66 篇原创文章 · 获赞 13 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/wxid2798226/article/details/104348951