存储引擎
数据库存储引擎是数据库底层软件组织,数据库管理系统(DBMS)使用数据引擎进行创建、查询、更新和删除数据。不同的存储引擎提供不同的存储机制、索引技巧、锁定水平等功能,使用不同的存储引擎,还可以获得特定的功能。常用的存储引擎有:InnoDB、MyIsam、Memory、Archive
在mysql v5.1版本之前,默认的存储引擎为MyIsam,而在其之后则变为了InnoDB引擎。
那么如何在mysql中查看默认引擎呢?进入mysql命令台后,输入:
show variables like '%engine%';
结果如下图:
那么如何查看mysql所支持的存储引擎呢?输入:
show engines;
结果如下图:
你会发现该命令也能看到mysql的默认引擎,而且在Comment一列中其实就能看到各存储引擎的主要特点了,下面根据上图简单介绍下各存储引擎:
InnoDB:其底层存储结构为B+树,即非叶节点不存储数据,所有数据都在叶节点中,且各叶节点构成一个有序的链表,在上一篇中已经讲过树的结构。从上图很明显看到该引擎支持事务、行内锁以及外键约束,且只有该引擎支持事务和外键,它也是我们日常开发中所用的最多的存储引擎了,由于是行级锁定,颗粒度较小,所以在并发处理方面表现比较好。
MyIsam:不支持事务、外键、行内锁,在执行Insert或Update操作时,由于不支持行内锁,所以采取的是表级锁定,即会对整个数据库进行加锁,效率便会低一些,但是也很好的避免了死锁问题。在读取操作时该引擎执行速度很快。
Memory:由上图可知,该引擎基于hash索引(但是也可使用B树索引),存储在内存中,所以查询速度非常快,一般用作临时表存储,适用于存放查询的中间结果。一旦服务关闭,所有数据将丢失,所以适合那些对安全性要求不高的场景。
Archive:如果只有Insert和Select操作,可以使用该引擎,其支持高并发的插入操作,一般用作存储历史数据,因为只涉及上述所说的两种操作。
事务知识
由于我们最常用的InnoDB存储引擎同时支持事务及行锁,下面针对这两个进行补充复习下:
1、事务概念及ACID特性
数据库事务是指一组要么全部执行成功,要么全部执行失败的sql语句,是数据库中最小的执行单位。事物的四大特性如下(ACID):
原子性:类似于java的原子性,同事务的概念,事务所包含的操作要么全部执行成功要么全部执行失败,只关注状态。
一致性:事务执行前和事务执行后数据库都处于一致性的状态,关注数据的可见性。
隔离性:多个并发事务执行时,两两互不影响,数据库提供了多种隔离级别来确保事务的隔离性。
持久性:事务一旦被执行,其对数据库的改变就是永久的。
2、并发事务所带来的问题
相比较串行处理,并发事务处理能大幅提高数据库资源的利用率,提高数据库系统的吞吐量,从而可以同时支持更多的用户,但同时并发事务下也会产生问题,例如经典的:更新丢失、脏读、不可重复读、幻读
更新丢失:当两个或多个事务并发执行更新操作时,都基于各自最初选定的值对同一行数据执行操作时,由于各自都不知道还有其他事务的存在,即会造成最后的更新操作覆盖了其他人的更新,产生更新丢失。
脏读:当数据库的一个事务A正在使用一个数据但还没有提交事务,另外一个事务B也访问到了这个数据,还使用了这个数据,这就会导致事务B使用了事务A没有提交之前的数据。
不可重复读:在一个事务A中多次操作一个数据,在这两次或多次访问这个数据的中间,事务B也操作此数据,并使其值发生了改变,这就导致同一个事务A在两次操作这个数据的时候值不一样,这就是不可重复读。
幻读:是指事务不独立执行产生的一种现象。事务A读取与搜索条件相匹配的若干行。事务B以插入或删除行等方式来修改事务A的结果集,然后再提交。这样就会导致当A本来执行的结果包含B执行的结果,这两个本来是不相关的,对于A来说就相当于产生了“幻觉”。(换种说法,事务 A 根据条件查询得到了 N 条数据,但此时事务 B 删除或者增加了 M 条符合事务 A 查询条件的数据,这样当事务 A 再次进行查询的时候真实的数据集已经发生了变化,但是A却查询不出来这种变化,因此产生了幻读)
幻读与不可重复读的区别:
幻读说的是存不存在的问题:原来不存在的,现在存在了,则是幻读
不可重复读说的是变没变化的问题:原来是A,现在却变为了B,则为不可重复读
3、事务隔离级别
首先先给出两个命令用以查询mysql的隔离级别,在mysql v8.0之前:
a、查询当前会话的隔离级别:
select @@tx_isolation;
b、查询当前系统隔离级别:
select @@global.tx_isolation;
在mysql v8.0及之后的版本,使用上述命令会报找不到tx_isolation变量,那是因为换成了transaction_isolation,替换上述变量即可
结果如下图:
下面切入正题,在并发事务处理所带来的问题中,“更新丢失”通常应该是完全避免的。但防止更新丢失,并不能单靠数据库事务控制器来解决,需要应用程序对要更新的数据加必要的锁来解决,因此,防止更新丢失应该是应用的责任。脏读、不可重复读、幻读其实都是数据库读一致性的问题,这里就需要数据库的隔离级别来控制了。一般的数据库,都含有以下四种隔离级别:
读未提交:顾名思义能读到未提交的内容,无特殊情况一般不会该种隔离级别
读已提交:只能读到已经提交的内容,能避免脏读现象,这是各种系统最常用的一种隔离级别,Sql Server和Oracle的默认隔离级别即为它。需要注意的是,除非在查询中显示加锁,如:
select * from T where ID=2 lock in share mode;
select * from T where ID=2 for update;
不然普通的查询是不会加锁的,那么是如何避免脏读的呢?即采用"快照"机制,这种即能保证一致性又不加锁的操作称为"快照读"
可重复读:顾名思义就是为了避免不可重复读现象,mysql的默认隔离级别即为它。在该级别下同样采用"快照读"的方式,但同时当事物启动时,就不允许进行修改操作(update),而不可重复读现象恰恰是因为两次数据读取之间进行了数据的修改,因此能有效的避免不可重复读现象。
串行化:这是数据库最高的隔离级别,即事务串行化顺序执行,一个一个排队等待,虽然能避免以上所有现象,但是执行效率奇差,所以基本没人会用。
下面比较下四种隔离级别:
隔离级别/读数据一致性及允许的并发副作用 | 读数据一致性 | 脏读 | 不可重复读 | 幻读 |
读未提交(Read uncommitted) | 最低级别,只能保证不读取物理上损坏的数据 | 是 | 是 | 是 |
读已提交(Read committed) | 语句级 | 否 | 是 | 是 |
可重复读(Repeatable read) | 事务级 | 否 | 否 | 是 |
串行化 (Serializable) | 最高级别,事务级 | 否 | 否 | 否 |
总结:综上所述,数据库实行事务隔离的方式,基本上可以分为以下两种:
a、在读取事务前,对其进行加锁,防止其他事务对数据进行修改
b、不加锁,通过一定机制生成一个数据请求时间点的一致性数据快照,并用这个快照来提供一定级别(语句级或事务级)的一致性读取。从用户的角度,好像是数据库可以提供同一数据的多个版本,因此,这种技术叫做数据多版本并发控制(MultiVersion Concurrency Control,简称MVCC或MCC),也经常称为多版本数据库。
锁机制
mysql各存储引擎大致有使用三种形式的锁机制,如下:
1、表级锁(table-level):锁定颗粒度最大,实现方式简单粗暴,直接锁定整张表,不会产生死锁问题。常用在:MyIsam、Memory、CSV等一些非事务性引擎
2、行级锁(raw-level):锁定颗粒度最小,所以发生锁定资源争用的概率也最小,能够给予应用程序尽可能大的并发处理能力,但是存在死锁的可能。常用在:InnoDb引擎,InnoDb实现了两种形式的行级锁:
a. 共享锁(S):又称读锁,顾名思义多个事务对于同一数据可共享一把锁,都能访问到数据,但是只能读不能修改,可通过select ... lock in share mode语句对数据进行加共享锁
b. 排他锁(X):又称写锁,顾名思义排它锁不能与其他锁并存,即当一个事务获取到某行数据的排他锁,其他事务就不能再获取该行数据的其他锁,包括排他锁和共享锁,但是获取排他锁的事务是可以对数据进行读取和修改。可通过select ...for update语句对数据进行加排它锁。这里有个点:就是当排他锁锁住某行数据时,其他事务在使用普通查询select ...from...语句是仍然可以读取该数据的,因为普通查询没有任何锁机制。
另外,为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁,意向共享锁和意向排他锁是数据库主动加的,不需要我们手动处理
c. 意向共享锁(IS):其作用在于:通知数据库接下来需要施加什么锁并对表加锁。如果需要对记录A加共享锁,那么此时innodb会先找到这张表,对该表加意向共享锁之后,再对记录A添加共享锁。
d. 意向排他锁(IX):与意向共享锁作用相同,即如果需要对记录A加排他锁,那么此时innodb会先找到这张表,对该表加意向排他锁之后,再对记录A添加排他锁。
行锁的注意事项:
InnoDB行锁是通过给索引上的索引项加锁来实现的,只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁,在实际应用中,要特别注意InnoDB行锁的这一特性,不然的话,可能导致大量的锁冲突,从而影响并发性能。下面通过一些实际例子来加以说明。
1、在不通过索引条件查询的时候,InnoDB确实使用的是表锁,而不是行锁。
2、由于MySQL的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行的记录,但是如果是使用相同的索引键,是会出现锁冲突的。
3、当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,另外,不论是使用主键索引、唯一索引或普通索引,InnoDB都会使用行锁来对数据加锁。
4、即便在条件中使用了索引字段,但是否使用索引来检索数据是由MySQL通过判断不同执行计划的代价来决定的,如果MySQL认为全表扫描效率更高,比如对一些很小的表,它就不会使用索引,这种情况下InnoDB将使用表锁,而不是行锁。因此,在分析锁冲突时,别忘了检查SQL的执行计划,以确认是否真正使用了索引。
3、页级锁(page-level):颗粒度介于表级锁与行级锁之间,是mysql一种比较独特的锁机制,在其他数据库管理软件中并不常见,同样存在死锁的可能。常用在:BerkeleyDB引擎
悲观锁与乐观锁
这其实只是两类锁的实现思想,以前总是理解的一知半解,迷糊时甚至还以为是啥特定的锁
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞等待直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁、表锁、读锁、写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。悲观锁适合多写的场景,因为此时冲突会比较多。
乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,乐观锁适用于多读的应用类型,这样可以提高吞吐量,乐观锁可以使用版本号机制和CAS算法实现,下面简单介绍下
乐观锁的两种实现方式
1、版本号机制:
一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值与当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
2、CAS算法:即compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三个操作数:
- 需要读写的内存值 V
- 进行比较的值 A
- 拟写入的新值 B
当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。