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

我先贴代码:


    /**
     * 核心处理卡的逻辑
     * @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,也不否定别人就是一个时代的经典。所以我们依照解决问题的方向出发,没必要批判什么好坏的

回到顶部