八股打卡二
MVCC机制(回顾)
MVCC(Multi-Version Concurrency Control)多版本并发控制,允许多个并发事务对数据进行读取和修改,而不会产生数据不一致的情况。它的核心思想是每个事务在数据库中看到的数据是事务开始时的一个快照,并不是实际最新的版本,这样保证了事务并发执行时互不干扰。
事务ACID中的隔离性就是通过锁和MVCC机制来实现的。
每一个Undo Log日志都有一个回滚指针,这个指针指向上一个版本的Undo Log。对于每一条记录都会构成一个版本链,新产生的Undo Log会插入到链表的头部。
在执行select事务的时候,它会创建一个ReadView,包含了版本链的信息。
- m_ids:正在活跃的事务
- id min_trx_id:版本链尾id
- max_trx_id:下一个要分配的事务的id(版本链头id+1)
- creator_trx_id:创建ReadView的事务的id
- 查询规则:
该版本由当前事务创建,直接返回(自己读取自己修改的数据),不是则进行下一个判断;
该版本事务id < min_trx_id,在ReadView创建之前数据已经提交,可以直接访问;
该版本事务id > max_trx_id, 在ReadView创建之后该版本开启,不能访问;
id在[min_trx_id, max_trx_id]之间,查看id是否在m_ids中,不在说明已经提交可以访问,在则不能访问。
索引失效的场景
- OR条件:当查询中有多个or条件,这些条件不涉及同一列,那么索引可能失效。数据库可能会全表扫描,而不会使用多个索引。
- 查询列进行数据转换:当查询中需要对列进行数据转换,如将String转为Date,那么索引可能会失效。
- 使用通配符前缀搜索:当查询使用通配符前缀(LIKE ‘prefix%’)进行搜索,那么索引可能会失效,大部分索引是按照列的完整值排序的。
- 不等号条件:查询中存在不等号条件,索引可能失效,通常情况下,索引只能用于等值比较。
- 连接中列类型不匹配:查询涉及的两个表中的列不匹配,索引可能失效。
MySQL的存储引擎
- MyISAM:不支持事务,不支持行级锁和外键约束。
- InnoDB:支持事务的ACID,支持行级锁和外键约束。
- Memory:将数据存储到内存,访问快,但是安全性不高。
MySQL的日志文件
- Undo Log:MySQL存储引擎层生成的日志,实现事务的原子性,主要用于事务回滚和MVCC机制。
- Redo Log:物理日志,记录数据页面进行了什么修改,每执行一个事务会产生一条或多条物理日志。
- Bin Log:归档日志,Server层生产的日志,主要用于数据备份和主从复制。
- Relay Log:中继日志,用于主从复制场景下,slave通过io线程拷贝master的bin log后本地产生的日志。
MySQL中的锁
全局锁
全局锁主要用于全库逻辑备份,在数据备份期间加上全局锁,防止数据或表结构更新导致备份数据与预期版本不一致的情况。加入全局锁,整个数据库处于只读状态。
表级锁
- 元数据锁:事务对数据库表进行操作时,表会被自动加上一个元数据锁,防止其他事务对表的修改,当前事务提交之后释放元数据锁。
- 意向锁:用于解决行级锁和表级锁之间的冲突。对某些记录加共享锁之前,要在表级别上加上意向共享锁;对某些记录加排他锁之前,要在表级别上加上意向排他锁。对于普通的select语句,不需要加行级锁,通过MVCC来保持一致性读。
- AUTO_INC锁(自增锁):数据库表的主键一般是自增的。在当前事务插入数据时,会给表加一个AUTO_INC锁,对auto_increment的字段赋值一个递增的值,插入事务提交后释放该锁。在插入期间,其他事务的插入被阻塞,保证了表中插入数据的字段值是连续递增的。
行级锁
- 记录锁(Record Lock):锁定一条记录,分为共享锁和排他锁。
- 间隙锁 (Gap Lock):只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下的幻读问题。间隙锁是兼容的,允许两个事务同时持有包含共同间隙范围的间隙锁,并不存在互斥。
- 临键锁(Next-key Lock):是记录锁和间隙锁的组合,能锁定一个范围并锁定记录本身,临键锁既能够保护记录,又能防止其他事务向被锁定记录前的间隙中插入数据。
- 插入意向锁:当前事务要插入数据时,需要事先看看在插入位置是否存在其他事务的间隙锁,如果存在,说明不能访问,进入阻塞状态,直到持有间隙锁的事务提交为止。阻塞期间会产生一个插入意向锁,表明该事务想在某个区间内插入数据,但是处于等待状态。
慢查询
什么是慢查询
数据库执行查询的时间超过指定的超时时间。
产生的原因
- 查询语句复杂:涉及到多个表,存在复杂的连接和子查询
- 查询数据量过大:尽管语句不复杂,但数据量过大会降低查询速度
- 缺乏索引:查询表中没有合适的索引,需要全表遍历
- 数据库表设计不合理:设计库表设计庞大,费时
- 存在并发冲突:多个查询访问相同的资源,可能导致并发冲突
- 硬件资源:MySQL服务器上的查询过多,服务器过载
优化方式
- 运行语句,找出慢查询的sql
- 查询区分度最高的字段
- 显示MySQL是如何选择索引执行sql语句的以及连接表,帮助找到更好的索引,写出更优的查询语句
- order by limit形式的sql语句,让排序的先查
- 创建索引原则
事务隔离的级别
读未提交
一个事务还未提交,它的修改就能够被其他事务看到。
容易发生脏读、幻读和不可重复读问题
读提交
一个事务提交之后,它的修改才能被其他事务看到。
容易发生幻读和不可重复读问题
可重复读
一个事务在执行过程当中看到的数据与事务开始时看到的数据是一致的。MySQL的InnoDB引擎默认的隔离级别。
可能发生幻读问题
串行化
对记录加读写锁,多个事务对记录进行读写操作,当发生读写冲突时,后执行的事务必须等待前一个事务执行完毕才能执行。
隔离级别从高到低:
串行化 > 可重复读 > 读提交 > 读未提交
tips:
脏读:一个事务读到其他事务修改未提交的数据,就发生了脏读
幻读:一个事务在执行多次查询,得到的结果记录前后不一样,就发生了幻读
如何防止脏读
MySQL InnoDB默认可重复读的隔离级别,很大程度上避免了幻读。但是无法完全解决,可以通过以下方式解决:
- 对于普通的select语句,通过MVCC机制解决了幻读。一个事务在执行过程中看到的数据与事务开启时看到的数据一致,即使其他事务插入了一条数据,它也是查询不到的。
- 对于select … for update语句一类的,通过使用临键锁(记录锁+间隙锁)可以防止幻读。如果有事务在临键锁范围内试图插入数据,那么会被阻塞,无法插入。
Redis的优缺点
Redis(Remote Dictionary Server)是一个基于内存的数据库,它的读写速度很快,常被用作缓存、消息队列、分布式锁、键值对数据库。支持多种数据类型,如字符串,列表,集合,有序集合,哈希表。它具有分布式的特性,能将数据存储在不同的节点上,提高了可用性和可扩展性。但是它不适合存储海量的数据,且存储的成本比磁盘高。
为什么Redis查询比较快
- 基于内存的操作:比磁盘I/O节约时间。
- 高效的数据结构:专门设计了String, list, hash等数据结构,提高读写效率。
- 单线程:单线程减少了线程上下文切换的开销和cpu消耗,没有资源竞争,避免死锁产生。
- IO多路复用:IO多路复用能够监听多个socket,根据socket事件的不同,分发到对应的事件处理器。
Redis的数据类型
- STRING:字符串,最基本的数据类型
- LIST:字符串元素的有序列表
- HASH:键值对,存储对象
- SET:无序集合,存储唯一的字符串元素
- ZSET:有序集合,类似集合,每个元素关联一个分数,可以按照分数排序
此外,Redis更新后增加了以下类型:
6. BitMap:存储位,用于位运算操作
7. HyperLogLog:用于基数估计的数据结构,统计元素数量
8. GEO:存储地理位置
9. Stream:专为消息队列设计
Redis单线程
Redis传统实现中是单线程的(网络请求处理时单线程,其他模块仍然是多线程),这意味着它使用单线程处理所有来自客户端的请求。好处:
- 简化模型:单线程模型简化了并发控制,避免了复杂的多线程同步问题。
- 性能优化:操作是在内存中进行的,避免了线程上下文切换开销和锁竞争开销。
- 原子性保证:单线程保证了操作的原子性,简化了事务和持久化的实现。
- 顺序执行:保证了请求的顺序执行。
虽然采用单线程,但是由于操作在内存中,依然有极高的吞吐量和低延时响应。
Redis6.0后增加了多线程,用于处理网络IO,能够充分利用cpu资源,减少网络IO阻塞带来的性能损耗。
Redis持久化
- AOF日志:每执行一条写操作指令,就将它追加写入一个文件。
- RDB快照:将某一时刻的内存数据以二进制的方式写入磁盘。
- 混合持久化方式:Redis4.0后新增的,集成了AOF和RDB的优势。
缓存三件套
缓存雪崩
缓存雪崩指的是在某个时间点,大量的缓存失效,造成直接访问数据库,增加了系统的负载。
解决:
合理设置缓存失效时间,分散缓存失效时间点,或者设置永不过期,配合定期更新缓存。
缓存击穿
缓存击穿指的是一个不在缓存中但在数据库中的数据,当大量的并发请求访问该数据,会导致直接访问数据库,增加数据库的负载。典型的场景:一个数据在缓存中过期或者被清理,有大量并发请求会直接访问底层存储系统。
解决:
设置互斥锁(分布式锁)或者在访问数据库前先看缓存中有没有,没有再访问数据库。
缓存穿透
缓存穿透指的是一个不在缓存也不在数据库中的数据,因为该数据永远不可能被缓存,有大量请求访问该数据会直接访问数据库,造成系统过载。典型场景:攻击者使用不存在的key大量访问缓存,造成数据库的频繁查询。
解决:
使用布隆过滤器过滤恶意请求,在查询数据库前进行参数的合法性校验。
如何保证缓存和数据库的一致性
- Cache Aside:先到缓存中访问数据,如果数据不在缓存中,直接访问数据库,再将数据加载到缓存;如果在缓存中则直接返回。更新操作时,先把数据持久化到数据库,再使缓存失效。
- Read Through/ Write Through
- Read Through:查询时更新缓存。缓存失效时,cache aside会让调用方将数据加载到缓存,而read through则是由缓存自行加载。
- Write Through:更新数据时,若缓存未命中,直接更新数据库,然后返回;若命中,则先更新缓存,再由缓存更新数据库。
- Write Behind:更新数据的时候,只更新缓存,不更新数据库,缓存异步批量更新数据库,这样的好处是数据IO操作非常快,坏处是数据不是强一致性的。