关于数据库事务并发的理解和处理

2,876 阅读6分钟

关于数据库事务并发的理解和处理

  • 并发的概念:在操作系统中,并发是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。

    在关系数据库中,允许多个用户同时访问和更改共享数据的进程。

  • 理解事务的概念

    1. 概念:

      MySQL 事务主要用于处理操作量大,复杂度高的数据, 比如说,在人员管理系统中,你删除一个人员,你即需要删除人员的基本资料,也要删除和该人员相关的信息,如信箱,文章等等,这样,这些数据库操作语句就构成一个事务!

    2. 事务的四个重要特征

      • 原子性

        一个事务(transaction)中的所有操作,要么全部完成,要么全部失败,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。也就是说事务执行成功全部会应用到数据库,失败的话对数据库没用任何影响。

      • 一致性

        在事务开始之前和事务结束以后,数据库的完整性没有被破坏。比如说A账户有1000块,B账户有100块,A转账500给B,那么A变成500,B变成600,也就是说两者之间无论如何转账都好,在事务结束之后加起来都是1100,这就是一致性。

      • 隔离性 : (这一点很重要,直接相关事务并发)

        数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。

      • 持久性

        事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

    3. 重点讲解事务的隔离性

      隔离性是如何防止多个事务并发执行时由于交叉执行而导致数据的不一致的安全问题的呢?答案是通过设置隔离级别来解决的。

      • mysql四种隔离级别
      隔离级别 脏读 不可重复读 幻读
      读未提交(read-uncommitted)
      不可重复读(read-committed) ×
      可重复读(repeatable-read) × ×
      串行化(serializable) × × ×
    • 其中需要注意的是:

      • 可重复读的隔离级别下使用了MVCC机制,select操作不会更新版本号,是快照读(历史版本);insert、update和delete会更新版本号,是当前读(当前版本)。所以这也会带来“脏写”问题

      • mysql中事务隔离级别为serializable时会锁表,因此不会出现幻读的情况,这种隔离级别并发性极低,开发中很少会用到。

    • 在mysql中查看当前隔离事务级别语句:select @@tx_isolation;

    • 在mysql中设置事务的隔离级别语句: set tx_isolation='隔离级别名称';

      那么既然可以通过设置隔离级别为串行来解决并发时带来的数据不一致问题,那为什么不直接把数据库隔离级别改为serializable就好?的确,这样的确可以解决数据在多事务并发处理下数据不一致问题,但是往往带来的是更大的性能开销,这些性能的开销往往是大于结合事务隔离级别和其它并发机制来处理事务的并发的开销的。正确性和效率不可兼得,很多小公司在涉及到钱的业务代码上都会启用事务把,但是其实是只有小数量的公司才会遇上高并发的情况,多数都只是处理好并发就好了,那么问题来了,如果只是处理很小量的并发,而且是时有时没有的时候,那就应该把级别改为串行?并不是的,改了是整个数据库受到影响的,包括所有的表,所以其他的查询和更改都会上锁了,效率是极其低下的。

  • 数据库并发控制解决方案

    锁的分类

    • 从数据库系统角度分为三种:排他锁(x)、共享锁(s)、更新锁(u)。

    • 从程序员角度分为两种:一种是悲观锁,一种乐观锁。

    这里只讨论悲观锁乐观锁

    • 悲观锁:

      需要数据库本身的支持,通过SQL语句“select for update”锁住select出的那批数据,总是假设最坏的情况,每次取数据时都认为其他线程会修改,当其他线程想要访问数据时,都需要阻塞挂起。悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。但是在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁的机会;另外,在只读型事务处理中由于不会产生冲突,也没必要使用锁,这样做只能增加系统负载;还有会降低了并行性,一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完才可以处理那行数据。

    • 乐观锁

      乐观锁,虽然名字中带“锁”,但是乐观锁并不锁住任何东西,而是在提交事务时检查这条记录是否被其他事务进行了修改:如果没有,则提交;否则,进行回滚。相对于悲观锁,在对数据库进行处理的时候,乐观锁并不会使用数据库提供的锁机制。如果并发的可能性并不大,那么乐观锁定策略带来的性能消耗是非常小的。乐观锁采用的实现方式一般是记录数据版本。

      数据版本是为数据增加的一个版本标识。当读取数据时,将版本标识的值一同读出,数据每更新一次同时对版本标识进行更新。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的版本标识进行比对,如果数据库表当前版本号与第一次取出来的版本标识值相等,则予以更新,否则认为是过期数据。一般地,实现数据版本有两种方式,一种是使用版本号,另一种是使用时间戳

  • 并发测试工具ab

    这是apache自带的压测工具,也可以拿来做并发测试

    使用方法 进入ab工具默认安装目录(注意这是windows环境下)

    V~J7722)6O9BS}I%)K~}1VG.png

执行命令 ./ab.exe -n 1000 -c 1000 http://localhost/tp5/public/

0_}27`@@E9Y1WMH8}62ERE8.png

下面是测试代码,用的是tp5

<?php
namespace app\index\controller;

use think\Controller;
use think\Db;

class Index extends Controller
{
    public function index()
    {
        Db::startTrans();
        try{
            $db = Db::table('test');
            $res = $db->where('id',1)->value('stock');//这样库存有可能会变负数
            //$res = $db->where('id',1)->lock(true)->value('stock');//这句解决并发带来的问题,比如库存负数
            if ($res >= 1){
                Db::table('test')->where('id', 1)->setDec('stock');
            }

            // 提交事务
            Db::commit();

        } catch (\Exception $e) {
            // 回滚事务
            Db::rollback();
            echo $e->getMessage();
        }
    }
}

部分参考自: blog.csdn.net/justloveyou…