在数据库当中,数据也是一种供许多用户共享访问的资源。如何保证数据并发访问的一致性、有效性,是所有数据库必须解决的一个问题,锁的冲突也是影响数据库并发访问性能的一个重要因素。从这一角度来说,锁对于数据库而言就显得尤为重要
场景描述
- 在做电商业务,我们会经常碰到这么一种情况。某个sku库存仅剩1,有10个用户在同时浏览,用户看到的库存都是1。然后这10个用户同时下单,如果下单业务系统这个地方没有并发处理,就会发生超卖情况。当然,实际的业务系统中,肯定是会做并发处理的。常见的处理方式也就是对库存加锁。
存储引擎和锁模式的关系
存储引擎 | 表锁 | 行锁 | 页锁 |
---|---|---|---|
MyISAM | 支持 | 不支持 | 不支持 |
BDB | 支持 | 支持 | 不支持 |
InnoDB | 支持 | 支持 | 不支持 |
悲观锁
- 悲观锁,是指对数据被外界修改持有保守态度。在数据处理的过程中,将数据锁起来。依靠数据库提供的锁机制来实现
- 使用悲观锁的时候,要关闭mysql的自动提交属性。set autocommit=0
- select for update,才会收到影响。select操作不会受到影响
- 根据加锁对象,可以分为行锁和表锁
- 根据加锁机制,可以分为共享锁和排他锁
行锁
- 概念:给单独的一条记录加锁。
- 特点:支持事务,开销大,加锁慢。会出现死锁。锁的粒度小,并发情况下,产生锁等待的几率较小,可支持较高的并发数。
- 查看行锁的命令:show status like ‘innodb_row_lock%’
- 场景:事务提交前,该行数据被其他sql更新
实现原理
- 在 Mysql 中,行级锁并不是直接锁记录,而是锁索引。索引分为主键索引和非主键索引两种,如果一条sql 语句操作了主键索引,Mysql 就会锁定这条主键索引;如果一条语句操作了非主键索引,MySQL会先锁定该非主键索引,再锁定相关的主键索引
- InnoDB 行锁是通过给索引项加锁实现的,如果没有索引,InnoDB 会通过隐藏的聚簇索引来对记录加锁。也就是说:如果不通过索引条件检索数据,那么InnoDB将对表中所有数据加锁,实际效果跟表锁一样。因为没有了索引,找到某一条记录就得扫描全表,要扫描全表,就得锁定表。
表锁
- 概念:整个表数据加锁
- 特点:开销小,加锁快。不会出现死锁。锁的颗粒度大,并发低。
- 查看锁表的命令:show open tables where In_use > 0;
- 场景:DDL操作会触发表锁(数据库或表的结构操作,例如创建、删除、修改、库或表结构),在DDL操作执行完成之前,对该表进行DML或者DDL或者DQL操作,均会进入等待状态。
//开始事务
begin;begin work;start transaction; (三者均可)
//查询出商品信息
select status from t_goods where id=1 for update; //触发行锁
//根据商品信息生成订单
insert into t_orders (id,goods_id) values (null,1);
//修改商品status为2
update t_goods set status=2;
//提交事务
commit;commit work;(两者均可) //释放锁
共享锁(S锁)
- 概念:共享锁:shared lock, 也成为读锁(read lock)
- 功能:若事务T对数据对象A加上S锁,则事务T可以读但是不能修改A。其他事务,可以对A加S锁,但是不能加X锁。直到事务T释放A上的S锁。这保证了其他事务可以读A,但是在T提交之前,其他事务不能修改A。
- 示例:
//对数据对象A加上S锁
start transaction;
select * from yitiao_user_coupon where id = 90 LOCK IN SHARE MODE; //commit之后,就会释放S锁
//在看一个事务,去更新数据对象A
start transaction;
update yitiao_user_coupon set coupon_id = 9 where id = 90; //等待锁
//查看锁sql
select * from information_schema.processlist where COMMAND = 'Query' and db = 'xxx';
//杀死锁
kill id
排他锁(X锁)
- 概念:排他锁:exclusive lock,也叫写锁(wirte lock)
- 功能:若事务T对数据对象A加上X锁,则事务T可以读和修改A。其他事务,不可以对A加任何锁。直到事务T提交,释放A上的X锁。这保证了其他事务在T释放A上的锁之前不能再读取和修改A。
- 示例:
//对数据对象A加上S锁
start transaction;
select * from yitiao_user_coupon where id = 90 for update; //commit之后,就会释放S锁
//在看一个事务,去更新数据对象A
start transaction;
select * from yitiao_user_coupon where id = 90 LOCK IN SHARE MODE; //等待锁
//查看锁sql
select * from information_schema.processlist where COMMAND = 'Query' and db = 'xxx';
//杀死锁
kill id
乐观锁
- 乐观锁一般认为数据不会冲突,在更新的时候,才会去检查数据是否被修改过。如果修改过,则返回错误信息,让用户去决定怎么做。
实现原理
-
数据版本记录实现:这是乐观锁最常用的一种实现方式。何谓数据版本?即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。
-
乐观锁定的第二种实现方式和第一种差不多,同样是在需要乐观锁控制的table中增加一个字段,名称无所谓,字段类型使用时间戳(timestamp), 和上面的version类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。