我先贴代码:
/**
* 核心处理卡的逻辑
* @param {object} reqCard
* @returns {Promise<*>}
* @private
*/
async handleCallbackCard(reqCard) {
const {app, ctx} = this;
const logger = this.app.getLogger('myLogger');
logger.info(`卡回调:${reqCard.order_no},状态:${reqCard.status}`);
//1.获取卡,然后获取卡的用户最后进行加锁,也需要给这个订单加锁
await this.getLock(reqCard.order_no, 10000);
const card = await app.mysql.get('ey_cards', {
order_no: reqCard.order_no,
status: 0,
money_status: 0
});
if (null === card) {
logger.info(`未处理的卡订单并没有找到:${reqCard.order_no},状态:${reqCard.status}`);
//释放订单锁
await this.freeLock(card.order_no);
return false;
}
//2.给用户加锁
await this.getLock(card.uid, 10000);//获取锁,10s超时
//3.开启事务
const conn = await app.mysql.beginTransaction();//开启事务
const groupId = parseInt(card.group_id);
reqCard.status = parseInt(reqCard.status);//状态
try {
//conn
let user = await conn.get('ey_user', {id: card.uid});
if (!user) {
throw new Error('未获取到相关用户');
}
const beforeMoney = user.money;
//如果卡成功了的话
if (reqCard.status === 1) {
//处理成功了
card.real_price = reqCard.real_price;
card.api_callback_result = reqCard.api_callback_result;
card.api_msg = reqCard.api_msg;
//card.money = card.real_price * card.money_rate / 100;
card.money = (card.real_price * card.money_rate / 100).toFixed(2);
card.money = parseFloat(card.money);
//给用户发钱
const afterMoney = parseFloat(user.money) + card.money;//计算理当增加金额后的钱
//同步更新用户的金额数据,当时为了防止脏数据使用的这种更新方式
// const result = await conn.query('UPDATE ey_user SET money=(money+?),score=(score+?),used_money=(used_money+?) where id=?', [
// card.money,
// card.money,
// card.money,
// card.uid
// ]);
//换一种更新算法
user.money = afterMoney;
user.score += card.money;
user.used_money += card.money;
const result = await conn.update('ey_user', user);//更新用户数据
const updateSuccess = result.affectedRows === 1;//是否更新成功
if (!updateSuccess) {
throw new Error(`用户:${card.uid}增加金额失败:${result.message}`);
}
//异步写入财务日志
await app.mysql.insert('ey_user_financial_logs', {
uid: card.uid,
create_time: new Date().getTime() / 1000,
money_before: beforeMoney,
money_after: afterMoney,
bound_data: card.money,
bound_type: 'CARD_MONEY',
type: groupId === 0 ? 1 : 2,//1:单卡,2:多卡,3:提现
do_note: '销卡入账',
note: '销卡入账 ' + card.money + ' 元',
status: 0
});
card.money_status = 1;
card.status = 1;
}
//如果status==2 就是失败了
if (reqCard.status === 2) {
card.status = 2;
card.real_price = reqCard.real_price;
card.api_callback_result = reqCard.api_callback_result;
card.api_msg = reqCard.api_msg;
}
//事务进行更新卡,如果是发了钱的状态
await conn.update('ey_cards', card);//更新卡的事务状态
const cres = await conn.commit();//提交事务
//获取更新后的金额
user = await app.mysql.get('ey_user', {id: card.uid});
//最后获取一遍金额
logger.info(`用户:${card.uid},金额增加:${card.money} ,之前:${beforeMoney} ,现在:${user.money},卡ID:${card.id},卡状态: ${card.status}, 钱状态:${card.money_status},处理卡组:${groupId}`);
//最终销卡组无论成功还是失败他的状态改变
this.handleCardGroup(card).then(res => {
console.log("处理卡组", res);
}).catch((err) => {
logger.error("处理卡组失败:" + err.message);
});
} catch (e) {
await conn.rollback();//回滚
logger.error(`${card.uid}回调错误,Err:` + e.message);
}
//最后进行提交
if (card.status === 1 && card.money_status === 1) {
//发送金豆
this.sendGold(card.uid, card.money).then((res) => {
console.log("发送金豆");
});
//更新用户的登陆
this.upUserLevel(card.uid).then(() => {
console.log("检查用户等级");
});
}
//6.free 释放用户锁,释放订单锁
await this.freeLock(card.uid);
await this.freeLock(card.order_no);
return card;
}
以上的逻辑是一个支付回调的逻辑,本身之前用的FOR UPDATE 用户行级别锁,但是发现不是很好,后续调整到redis setnx lock。
问题如下
偶尔会发现如图所示卡壳现象,我提取了mysqlbinlog,发现如下问题
mysqlbinlog
服务器配置,linux ,单机1cpu。 本地已经对dev环境进行压测过,测试1000并发压测,jemter。 但是线上出现这种情况让我感到很诡异,有朋友能帮我梳理一下吗?
我很多方式都尝试过:
- 1.使用for update 进行行级锁
- 2.开始未对order_no加锁导致订单阻塞到用户lock之前有重复的情况,所以加了userid和order_no锁。
- 3.服务器偶尔出现这种情况,我试着升级cpu=2,这种情况明显减少,如果需更多的详细日志和数据,我可以提供的。
感谢所有的社区朋友
说个极端的方案: 更换数据库,一了百了。免费的选pgsql, 商业的选oracle。 细节是魔鬼,mysql这货无处不是坑。 比如 可重复读事务级别的写入就有大坑
@waitingsong 额, 我觉得你有些极端了
@waitingsong 我们还是以解决问题为目的
@waitingsong 虽然话不好听,但是是事实。 解决问题? 请问你有 阿里那几百万年薪的 DBA吗?
换数据库确实比较现实
- 有钱干啥都行, 比如弄个超算拿记事本魔改当数据库 (mysql 我认为算个超级记事本) 。
- 没足够资金, 数据不值钱,丢了数据不需要赔钱跑路的尽管上 mysql 。
- 没足够资金,数据值钱,不想随时踩坑,就不要碰这烂货。
你可以试一下能不能本地压测复现。 如果for update还不行吗?那本地开serializable级别压一下来观察情况。
下面的代码如果查询的时候没有for update,照道理说你这个虽然避免了脏读但是没有避免不可重复读。反而问题更大,默认的隔离级别应该是能避免脏读的。还有建议update,insert,delete都判断结果,如果没有执行成功就抛出异常回滚。
//同步更新用户的金额数据,当时为了防止脏数据使用的这种更新方式
// const result = await conn.query('UPDATE ey_user SET money=(money+?),score=(score+?),used_money=(used_money+?) where id=?', [
// card.money,
// card.money,
// card.money,
// card.uid
// ]);
//换一种更新算法
user.money = afterMoney;
user.score += card.money;
user.used_money += card.money;
const result = await conn.update('ey_user', user);//更新用户数据
@waitingsong mysql是有坑,但数据安全方面还是没什么问题的。坑主要集中在操作方面,比如很多人不知道not in 里面不能有null
@zy445566 mysql从出生就以快取胜,代价是不稳定。之前 myisam 引擎崩表是常事。innodb 后有好转。 不过,稳定的代价是速度。 所以如果 mysql 还以速度为荣,那稳定性肯定依然没谱。 如果哪天说稳定性提高许多,那速度必然下降更多。 先天不足,后天怎么补都没用。
@zy445566 你好,谢谢你的回复,我尝试过使用for update,会还有出现更严重的情况,至于你说要看返回结果,确实,你可以看到我使用commit后,再去查询user中的数据后打印的日志
@waitingsong 至于采用什么数据库,我觉得应该不是我采用了数据库的什么问题,因为我们很多业务体系是mysql,也不否定别人就是一个时代的经典。所以我们依照解决问题的方向出发,没必要批判什么好坏的