这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战
一、解决并发事务的两种基本方式
1.1、事务并发执行时可能带来的各种问题,并发事务访问相同记录的情况大致可以划分为以下3种情况
读-读情况:即并发事务相继读取相同的记录
读取操作本身不会对记录有影响,并不会引起什么问题,所以允许这种情况的发生
写-写情况:即并发事务相继对相同的记录做出改动
在这种情况下会发生脏写的问题,任何一种隔离级别都不允许这种问题的发生。所以在多个未提交事务相继对一条记录做改动时,需要让它们排队执行,这过程通过锁来实现,锁是一个内存中的结构,在事务执行前没有锁,也就是没有锁结构和记录进行关联。
当一个事务对一条记录做改动时,首先要看内存中有没有记录与这条记录关联的锁结构,当没有时就会在内存中生成一个锁结构与之关联,比如事务T1要对记录做改动,就需要生成一个锁结构与之关联\
如果在事务T1提交之前,别一个事务T2也想对该记录做改动,先查看有没有锁结构与这条记录关联,如果有一个锁结构存在了,然后也生成一个锁结构与这条记录关联,is_waiting=true(表示当前事务获取锁失败需要等待)\
如果此时事务T1提交,就会把该事务生成的锁结构释放,发现事务T2在等待获取锁,把事务T2对应的锁结构is_waiting设置为true,然后把该事务对应的线程唤醒,此时事务T2获取到锁\
读-写或写读情况:也就是一个事务进行读取操作,另一个进行改动操作
这种情况下可能发生脏读、不可重复读、幻读问题。
1.2、怎么解决脏读、不可重复读、幻读问题
方案一:读操作利用多版本并发控制(MVCC),写操作进行加锁
MVCC就是通过生成一个ReadView,然后通过ReadView找到符合条件的记录版本(历史版本是由undo日志构建),查询语句只能读到在生成ReadView之前已提交事务所做的更改,在生成ReadView之前未提交的事务或者之后才开启的事务所做的更改是看不到的。而写操作针对的是最新版本的记录,读记录的历史版本和改动记录的最新版本本身并不冲突,也就是采用MVCC时,读-写操作并不冲突。
普通的select语句在Read committed 和 Repeatable Read隔离级别下会使用的MVCC读取记录
在Read Committed隔离级别下,一个事务在执行过程中每次执行select操作时都会生成一个ReadView,ReadView的存在本身就保证了事务不可以读取到未提交的事务所做的更改,也就是避免了脏读现象;
在Repeatable Read隔离级别下,一个事务在执行过程中只有第一次执行select操作才会生成一个ReadView,之后的select操作都复用这个ReadView,这样也就避免了不可重复读和幻读的问题
<br />方案二:读、写操作都采用加锁的方式<br />如果一些业务场景不允许读取记录的旧版本,而是每次都必须去读取记录的最新版本。<br />
综上所述,采用MVCC方式,读-写操作彼此并不冲突,性能更高,采用加锁方式,读-写操作彼此需要排队执行,影响性能。一般情况下采用MVCC来解决读写操作并发执行的问题。
二、共享锁和独占锁
共享锁(shared locks):简称S锁。在事务要读取一条记录时,需要先获取该记录的S锁;
独占锁(exclusive locks):简称X锁,也常称排他锁,在事务要改动一条记录时,需要先获取该记录的X锁;
假设事务T1首先获取了一条记录的S锁之后,事务T2接着也要访问这条记录:
- 如果事务T2想要再获取一个记录的S锁,那么事务T2也会获得该锁,也就意味着事务T1和T2在该记录上同时持有S锁。
- 如果事务T2想要再获取一个记录的X锁,那么此操作会被阻塞,直到事务T1提交之后将S锁释放。
假设事务T1首先获取了一条记录的X锁之后,那么不管事务T2接着想获取该记录的S锁还是X锁都会被阻塞,直到事务T1提交。
所以S锁和S锁是兼容的,S锁和X锁是不兼容的,X锁和X锁也是不兼容的。
三、锁定读的语句
3.1、对读取的记录加S锁
SELECT ... LOCK IN SHARE MODE;
3.2、对读取的记录加X锁
SELECT ... FOR UPDATE;
四、行锁和表锁
InnoDB存储引擎既支持表锁也支持行锁,表锁粒度粗,仅仅需要锁住几条记录,但使用表锁相当为表中的所有记录都加锁,所以性能比较差。行锁粒度更细,可以实现更精准的并发控制。