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(其实就是类似工行的渠道事件编号),重复交易具有了幂等性
通证的实战应用
-
- 新增用户过程中,如果调用方uuid上送重复了,返回的报文是一模一样的
-
- 转账过程中,如果上送的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锁来获得执行异常后补提现的功能,因为异常导致锁不能释放,进而影响提现
10.认证授权问题
虽然都是内网应用,但是也存在被攻击的可能,人民币的9月24日版本主要就是针对ak/sk做
有了ak/sk可以认证发起交易的应用服务器是正确的
不过还是不能验证是否是对应的人发起(对应人发起的,要靠支付密码;平常小额,要用免密规则)