事务的核心是锁和并发,采用同步控制的方式保证并发的情况下性能尽可能高,且容易理解。
一、事务有四个基本特性:
1、原子性(atomicity):数据库操作的最小基本单位,事务内的操作要么都成功,要么都失败,中途不能被打断。
2、一致性(consistency):事务执行后数据从一种状态变成另外一种状态,不会存在事务的一部分数据写入了数据库,一部分没有写入。
3、隔离性(isolation):事务之间独立执行,不相互干扰。
4、持久性(durability):事务一旦提交,对数据库的修改是永久性的,不会改变。
二、事务隔离
当多个线程同时执行读、算、写操作时,如果不加访问控制,系统势必会产生冲突,举例说明:
并发的时候,事务不断执行,这段时间简称T。T上有很多时间点,T0,T1,T2,T3,T4,T5..........T0为时间起点,数据库上有一个表user,字段name,point。
事务A:select name,point from user where id =1;
事务B:update user set point=point+1 where id =1;
脏读:首先事务B开始执行,在T1的时候事务A又开始执行,这个时候B事务并没有提交,事务A查询的值是事务B未提交的值,如果此时B事务回滚,会造成事务A查询到错误数据。这就是脏读。
不可重读:于是事务B修改point值的时候加锁,事务A只能在B事务提交后才能查询到结果,这样防止了脏读。但是如果事务A有两次相同的查询,分别在时间T0和T2,而再T1的时候事务B提交就会造成事务A两次相同查询的结果不一样。这就是所谓的不可重读。
幻读:为了防止出现重读,则需要在事务A查询的时候加读锁,在事务A提交之前,查询的数据不会被修改。出克脏读和不可重读,还会出现幻读的情况。出现幻读的情况也是一个事务内两次相同的查询结果不一样,和不可重读不一样的是,不可重读第二次查询的结果被修改或删除,而幻读第二次会查出新增的记录,可能是其他事务插入了满足条件的记录。而解决幻读的问题会复杂很多,需要对第一次查询的查询条件的一段范围加锁,具体就不细说了。
为了防止以上的情况发生,事务隔离产生了:
Read Uncommitted:这个隔离级别最低,会出现脏读、不可重读和幻读。这种隔离级别很危险,一般不用。
Read Committed:这个隔离级别稍微高一些,不会出现脏读,但会出现不可重读和幻读,oracle默认使用的事务隔离级别。
Repeatable Read:不会出现脏读和不可重读,会出现幻读。mysql默认的事务隔离级别。
Serializable:不会出现脏读、不可重读和幻读。因为串行化隔离需要很大的系统消耗,并发不好,一般不会使用。
而mysql用了一个并发版本控制机制mvcc,为了提高数据库并发量,允许事务并行读取写入数据,这个时候如果我们需要严格控制并发,就要加锁。
三、处理事务常用方法
处理事务常用方法有排队法、排他锁、读锁、读写锁、mvcc.
1、排队法
最简单也是最重要的事务处理方法,用单一线程处理数据,避免了并发带来的同步问题。如redis,数据全部在内存中,则单线程处理所有Put、Get操作效率最高。
2、排他锁
可以利用排他锁的方式来快速隔离并发读写事务。一个事务获得锁后,其他事务的读写操作block住。
3、读写锁
如果是一系列读事务,不会对数据进行修改,那这些事务是可以并行的,因此一种针对读读场景的优化自然而然产生——读写锁。读写锁的核心是在多次读的操作中,同时允许多个读者来访问共享资源,提高并发性。
4、mvcc
本质是Copy On Write,也就是每次写都是以重新开始一个新的版本的方式写入数据,因此,数据库中也就包含了之前的所有版本。在数据读的过程中,先申请一个版本号,如果该版本号小于正在写入的版本号,则数据一定可以查询到,无需等到新版本完全写完即可返回查询结果。这种方式可以在读读不阻塞的前提下,实现读写/写读不阻塞,尽可能保证所有的读操作并行,而写操作串行。
写写是没法优化的,必须串行。
四、调优
事务的调优的思路是在不影响业务应用的前提下:
第一,尽可能减少锁的覆盖范围,例如Myisam表锁到Innodb的行锁就是一个减少锁覆盖范围的过程;对于原位锁(排他锁、读写锁等)可变为MVCC多版本(本质仍然是减少锁的范围)。
第二,增加锁上可并行的线程数,例如读锁和写锁的分离,允许并行读取数据。
第三,选择正确锁类型,其中悲观锁适合并发争抢比较严重的场景;乐观锁适合并发争抢不太严重的场景。