数据库进阶: 锁, 事务, 数据库引擎,字符集

目录

 

1.1 锁

1.2 事务

1.3 数据库引擎

1.4 字符集


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), 恢复速度块
    • 支持行级锁&表级锁
      • 并发访问时效率高
    • 支持外键约束
    • 插入/更新/主键查询
    • 需要内存和硬盘多
    • 常规推荐使用
  • 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; 可以显示出注释

猜你喜欢

转载自blog.csdn.net/Regan_Yu/article/details/95180918
今日推荐