概念
锁,在现实生活中是为我们想要隐藏于外界所使用的一种工具,在计算机中,是协调多个进程或线程并发访问某一资源的一种机制,在数据库当中,除了传统的计算资源(CPU、RAM、I/O等等)的争用之外,数据也是一种供许多用户共享访问的资源,如何保证数据并发访问的一致性、有效性,是所有数据库必须解决的一个问题,锁的冲突也是影响数据库并发访问性能的一个重要因素
举例
在购买商品时, 商品库存只有 1 个时,两个人同时买时, 谁买到的问题,会用到事务, 先从库存表中取出物品的数据, 然后插入订单,付款后,插入付款表信息,更新商品的数量, 在这个过程中, 使用锁可以对有限的资源进行保护,解决隔离和并发的矛盾
锁分类
按操作分
读锁(共享锁):针对同一份数据,多个读取操作可以同时进行而不互相影响
写锁(排它锁):当前写操作没有完成前,会阻断其他写锁和读锁
按粒度分
- 表锁
- 行锁
- 页锁
表锁
偏向 MYISAM 存储引擎,开销小,加锁快,无死锁,锁定粒度大,发生锁冲突的概率最高,并发最底,整张表就只能一个人使用
示例
1.建立一张Myisam引擎的表
1 | CREATE TABLE `locktest` ( |
2.查看表有没有被锁过
1 | SHOW OPEN TABLES; |
3.对表加锁
1 | LOCK TABLE locktest READ, |
4.对表进行解锁
1 | UNLOCK TABLES |
读写锁对操作和性能产生哪些影响
1.对 locktest 添加读锁 lock table locktest read;(共享)
当前连接
是否可以查看自己 | 可以 |
---|---|
是否可以更新 | 不可以 |
能不能读别的表 | 不可以,当前表还没有解锁,不能放下当前, 操作别的内容 |
另一个连接
是否可以查看 | 可以 |
---|---|
是否可以更新 | 当更新时,处理阻塞状态,等待解锁后,才能进行更新 |
能不能读别的表 | 可以 |
2.对 locktest 添加写锁 lock table locktest write;(排它)
当前连接
能否读自己锁过的表 | 可以 |
---|---|
能否改自己锁过的表 | 可以 |
能否读取别的表 | 不可以 |
另一个连接
能否没有加过锁的表 | 可以 |
---|---|
能否对被锁过的表进行操作 | 阻塞,等待解锁时,能查到 |
表锁分析
1 | SHOW STATUS LIKE 'table%'; |
- Table_locks_immediate:产生表级锁定的次数,表示可以立即获取锁的查询次数
- Table_locks_waited:出现表级锁定争用而发生等待的次数
Myisam 的读写锁调度是写优,这也是 myisam 不适合做写为主表的引擎,因为写锁后,其它线程不能做任何操作,大量更新会使用查询很难得到锁,从而造成永久阻塞
淘宝
买家库,偏向读取
卖家库,更多偏向写
行锁
偏向 InnoDB 存储引擎,开销大,加锁慢,会出现死锁,锁定粒度最小,发生锁冲突的概率最底,并发度也最高,InnoDB 与 MyISAM 的最大不同点:一是支持事务,二是采用了行级锁
事务
ACID属性
- 原子性(Atomicity)原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚。
- 一致性(Consistency)一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。让数据保持一定上的合理,一个商品出库时,仓库商品数量减 1,对应用户的购物车中商品加 1。
- 隔离性(Isolation)隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。
- 持久性(Durability)持久性是指一个事务一旦被提交了,就不能再回滚了,已经把数据保存到数据库当中了。
并发事务处理带来的问题
更新丢失:两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其它事务的存在,就会发生丢失更新问题,最后的更新覆盖了其它事务所做的更新
脏读:老板要给程序员发工资,程序员的工资是 3.6 万/月。但是发工资时老板不小心按错了数字,按成 3.9 万/月,该钱已经打到程序员的户口,但是事务还没有提交,就在这时,程序员去查看自己这个月的工资,发现比往常多了 3 千元,以为涨工资了非常高兴。但是老板及时发现了不对,马上回滚差点就提交了的事务,将数字改成 3.6 万再提交,实际程序员这个月的工资还是 3.6 万,但是程序员看到的是 3.9 万。他看到的是老板还没提交事务时的数据。这就是脏读。事务 A 读到了事务 B 已修改,但尚未提交的数据
解决办法,Read committed!读提交,能解决脏读问题
不可重复读:程序员拿着工资卡(卡里当然是只有 3.6 万),当他买单时(程序员事务开启),收费系统事先检测到他的卡里有 3.6 万,就在这个时候!!程序员的妻子要把钱全部转出充当家用,并提交。当收费系统准备扣款时,再检测卡里的金额,发现已经没钱了。程序员就会很郁闷,明明卡里是有钱的…,一个事务范围内两个相同的查询却返回了不同数据,这就是不可重复读,事务 A 读取到了事务 B 已经提交的修改数据
解决办法,Repeatable read
重复读:程序员拿着工资卡(卡里还是有 3.6 万),当他买时(事务开启,不允许其他事务的 UPDATE 修改操作),收费系统事先检测到他的卡里有 3.6 万。这个时候他的妻子不能转出金额了。接下来收费系统就可以扣款了。
幻读:程序员某一天去消费,花了 2 千元,然后他的妻子去查看他今天的消费记录(妻子事务开启),看到确实是花了 2 千元,就在这个时候,程序员花了 1 万买了一部电脑,即新增 INSERT 了一条消费记录,并提交。当妻子打印程序员的消费记录清单时(妻子事务提交),发现花了 1.2 万元,似乎出现了幻觉,这就是幻读。
解决办法,Serializable 但是这种事务隔离级别效率低下,比较耗数据库性能,一般不使用。
对应关系
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交(read-uncommitted) | 是 | 是 | 是 |
不可重复读(read-committed) | 否 | 是 | 是 |
可重复读(repeatable-read) | 否 | 否 | 是 |
串行化(serializable) | 否 | 否 | 否 |
事务隔离级别
- Read uncommitted:就是一个事务可以读取另一个未提交事务的数据
- Read committed:一个事务要等另一个事务提交后才能读取数据
- Repeatable read:就是在开始读取数据(事务开启)时,不再允许修改操作
- Serializable:在该级别下,事务串行化顺序执行,可以避免脏读、不可重复读与幻读
查看隔离级别
1 | SELECT @@GLOBAL .transaction_isolation ,@@transaction_isolation; |
设置隔离级别
- 全局的
1 | SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED; |
- 当前会话
1 | SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; |
示例
- 演示行锁
5.5 以后,默认后,事务会自动提交,由于演示,开两个连接都关闭自动提交
1 | SET autocommit = 0; |
做以下操作,执行更新操作,自己可以查看到更新的内容,连接 2 看不到更新的内容,只有 commit
后,才能看到更新的内容,连接 1 执行更新操作,连接 2 也执行更新操作,更新同一条记录,连接 1 没有提交事务时,连接 2 更新处于阻塞状态,当 commit 时,连接 2 才会继续执行,连接 2 更新也要 commit,连接 1 和连接 2 同时更新数据,但更新的不是同一条记录,不会影响
索引失效行锁变表锁
使用 varchar 类型时,没有添加引号,导致索引失效,就会造成行锁变表锁,另一个连接更新数据时,会造成阻塞
间隙锁
当我们使用范围条件,而不是相等条件检索数据,并请求共享或排它锁时,InnoDB 会给符合条件的已有数据记录的索引项加锁,对于键值在条件范围内但并不存在的记录叫做 间隙
,InnoDB 也会对这个 间隙
加锁,这种锁机制,就是所谓的间隙锁
如何锁定一行数据
在查询之后添加 for update,其它操作会被阻塞,直到锁定的行提交 commit
1 | SHOW STATUS LIKE 'innodb_row_lock%'; |
悲观锁
就是很悲观,它对于数据被外界修改持保守态度,认为数据随时会修改,所以整个数据处理中需要将数据加锁。悲观锁一般都是依靠关系数据库提供的锁机制,事实上关系数据库中的行锁,表锁不论是读写锁都是悲观锁
乐观锁
顾名思义,就是很乐观,每次自己操作数据的时候认为没有人回来修改它,所以不去加锁,但是在更新的时候会去判断在此期间数据有没有被修改,需要用户自己去实现,不会发生并发抢占资源,只有在提交操作的时候检查是否违反数据完整性
为什么要使用乐观锁呢?
对于读操作远多于写操作的时候,大多数都是读取,这时候一个更新操作加锁会阻塞所有读取,降低了吞吐量。最后还要释放锁,锁是需要一些开销的,我们只要想办法解决极少量的更新操作的同步问题。换句话说,如果是读写比例差距不是非常大或者你的系统没有响应不及时,吞吐量瓶颈问题,那就不要去使用乐观锁,它增加了复杂度,也带来了额外的风险。
乐观锁的实现方式
版本号
就是给数据增加一个版本标识,在数据库上就是表中增加一个 version 字段,每次更新把这个字段加 1,读取数据的时候把 version 读出来,更新的时候比较 version,如果还是开始读取的 version 就可以更新了,如果现在的 version 比老的version 大,说明有其他事务更新了该数据,并增加了版本号,这时候得到一个无法更新的通知,用户自行根据这个通知来决定怎么处理,比如重新开始一遍。
时间戳
和版本号基本一样,只是通过时间戳来判断而已,注意时间戳要使用数据库服务器的时间戳不能是业务系统的时间,同样是在需要乐观锁控制的 table 中增加一个字段,名称无所谓,字段类型使用时间戳(timestamp), 和上面的 version 类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。