Mysql 隔离级别
-
读未提交,读已提交,可重复读:事务过程中看到的数据和事务启动时看到的数据是一致的;串行化:读会加读锁,写会加写锁,读写锁,写写锁冲突时,后一个事务需要前一个事务执行完;
-
隔离级别的实现:
读未提交:每次返回记录上的最新值;
读已提交:MVCC多版本视图,在sql 执行前开始创建视图;
可重复读:MVCC 多版本视图,在事务启动时创建视图;
串行化:加锁的方式规范并行访问;
- MVCC:是指每条记录在更新时都会同时记录一条回滚操作,通过回滚段就可以生成同一条记录的多个版本,每个版本和更新的 transaction_id 绑定,transation_id 是严格递增的,这样开启事务时,通过一个数组记录当前时刻启动了但未提交的 transactionId,那么每条记录的可见性,就通过比较是否小于该数组记录来判断,由此就生成了 mvcc 的视图;
mvcc 分析可见性,通过:1. 自己的更新总是可见;2. 版本未提交,不可见;3. 版本已提交,在视图创建后提交的,不可见;4. 版本已提交,而且是在视图创建前提交的,可见;
-
长事务需要记录大量的回滚段,且占用锁资源时间长,要尽量避免;
-
建议生产中开启自动提交,当需要手动定义一个事务边界时,使用 begin-commit/rollback;
全局锁和表锁:
-
全局锁就是对整个数据库实例加锁,主要用在数据库备份的场景中,加锁后整个数据库实例处于只读状态;
-
表锁是限制对单表的读写操作,语法是 lock tables table_name read/write,用在不支持行锁的存储引擎上控制并发;
-
另一个表级锁是 MDL,当对一个表做增删改查操作时,会加 MDL 读锁;当对表结构做变更操作时,会加 MDL 写锁;并且是系统默认加锁,且读锁和读锁不冲突,其他锁之间是相互冲突的;
-
要特别注意,给表加字段,修改字段,加索引,都是需要扫描全表的,并且加 MDL 写锁,会阻塞后面的所有增删改查操作请求,如果加 mdl 写锁也需要阻塞等待之前线程释放 mdl 读锁,所以可以在sql 中添加 wait n,nowait 来限制等待 mdl 写锁时间,避免长时间阻塞后续操作;
行锁
-
innodb 引擎支持实现,用来控制并发;
-
两阶段锁协议,事务中,行锁是在需要的时候才加上,要等到事务结束时才释放;因此事务中,要把最有可能冲突的记录尽量放在事务后半段执行,这样可以减少锁时间;
死锁
-
死锁,事务A update id =1,事务B update id = 2,事务B update id = 1,事务A udpate id = 2,此时两个事务就发生了死锁,解决方案,一种是设置锁等待超时,太长,如果发生死锁,业务需要阻塞长时间,太短,会将正常的锁等待当作死锁处理;
-
另一种是死锁检测,每一个等待锁的线程,都判断是否因为自己的加入造成了死锁,需要判断更新同一记录的所有线程,如果发现死锁,主动回滚自己的事务,推荐使用这种策略,因为第一种锁超时策略的超时时间难把握;
-
死锁检测有一定成本,特别对于并发竞争激烈的数据库行记录,因为更新操作都需要加锁,需要阻塞进行,并且死锁检测的成本会急剧增加,此时可以考虑将单行竞争记录的行记录,分为多行来记录,减少单行的并发修改操作;
当前读和更新丢失
-
update 操作,既需要加锁,同时都是先读后写的,并且是当前读,只能读当前的值,这样就不会发生更新丢失,除非业务代码中是先查询,然后根据查询结果来更新的,因为这个查询就不是当前读了,只会读到 mvcc 视图中的值;
-
普通的 select 语句是一致性读,根据 mvcc 视图来确定可见性,如果 select 语句加上 lock in share mode 或 for update,就是是当前读,能读到最新的值;
-
回滚丢失,任何隔离级别都不会出现;
-
示例参考:https://time.geekbang.org/column/article/70562?utm_source=pinpaizhuanqu&utm_medium=geektime&utm_campaign=guanwang&utm_term=guanwang&utm_content=0511;
数据库索引结构
-
哈希索引,适用于只有等值查询,不适合范围查找,因为无序存放,需要将范围区间全部扫描一遍;
-
有序数组索引,等值查找,使用二分查找就可以,范围查找,也只需要先找到大于等于区间的最小值,然后依次遍历,直到第一个大于区间最大值的即可,但这种结构索引的修改成本很高,因此适合静态数据;
-
二叉平衡树,查找和插入的时间复杂度都是 O(logN),树高 20,可以存储的节点是 100 万,通常每一层放入一个数据块中,那么一次查询可能就需要 20 次访问数据块,假设每次 io 访问数据块 10 ms,那么就需要 200ms,相当耗时;
-
n 叉平衡树,假设 n = 1200 时,树高 3,就可以存储 17 亿的记录了,那么最多三次 io 操作访问数据块,就可以查找到索引值,比较适合做数据库索引结构;
-
n 阶 b+ 树和 b 树,表示每个节点最多有 n -1 个关键字,n 个孩子节点,插入新的关键字时,如果节点关键字大于 n -1,就需要进行分裂,提取中间节点到父节点,b 树就是普通的 n 阶多叉树;
-
b+ 树,每个父节点都会在其右子节点冗余存储一份,所以树中所有关键字在叶子节点都会冗余存储,其非叶子节点就只会存储索引值,叶子节点会存储索引值和主键值,并且每个叶子节点都有相邻叶子节点的指针,通过指针,叶子节点就是按照关键字从小到大顺序连接的;
-
b+ 树这种结构,非叶子节点存储数据更多,每次查找都需要找到叶子节点,查询稳定性更高,并且叶子节点形成有序结构,更利于范围查找;
-
innodb 引擎中,使用的是 b+ 树的索引结构,表中数据是按主键的索引结构存放的,每一个索引都是一棵 b+ 树,其中主键索引,聚簇索引,其叶子节点是整行数据,非主键索引,叶子节点是主键值,所以非主键索引查找,通常需要回表;
主键通常选择自增,因为自增有序,很方便的维护索引树,如果无序的话,可能涉及数据页的分裂,合并,并且自增主键使得非主键索引的占用空间最小;
-
索引范围查找,是先找到区间最小值的索引值,然后按顺序遍历,直到找到第一个不满足范围区间的值即可;
-
使用联合索引是一个常用的性能优化手段,索引中包含所有要查询的字段列,联合索引有最左匹配的原则,有了 (a,b) 的联合索引,通常就不需要单独的 a 索引了;