关于mysql事务操作在循环里的疑惑

需求的描述是有一个数组,循环处理数组数据,写入并更新数据库,写入+更新为一组数据处理,想要达到的效果是循环中如果某一组数据写入/更新失败,回滚当前组的数据库操作;
以此需求为前提写了下面这段事务处理代码,但总觉得这样写不合适

$this->db->trans_begin();
foreach ($record_data as $key => $value) {
    /**
    * 更新数据库操作
    **/
    若干代码.......
    /**
    * 插入数据库操作
    **/
    若干代码.......
    
    if ($this->db->trans_status() === FALSE) {
        $this->db->trans_rollback();
    } else {
        $this->db->trans_commit();
        // 这里需要记录当前组操作成功的
        $ok_ids[] = $value['id'];
        $user_monetary[$value['user_id']] = $diff;
    }
}

上面这段代码是一段典型的CI框架手动开启数据库事务的例子,有几个纠结/疑惑的点:

  1. 我把开启事务放在循环之前,把执行事务处理的部分放在代码里,可以把更新+插入当做一组数据处理,如果我循环执行到中间,有一组数据执行失败,触发了回滚,会不会把我之前的已经处理过的数据也回滚掉?
  2. 如果我把开启事务放在循环体里,因为每次进入循环体都要开启事务,然后进行事务处理,会不会影响执行效率,每次循环次数在100~500之间不等;

由于上面的疑惑写了一段测试代码:

    $ins_data = [
        'monetary'     => 202,
        'consume_time' => 20170798,
    ];
    
    $this->db->trans_begin();
    for ($i=0; $i < 100; $i++) { 
        if($i == 50) {
            $ins_data['user_id'] = 366750;
        }else {
            $ins_data['user_id'] = 3667;
        }
        $this->xxx_model->insert($ins_data); // 插入到数据库
        if($i == 50) {
            $this->db->trans_rollback();
        }else {
            $this->db->trans_commit();
        }
    }

运行这段代码的结果是:插入操作无一例外正常执行,366750这条记录还是插入了数据库里面,并无产生回滚事件

查阅资料:

在事务中,每个正确的原子操作都会被顺序执行,直到遇到错误的原子操作,此时事务会将之前的操作进行回滚。回滚的意思是如果之前是插入操作,那么会执行删 除插入的记录,如果之前是update操作,也会执行update操作将之前的记录还原。
因此,正确的原子操作是真正被执行过的。是物理执行。

看到说直到遇到错误的原子操作才能进行回滚,为了验证这个,我放弃了在循环中执行事务,把代码改成了在循环体外执行事务的处理:

    $ins_data = [
        'monetary'     => 202,
        'consume_time' => 20170798,
    ];
    
    $this->db->trans_begin();
    for ($i=0; $i < 100; $i++) { 
        $this->xxx_model->insert($ins_data); // 插入到数据库
    }

    $this->db->trans_rollback();
    // $this->db->trans_commit();

上面这段代码中,当最后操作rollback的时候,数据库并不会插入数据,当是commit的时候,数据库才会插入数据。那么这一现象就打翻了前面说的只有遇到错误的原子操作才能进行回滚的陈述,这时候我就更懵了。

那么什么是事务?事务的触发条件是什么?符合我这种场景的数据操作该如何进行事务处理呢?还请路过的大神能够指点一下、抱拳~

阅读 13.8k
5 个回答

我是这样认为的:
你的那段测试代码,在循环体内多次commit了。由于你的事务是在循环开始前就开启了,当循环体内第一次commit的时候,这个事务就结束了,所以后面执行的回滚是无效的。也就是说事务的开始和结束是配对的,区间的所有操作都是放在一个数据库连接中。可以这样改下程序,把提交放到循环结束后在提交,回滚不变:

 $ins_data = [
        'monetary'     => 202,
        'consume_time' => 20170798,
    ];
    
    $this->db->trans_begin();
    for ($i=0; $i < 100; $i++) { 
        if($i == 50) {
            $ins_data['user_id'] = 366750;
        }else {
            $ins_data['user_id'] = 3667;
        }
        $this->xxx_model->insert($ins_data); // 插入到数据库
        if($i == 50) {
            $this->db->trans_rollback();
        }
    }
    //循环结束在提交
    $this->db->trans_commit();

INSERT INTO table (field1,field2,field3) VALUES ('a',"b","c"), ('a',"b","c"),('a',"b","c"); 为什么不用 sql进行批量插入 一个事务开启控制

比如你有三个数据库操作,而且需要按照以下顺序执行

  1. 减去库存

  2. 更新钱包

  3. 写日志

你可以这样做

  1. 事务开始

  2. 如果 减去库存成功 继续往下 否则事务回滚

  3. 如果 更新钱包成功 继续往下,否则事务回滚

  4. 如果写日志成功 继续往下 否则事务回滚

  5. 提交事务

以上执行,如果第2步回滚了,那么数据库无任何变化;如果第3步回滚了,那么第2步的操作被撤销;如果第4步回滚了,那么第2第3步都被撤销;如果第5步执行了,那么这三步全部执行成功。

遇到错误是指一个事务里有错误,会进行回滚。你这个问题可以通过try cache捕获错误,出现错误进行回滚即可

try{
    $ins_data = [
        'monetary'     => 202,
        'consume_time' => 20170798,
    ];
    
    $this->db->trans_begin();
    for ($i=0; $i < 100; $i++) { 
        $this->xxx_model->insert($ins_data); // 插入到数据库
    }
    $this->db->trans_commit();
}  catch (\Exception $e) {
    $this->db->trans_rollback();
}

了解一下事务的特性:ACID

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
宣传栏