mysql事务操作,出现异常conn和数据不同步一致问题
发布于 7 年前 作者 zhaojunlike 2683 次浏览 来自 问答

我先贴代码:


 /**
 * 核心处理卡的逻辑
 * @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,发现如下问题 image.png

mysqlbinlog image.png

12 回复

服务器配置,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吗?

换数据库确实比较现实

@zuohuadong

  • 有钱干啥都行, 比如弄个超算拿记事本魔改当数据库 (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,也不否定别人就是一个时代的经典。所以我们依照解决问题的方向出发,没必要批判什么好坏的

回到顶部

AltStyle によって変換されたページ (->オリジナル) /