目录
1.1 锁
- 目的
- 解决并发情况下资源抢夺问题, 维护数据的一致性
- mysql的锁虽然开发者可以手动设置, 但比较影响并发性, 一般会使用乐观锁代替(如Django中的库存问题)
- 由于mysql会自动使用锁, 所以需要了解锁的机制, 以便优化数据库并发能力
- 粒度/覆盖范围
- 表级锁
- 对整个表锁定, 并发差, 资源消耗少
- 行级锁
- 对数据行锁定, 并发好, 资源消耗多
- 不同数据库引擎支持的锁也不同
- MyISAM (5.5之前默认)支持表级锁
- InnoDB 支持行级锁和表级锁
- 表级锁
- 锁和事务
- 无论操作是否在事务中, 都可以获取锁, 只不过在事务中, 获取的锁只有执行完事务才会释放
- MyISAM
- 只支持表级锁
- 表读锁/共享锁
- 获取后, 其他请求可以读不能写
- 表写锁/排它锁
- 获取后, 其他请求既不能读也不能写
- 加锁方式
- 数据库自动管理, SELECT前给设计的表添加读锁, 更新前(增删改)给涉及的表加写锁
- InnoDB
- 支持行级锁和表级锁, 优先使用行级锁
- 行共享锁
- 获取后, 其他事务也可以获取目标集的共享锁, 但是不能获取目标集的排它锁(排队等待)
- 行排它锁/互斥锁
- 获取后, 其他事务既不能后去目标集的共享锁, 也不能获取对应的排它锁
- 加锁方式
- 增删改必须获取排它锁, 普通查询不需要获取锁
- 加锁查询
- selet * from t_user where name='zs' lock in share mode 获取目标集共享锁后, 执行查询
- select * from t_user where name='xx' for update 获取目标集排它锁后, 执行查询
- 行锁与读写权限
- 行共享锁
- 获取行共享锁后, 当前事务可以读(不影响), 不一定能写(其他事务也获取读锁, 只能等待), 其他事务可以读, 不能写
- 共享锁容易出现死锁陷阱
- 行共享锁
# 准备数据
create table t_deadlock(
id int not null atuo_increment, name varchar(20),
type int,
key (type),
primary key (id)
);
insert into t_deadlock (name, type) values ('zs', 1);
insert into t_deadlock (name, type) values ('ls', 2);
insert into t_deadlock (name, type) values ('ww', 3);
# 需求: 对zs的type做加1操作, 为防止资源抢夺(更新丢失), 设置锁
---事务1----
begin;
select type from t_deadlock where name='zs' lock in share mode; # 共享锁
---事务2----
begin;
select type from t_deadlock where name='zs lock in share mode; # 共享锁
---事务1----
update t_deadlock set type=2 where name='zs'; # 等待事务2释放共享锁
---事务2----
update t_deadlock set type=2 where name='zs'; # 等待事务1释放共享锁
# 相互等待, 产生死锁
# 更新丢失的解决办法
# 1. 使用update子查询更新 (乐观锁)
update t_deadlock set type+=1 where name='zs';
# 2. 查询时直接使用排它锁 (悲观锁)
select type from t_deadlock where name='zs' for update;
- 行排它锁
- 获取后, 当前事务既可以读, 也可以写; 其他事务可以读, 不能写
# 需求: 记录的数量=3, 才插入一条数据
---事务1----
begin;
select count(*) from t_deadlock; # 获取记录数量为3
---事务2----
begin;
select count(*) from t_deadlock; # 获取记录数量为3
---事务1----
insert into t_deadlock (name, type) values ('zl', 1);
commit; # 插入成功
---事务2----
insert into t_deadlock (name, type) values ('fq', 1);
commit; # 插入成功, 结果插入了两条数据
# 并发插入的解决办法: insert后边不能直接连接where, 并且insert只锁对应的行, 不锁表,
# 不会影响并发的插入操作(无法使用乐观锁完成需求), 只能在查询时就手动设置排它锁(悲观锁)
---事务1----
begin;
select count(*) from t_deadlock for update; # 获取记录数量为3
---事务2----
begin;
select count(*) from t_deadlock for update; # 等待获取排它锁
---事务1----
insert into t_deadlock (name, type) values ('zl', 1);
commit; # 插入成功
---事务2----
select count(*) from r_deadlock for update; # 事务1完成, 获取到记录量为4, 不再执行插入操作
- 行锁是通过 给索引加锁 实现的, 如果 查询时没有触发索引, 就会锁表
- 使用 RC 级别, 只锁行, 不锁表 (Read Committed)
- 合理的索引很重要
- 间隙锁
- 在击中索引的情况下, 获取行锁时, InnoDB不仅会对符合条件的已有数据行加锁(record lock), 对于键值在条件范围内但并不存在的记录, 叫做 '间隙(GAP)', InnoDB也会对这个"间隙"加锁(gap lock)
- InnoDB完整的行级锁机制为 next key lock = record lock + gap lock
- 缺点
- 会阻塞符合条件的插入操作
# gap锁场景1: 使用范围条件
begin; # 事务1
select * from t_user where age<30 for update;
# 如果此时事务2插入记录(a<30), 则会阻塞 (age不是索引触发表锁, age是索引触发的是间隙锁)
# gap锁场景2: 锁定索引的前后区间 [prev, next]
update t_user set name='lisi' where age=30;
# 如果age为索引, 且数据中最接近age=30的值为20和40, 则age=[20, 40)的范围也会被锁定, 左闭右开
- 目的
- 防止幻读
- 解决办法
- 尽量不要对由频繁插入的表进行范围条件的检索
- 使用 RC 级别(不存在间隙锁)
- 使用唯一索引/主键索引进行查询(间隙锁只会对普通索引生成)
# 查看隔离级别
SELECT @@global.tx_isolation, @@session.tx_isolation;
# 设置隔离级别(重启后会重置)
SET [SESSION|GLOBAL] TRANSACTION ISOLATCION LEVEL [READ UNCOMMITTED|READ COMMITTED|REPEATABLE READ|SERIALIZERABLE]
# 修改配置问及爱你设置隔离级别(重启不重置)
[mysqld] transaction-isolaction = READ-COMMITTED
1.2 事务
- 目的
- 保证数据库安全稳定的运行技术
- 四大特性 ACID 原子性 一执性 隔离性 持久性
- 原子性
- 要么都成功, 要么都失败
- 实现机制是undo log
- 一致性
- 操作前后, 系统稳定, 数据一致
- 原子性不代表一致性
- 脏读/不可重复读/幻读
- 解决办法: 调整事务隔离级别
- 提交事务后, 只有一半操作持久化成功
- 解决办法 redo log
- 脏读/不可重复读/幻读
- 隔离性
- 每个事务是独立的, 相互不会影响
- 实现机制 多版本并发控制+锁
- 持久性
- 保证事务的执行结果一定在数据库中同步完成, 无论数据库是否瘫痪
- 实现机制 refo log
- 原子性 持久性 隔离性 实现了一致性
- 原子性
- MVCC 多版本并发控制
- 简单来说就是对数据做了多版本处理
- 事务隔离级别中的 RC 和 RR 就是 MVCC 实现的
- RR 可重复读级别
- 快照读 select * form xx; # 在事务中无论读多少次都和第一次读的结果一样
- 当前读 select * from xx lock in share mode; select * form xx update insert update delete
- RC 读取已提交级别
- 仍然有快照读, 事务提交成功的数据可以读取得到, 但读不到未提交的数据
- 事务隔离级别
- 四个级别, 只会用到读已提交和可重复读这两个
- mysql默认为可重复读
- 更建议使用 RC
- 不会出现间隙锁(影响并发)
- 索引没触发, 不会锁表, 只是锁行
- 不可重复和幻读问题, 一般不需要管, 如果有强制一致性要求, 加悲观锁/乐观锁
- UNDO
- 作用
- 用于回滚, 实现事务的原子性
- 实现多版本并发控制(MVCC)
- 原理
- 在数据操作执行之前,先将牵扯到的数据备份到 undo.log, 然后再进行数据的操作
- 作用
- Undo Log参与事务的简化过程
假设有A, B两个数据, 值分别是 1, 2. A. 事务开始 B. 记录A=1到undo log. C. 修改A=3.(写入缓冲区) D. 记录B=2到undo log. E. 修改B=4. F. 事务提交
-
- 如果出现回滚操作, 系统可以利用Undo Log中的备份将数据恢复到事务开始之前的状态
- Undo Log必须先于数据持久化到磁盘. 如果在D, E 之间系统崩溃, undo log是完整的, 可以用来回滚事务.
- REDO
- Redo Log记录的是新数据的备份
- 作用
- 保证事务持久性
- 原理
- 新数据写入到内存缓冲区后, 将执行的更新操作写入到redo log, 再将数据写入磁盘(一定发生在redo写入之后, 但未必立即执行)
- 当系统崩溃时, 虽然数据没有写入磁盘, 但是Read Log已经持久化. 系统可以根据Redo Log的内容, 将所有数据恢复到最新的状态
- 虽然redo log和 写入数据库 都是写入磁盘, 但是redo log 的性能高于 写入数据库
- Undo + Redo事务的简化过程
假设有A, B两个数据, 值分别是1, 2.
A. 事务开始.
B. 记录A=1到undo log.
C. 修改A=3. (写入到缓冲区)
D. 记录A=3到redo log. (之后的某个时间点写入磁盘)
E. 记录B=2到undo log
F. 修改B=4.
G. 记录B=4到redo log.
J. 事务提交
-
- 数据库恢复
- msyql重启后自动进行
- 先 REDO, 再 UNDO
- 进行恢复时,
- REDO不区分事务, 会重做所有操作(包括未提交的操作和最终回滚的操作)
- 然后再由UNDO来回滚未提交和要执行回滚的事务
- 数据库恢复
- 关于锁和事务的优化建议
- 使用RC隔离级别
- 精心设计索引, 并尽量使用索引访问数据, 使加锁更加精确, 从未减少锁冲突的机会
- 选择合理的事务大小, 小事务冲入的记录表也更小
- 给记录集显式加锁时, 最好一次性请求足够级别的锁. 比如要修改数据的化, 最好直接申请排他锁, 而不是先申请共享锁, 修改时再申请排他锁, 这样容易产生死锁
- 不同的程序访问一组表时, 应该尽量约定以相同的顺序访问各表, 对一个表而言, 尽可能以固定的顺序存取表中的行. 这样可以大大减少死锁的机会
- 尽量用相等条件访问数据, 这样可以避免间隙锁对并发插入的影响
- 除非必须, 查询时不要显式加锁. MySQL的MVCC可以实现事务中的查询不用加锁, 优化事务性能; MVCC只在READ COMMITTED (读提交) 和 REPEATABLE READ (可重复读) 两种隔离级别下工作
1.3 数据库引擎
- 实现数据库存储的不同解决方案
- InnoDB mysql5.5开始 默认
- 支持事务(回滚/提交/ACID特性/多版本并发控制等)
- 数据恢复可使用事务日志(undo redo log), 恢复速度块
- 支持行级锁&表级锁
- 并发访问时效率高
- 支持外键约束
- 插入/更新/主键查询
- 需要内存和硬盘多
- 常规推荐使用
- 支持事务(回滚/提交/ACID特性/多版本并发控制等)
- MyISAM
- 不支持事务
- 不支持外键约束
- 只支持表级锁
- 批量插入/查询/count()速度块
- 简单, 适合小型项目/以批量插入和查询为主的系统(内部管理系统)
- 系统公告表选择MyISAM
- 因为基本不会修改, 不存在大量并发写操作, 也就不需要行级锁和事务为数据安全隐私做保障
- 查询多, MyISAM会更快
1.4 字符集
- 字符集问题
- utf-8如果保存数据中包含表情符号会崩溃
- utf-8编码最大字符长度为3字节, 而Unicode中大编码实现的表情符号(emoji)为4字节
- 编码方式需要设置为utf8mb4
- sql注释 COMMENT xxx
- show create table xxx; / show full columns from test; 可以显示出注释