一、事务
数据库事务的特性?
数据库事务的四大特性是ACID。
- 原子性:就是所有操作要么全不做,要不全做。通过undo日志来实现。
- 一致性:就是在并发情况下数据库由一个状态转移到另一个状态的数据要一致。通过事务的隔离级别来实现。
- 隔离性:多个事务的操作互相不影响,数据有4种隔离级别来解决。基于锁+mvcc机制来实现。
- 持久性:数据库中事务一旦提交,数据持久到硬盘中。基于redo日志来实现。
数据不一致的问题
- 脏读:事务A读取一个数据,事务B对该数据进行修改,修改前后的数据都可以读到,但是数据会不一样,导致了脏读。脏读强调的是读到其他事务未提交的数据。
- 不可重复读:事务A读一个数据,事务B对该数据进行修改后提交,导致事务A前后读取的数据值不一致。不可重复度强调的是读到其他事务提交后的数据和提交前的数据不一致。
- 幻读:事务A读取一个数据,事务B在insert、delete增删记录,事务A两次读取的数据条数不一致;
事务的隔离级别
事务的隔离级别描述的是在多个事务同时运行过程中各自事务中数据的隔离办法,事务的隔离级别是为了解决数据库中以上常见的三类问题,包括:脏读、不可重复读和幻读的问题,者四类隔离级别分别为:
- 未提交读:事务A可以读到事务B未提交的数据;
- 已提交读:事务A只能读到事务B提交后的数据;
- 可重复读:事务A能读取到事务B还未执行完的数据,但是读取到的是事务B未开始事务前的数据,只有当事务B执行完成了才会读取到新的数据;
- 可串形化:事务A、事务B的读写操作是串形化的;
总结:
- 以上4类隔离级别的约束条件从宽松到严格;
- 未提交读隔离级别没有解决任何问题;
- 已提交读解决了脏读的问题;
- 可重复读解决了不可重复读的问题,但还存在幻读的问题,但结合MVCC和record lock能解决幻读的问题;
- 只有可串形化隔离级别能解决所有数据不一致的问题;
- Mysql的默认隔离级别是可重复读。
mysql如何解决幻读?
在快照读情况下,即不加锁的非阻塞读,像普通select操作下通过mvcc避免幻读;通过乐观锁类避免。
对于可重复读的隔离级别下,select * from table的语句是采用快照读的模式,新的事务会读快照;
在当前读情况下,当前读表示需要读取当前最新数据,是加锁的阻塞读写操作,像普通update/insert/delete和加update的select操作,是通过next-key lock避免幻读;通过悲观锁来避免。
对于可重复读的隔离级别,如果是当前读场景,需要通过间隙锁(Gap Lock)模式来避免幻读,这样行间间距会被加锁,不会被新的事务插入新的数据,间隙锁加行锁合起来称为next-key lock;
二、锁
锁是计算机中协调多线程并发访问共享资源的机制,在数据库中就是协调多个事务同时访问同一数据记录的机制,在Mysql数据库中进行加锁的对象是索引中的索引项,会在索引项上做加锁标记。
锁的分类
mysql数据库的锁分为行锁和表锁。
- 行锁。是innodb存储引擎的默认加锁方式,特点是开销大,加锁慢,锁力度小,所以并发度高,适合写多读少场景。
表锁。是myisam存储引擎的默认加锁方式,特点是开销小,加锁快,锁粒度大,所以并发度低,适合读多写少场景。
记录锁(Record lock):即是行级锁,是innodb存储引擎的默认加锁方式。
- 间隙锁(Gap lock):会对记录之间的左开右闭的区间进行加锁;
- 临键锁(Next key Lock):记录锁+间隙锁统称为临键锁。
- 共享锁(读锁)&独占锁(写锁):共享锁又称S锁/读锁,对记录进行读操作时进行加的锁,可以共享;独占锁又称X锁/写锁,对记录进行写操作时进行加锁,只能独占;
- 意向共享锁&意向排它锁:为了解决对记录加了共享锁&独占锁,再对表加表锁的时候需要遍历,所以在对行加共享锁&独占锁时候,会对表加上意向共享锁&意向排它锁,下次加表锁一目了然,这是一类表锁。
- 插入意向锁:就是多个事务在进行插入的时候,已经有事务在进行插入操作了,另一个在等待的事务需要加上插入意向锁,用以表面针对目前的间隙位置有插入的需求;
数据库里的乐观锁和悲观锁?
所有需要加锁的操作都是在进行多线程情况下才需要的。
- 乐观锁比较好理解,就是预测所有线程对数据的操作都是不会冲突的,所以每次对数据进行操作时候都不会加上锁;mysql内部机制可以通过mvcc机制来是实现乐观锁;
- 悲观锁则相反,就是认为每次线程对数据的操作都可能存在冲突,所以需要对数据加上行锁、表锁等。mysql主要通过record lock+nextKey lock来实现。
mysql的悲观锁实现?
mysql加锁是通过锁住索引来实现的,如果没有索引和主键则会锁住整张表。mysql是通过record lock、Gap lock和next-key lock来实现加锁策略的。
- Record lock:对行进行加锁
- Gap lock:对间隙进行加锁
- Next-key lock:Record lock+Gap lock的统称,innodb的默认实现方案;
InnoDB在执行查询语句SELECT时(非串行隔离级别),不会加锁。但是update、insert、delete操作会加行 锁。
mysql默认是autocommit提交事务,如果需要对select语句加锁,需要使用手动设置加锁操作。手动设置加锁的步骤:
begin;
Select * from xxx where xxx for update//加上for update进行强制加写锁;
select * from xxx where xxx lock in share mode//加上lock in share mode进行强制加读锁
进行各种操作//这样的话在还么有commit之前,该行记录一直处于锁定状态;
commit;
mysql的乐观锁mvcc机制原理?
mysql是通过mvcc机制来实现乐观锁的。mvcc是一种不加锁的方式解决读写冲突的机制,支持mysql上了写锁,还能继续支持读,解决读操作阻塞写操作的问题。在mysql中对一行记录的读写,默认使用的是mvcc机制,避免频繁使用加锁机制影响性能。
主要通过隐含字段、undo日志和read view来实现。核心思想是在select查询操作前生成一个read view(一致性读视图)中事务id的大小,用来判断是否需要从undo日志组成的版本链中读取历史数据。
- 隐藏字段:每一条记录都会隐藏row_id、trx_id、roll_ptr。
- undo日志:mysql中的undo日志记录操作前的状态,可以实现回滚到之前状态。
- read view:mysql中存在当前读和快照读的概念,mvcc是通过快照读来实现多线程并发操作的,每次进行读操作时,会生成一个read view,读取之后比较事务id的大小来判断是否进行了一致性读。
mysql本身如何处理死锁问题?
Mysql目前机制是死锁超时会退出,将持有最少行级互斥锁的事务回滚。针对mysql无法自己解决的死锁问题,需要通过线程日志找到发生死锁的线程id,再通过kill 杀掉发生死锁的线程。
如何避免死锁?
问题说明:
两个或多个事务占用同一个共享资源,并且互相请求对方资源的锁,这样就会造成死锁。比如事务A请求事务B加锁的数据,同时事务B请求事务A加锁的数据,这样就会形成死锁情况。
解决方案:
- 让数据的查询尽量索引完成,避免索引加锁失败,从行级锁升级成表锁。
- 保持事务的顺序执行;
- 尽量减少查询范围,避免间隙锁加锁的范围;
- 事务执行逻辑简短,减少加锁时间;
- 死锁检测。执行之前做循环引用的检测。
Mysql只操作一条记录也有可能发生死锁吗?
有可能。Mysql的加锁对象是索引而不是记录,所以当一个事务对该记录的非主键索引加锁,并请求主键索引的锁,但另一个事务对该记录的主键索引已加锁,并请求该记录的非主键索引的锁,那么就会进入死锁状态。
如何优化事务
- 将数据处理工作放在事务外进行,缩短在事务中执行的时间;
- 不要一次性创建逻辑流程太长的事务,将这些事务拆分成多个小事务;
- 事务中不要进行RPC等耗时较长的操作,调用超时可能导致事务超时;