数据(data)
数据是人类世界非常重要的存在。世界上的一切事物是一部分数据的集合,那么理论上我们就可以创造出一切事物,前提是获得相应的数据并建立起一个模型(对事物的描述)。以印刷术为例,每一个字的形状都是一个数据,人们以对这些数据的认知对每一个字都建立了一个模型,然后可以通过这些模型就可以印出相应的字符。进入电子信息时代,数据得到了更广泛,更深度的使用。例如在医学,航天,机器人(人工智能)。
越复杂的事物我们越难描述(建立模型),因为越复杂的事物包含的数据元素就越庞大,此时我们需要将收集到的数据存储起来进行分类管理,需要时读取,这时就引入了数据库。
数据库DB(database)
数据库从很早就开始存在,例如古时候用于记账的账簿,记录个人信息的档案库,藏在某处的武林秘籍等等。到了电子信息时代,已经可以实现将数据存储在计算机的磁盘上了。这里定义计算机磁盘空间中的长久存储的大量数据的集合就是一个数据库(database),使用磁盘作为数据库有数据永久性保存,查询管理方便等优点。
数据库系统DBS(database system)
数据库系统一般由数据库、数据库管理系统(及其开发工具)、应用系统、数据库管理员构成。该系统维护数据库的正常生命周期。
数据库管理系统DBMS(database manager system)
数据库管理系统是位于用户与操作系统之间的一层数据管理软件。这个系统方便用户操作,维护数据库。常用的DBMS有:MySQL、Oracle、DB2、SQLServer等。本文要讨论的是MySQL。
当我们使用计算机磁盘作为数据库存储数据,使用相应的DBMS进行访问操作数据之后,单用户访问操作数据库是没有问题的,但是单用户访问的效率实在太低,此时我们想实现多用户并发访问。但是此时就会出现问题:
1.用户1读取了某数据,准备使用。
2.用户2在之后修改了该数据。
3.用户1在使用之前忘记了自己读的数据,再次读取。
那么我们希望能够解决这些问题来实现多用户并发访问数据库。此时我们对DBMS的设计引入以下规则:
事务(Transaction):完成某个需求需要做的一连串的操作称为一个事务,事务是DBMS的执行单元。
事务具有以下特性:
原子性:事务是不可分割的,完成事务的步骤要么全部不做,要么全部做完。(rollback&commit)
一致性:事务执行过程中操作的数据必须是一致的。(不同需求下对于一致性的要求不同)
隔离性:多个事务并发执行的时候,多个事务之间操作的数据互不影响。(隔离性实现一致性)
持久性:事务一旦提交,数据就永久存储在数据库中,即使是产生故障也不能影响。(recovery)
由于不同需求下对于并发访问的产生的结果要求不同,提出了以下四大隔离级别:
read uncommitted:读未提交
此隔离级别下,事务会读到未提交事务的数据。
read committed:读已提交
此隔离级别下,事务会读到已经提交事务的数据。
repeatable read:可重复读
此隔离级别下,其他事务对于数据的修改不会影响到本事务读数据。
serializable:可串行化
此隔离级别下,所有事务串行化(排队)执行。
并发访问问题汇总:
1.修改丢失:
1)事务1读取了数据对象A=3,保存下来赋值为B1。
2)事务2读取了数据对象A=3,保存下来赋值为B2。
3)事务1修改了数据对象A=B1+1=4。
4)事务2修改了数据对象A=B2+1=4。
场景:多窗口卖票。
窗口1,2同时知道已经卖了3张票,然后在同一时间窗口1卖出了一张票,窗口2也卖出了一张票,本应该卖出了5张,结果只卖了4张。
2.脏读:事务2修改数据未提交,事务1读取了数据。
1)事务2修改了数据A=3,操作未完成...
2)事务1读取到了数据对象A=3。
3)事务2修改了数据A=2,操作完成。
场景:银行转账
X向Y转账,银行中X账户减钱,Y还没加钱,X此时查看了两个账户。
3.不可重复读:事务两次读取的数据不同。
①事务1读取数据A之后,事务2修改了A并提交,事务1再读。
1)事务1读取数据A。
2)事务2修改A并提交。
3)事务1读A。
场景:查看银行余额,查询完成之前,之后再有人转账余额都不应该变化。
②幻读:事务1读取了数据之后,事务2插入或者删除了某些数据并提交,事务1再读。
1)事务1读取了一个范围内的数据{A,B,C......}。
2)事务2插入了数据D到此范围并提交。
3)事务1读此范围发现新增了D,不一致。
场景:网站实时统计某一时间在线用户人数,在统计完成之前后面再有新增的用户记录都不应该没查询到。
不同的隔离级别与并发问题对应关系:
1.修改丢失:
这种问题属于逻辑上的问题,数据库是处理不了的。以卖票为例,当有窗口在售票的时候,其他窗口就应该等该窗口售完票之后修改了售票数额再进行读取余额进行修改。所以应该由程序进行控制。
2.脏读:read uncommitted隔离下会产生脏读,read committed以上的级别都处理了脏读。
3.不可重复读:read committed以下隔离级别有不可重复读,repeatable以上级别处理了该问题(有一些DBMS没有处理幻读的问题)。
下面介绍MySQL处理这些问题并实现事务四大特性的原理:
首先我们先引进锁的概念:
目前在计算机中对于共享数据的并发访问大多只能通过“锁”来进行处理。在数据库中也引进了两种锁的概念:悲观锁与乐观锁。
悲观锁:
顾名思义:悲观锁及在事务对数据进行访问的时候,悲观地认为之后会有其他事务会访问该数据并进行修改而在数据上加上一把锁,而阻塞其他事务。其中悲观锁分为共享锁和排他锁。
共享锁(Share Lock):简称S锁
共享锁又称为读锁。若事务T对数据对象A加上S锁,则只事务T可以读A但是不能修改A,其他任何事务只能再对A加S锁,而不能加X锁 ,直到T释放A上的锁。
排他锁(eXclusive Lock):简称X锁
排他锁又称为写锁。若事务T对数据对象A加上X锁,则只允许T读取和修改A,其他任何事务都不能对A加任何类型的锁 ,直到T释放A上的锁。
注意这里的两个命题:
1.事务对A加上了X锁(条件)能读写A(结果)
2.事务对A加上了S锁(条件)能读A(结果)
是充分条件,不是必要条件。
即:
1.事务对A能读写A(条件)就加上了X锁(结果)
2.事务对A能读A(条件)就加上了S锁(结果)
这是不成立的。
上述粗体字段就是对前半命题的补充,拿其他任何事务都不能对A加任何类型的锁举例:
这里是定义了,加了X锁之后,不能加其他的锁,而不是不允许其他事务读取A了。这里只是定义了加锁操作之间的互斥关系。
乐观锁:
顾名思义:这是一个乐观的“锁”,其实称其为乐观控制(验证)法更为准确。即事务在访问数据的时候,乐观地认为不会有其他事务对数据进行修改,不阻塞任何事务,在之后再对该数据进行访问的时候会验证其是否和之前访问的数据是一致的。
下面讨论使用这两种锁如何实现事务四大特性
1.理论上,只使用悲观锁是可以实现的:
1)在读之前要加共享锁。
2)在写之前要加排他锁。
在这种情况下,确实实现了事务的四大特性,但是因为锁的特性会导致,写操作会阻塞大量的读操作(读操作占了数据库访问中很大的比重),严重影响了并发访问程度,而且会很容易导致死锁的发生(事务之间互相等待释放锁,解决死锁有多种方式,这里不具体讨论)。
2.排他锁加乐观锁,即多版本并发控制。
1)读不加锁。
2)写之前加排他锁。
3)维护多个版本的数据供不同的事务读取。(不同隔离级别情况不同,下面有介绍)。
多版本并发控制(MutiVersion Concurrency Control),简称MVCC。它使得大部分支持行锁的事务引擎,不再单纯的使用行锁来进行数据库的并发控制,取而代之的是把数据库的行锁与行的多个版本结合起来,只需要很小的开销,就可以实现非锁定读,从而大大提高数据库系统的并发性能。
这里引入新概念:行锁。行锁指悲观锁的粒度(即加锁的对象单位),其中有行锁,表锁,页锁,数据库锁等等。
MySQL(InnoDB)实现事务四大特性的原理:
概念简述:
1.undo log(撤销)
undo log记录数据的多版本快照,事务在修改数据之前会将旧版本的数据复制到undo log,读操作不写undo log。
存在形式:会分配到一定的内存(cache),也会写入磁盘(ibdata1文件)。
写入方式:按修改操作顺序写入,不分事务。
1)用于事务的回滚而实现原子性。
2)用于事务读操作匹配版本实现一致性,隔离性。
2.redo log (重做)
事务在不会直接覆盖修改数据库磁盘中的数据(以下称为data file),会先复制一份数据到redo cache中进行修改,然后写到redo file之后,再修改data file。
存在形式:会分配到一定的内存(cache),也会写入磁盘(ib_logfile0、ib_logfile1文件)。
写入方式:按修改操作顺序写入,不分事务。
redo log主要用于维护数据的持久性。
3.DATA_TRX_ID、DATA_ROLL_PTR、DELETE BIT
在MySQL表中有三个隐藏字段:
DATA_TRX_ID:事务修改当前行数据的时候会将这个字段记录为自己的事务ID
DATA_ROLL_PTR:回滚指针,事务修改当前行数据的时候将这个字段记录指向undolog中上一次修改该行的记录。
DELETE BIT:事务修改当前行数据的时候记录标记为“被删除”。
事务修改数据的流程分析:
事务将数据对象A从数据库(磁盘)中复制到redo cache buffer中进行修改,在修改之前再复制一份填写删除标识字段,将其回滚指针指向undo中上一条记录,放到undo cache buffer中,然后将redo cache中的数据副本中的trxid设为自己的事务id,undo&redo cache每隔一段时间(1秒)刷到undo&redo file,redo file写入之后将写入redo file的数据覆盖写入data file数据。
事务修改数据的流程图解:
①原子性:事务没完成,可以通过回滚指针一条一条记录往前反向复原,直到事务修改之前的记录版本。
②一致性&隔离性(因为一致性依赖于隔离性,这里一起讨论):
read uncommitted:
读不加锁,写加X锁。
read committed&repeatable read:
事务读操作每次都是结合read view在data+undo file中匹配适合的数据。(MVCC)
新概念:
read view:
每个事务在创建的时候InnoDB(MySQL数据库引擎)都会记录当前系统中的活动事务id(不包含committed以及本事务的id)到一个副本-->read view。这个副本的功能是记录了对与本事务来说应该隔离的事务(不可见)。
注:事务id是按照创建顺序递增分配的。 .
1.将data+undo file中的符合条件的数据全部找出(多版本)。将排在第一的数据的transaction_id赋值给trx_id,read view中的最小值id为trx_min,最大值为trx_max。
2.如果trx_id<trx_min,说明创建该版本数据的事务是在read view创建之前(也就是在当前事务创建之前)就已经committed了。跳到步骤6。
3.如果trx_id>trx_max,说明创建该版本数据的事务是在read view创建之后(也就是当前事务创建之后)创建的,此版本数据不可见。跳到步骤5
4.如果trx_min <= trx_id<= trx_max,说明创建该版本数据的事务在当前事务创建之前创建的,(但是不确定是否是在当前事务创建之前提交的)。拿trx_id遍历匹配read view:
1)如果有相等的事务id,说明创建该版本的事务在当前事务创建的时候还是活跃的。此版本数据也不可见,跳到步骤5;
2)如果没有匹配到相等的事务id,说明创建该版本的事务在当前事务创建的之前就提交了,只不过它比一些在它之前创建的事务提交得早。调到步骤6。
5.用回滚指针找到上一版本数据,将其trasaction_id赋值给trx_id,跳到步骤2。
6.该版本数据是可见的,返回该数据到读取操作。
在rc隔离级别下:在事务创建之初以及每次read的时候都会创建一份read view。
在rr隔离级别下:只在事务创建的时候创建一份相对与该事务全局的read view。
serializable:
读加S锁,写加X锁。
③持久性:因为所有数据修改都是在提交完成之前已经通过redo log写到了服务器数据库磁盘中去了。即使之后数据库故障都可以通过redo中的记录进行恢复。(根据undo进行的rollback操作也会被写入redo,也就是说恢复数据库的时候,rollback操作也会重新执行)。