首先,我们要理解,在mysql里面主要为两种,一种是概念上的区分,另一种是实际运用效果不同的区分。
概念区分的锁
- 乐观锁
- 悲观锁
实际运用的锁
- 行锁
- 表锁
指事务某个状态的锁
- 死锁
锁的介绍
一、表锁
表锁分为两种类型:
(1)、表读锁
lock tables db.goods read; //给数据库 db 的 goods表上读锁
当给这个表上读锁的时候,其他线程请求对这个表的操作只能读,不能写
(2)、表写锁
lock tables db.goods write; //给数据库 db 的 goods表上写锁
当给这个表上写锁的时候,其他线程请求对这个表的操作会阻塞,不能读和写
解锁
unlock tables;
总结
在实际开发场景其实很少用到,毕竟锁住整个表对业务场景来说基本都是痛鸡
.
.
.
二、行锁
行锁就是说,一个线程update表里一条数据,当其他线程请求这条数据时,是不能操作这条数据的,只有等第一个线程结束了才能去操作,即第一个线程独享这条数据的操作权限
举个栗子,查询商品的库存,在sql语句后面加 for update ,给商品表 id = 1 的记录上锁
select quantity from goods where id =1 for update;
扩展思考
值得注意的是,被扫描的行也会上锁,所以使用锁的时候必须走索引,指定一条数据上锁,否则全表扫会导致其它行甚至整个表上锁!
课外提示
(1)事务上锁的时机:
使用mysql的事务机制,当执行到update等更新数据库的操作语句时候,才会为当前的操作行上锁,直到事务释放为止。
.
.
三、死锁
死锁是多个事务出现互相等待对方释放资源的状态
举个栗子:
事务A和事务B请求同一个表
事务A ,需要请求id=1和 id=2 的行资源,
事务B, 需要请求 id=2 和 id=1的行资源。
就是并发场景下,A事务给id=1的行上锁了想拿id=2的行锁,发现B事务已经拿了id=2的锁,就在阻塞等待事务B释放锁;同时,事务B在拿了id=2的锁后,想拿id=1的锁,却发现了A事务拿了id=1的锁,也阻塞等待事务A释放锁,就造成了两个事务傻傻的互相等待对方释放资源
怎么解决呢?
(1)、事务的顺序合理性
什么是合理的顺序呢?就以上面例子,出现死锁的原因其实两个加锁的方向反了才导致,可以调整一下事务的顺序,例如
大家都从id=1 的行去一起开始处理,就大概率不会出现上面的情况
(2)、设置死锁时间
在innodb里,默认死锁时间是50S,可以通过innodb_lock_wait_timeout命令设置合理的时间
(3)、避免长事务
一般使用事务的时候,如非必要尽量不要使用长事务。如果你非要用长事务,我也很无奈(只能采用其他手段尽量避免避免并发事务,如redis、消息队列、异步等等方案)
.
.
.
四、悲观锁
理解
对一条数据操作时,持悲观态度,觉得数据可能产生冲突,所以利用mysql的锁机制上锁。所以,上面提到的事务和行锁都是悲观锁的一种实现
五、乐观锁
理解
与之相反乐观锁,就是操作数据的时候持乐观态度,觉得数据不会冲突,只在代码的逻辑层做限制,一旦出现冲突,就返回提示取消执行
那么代码怎么做限制呢?
乐观锁的实现一般需要借助字段来做版本控制
(1)举个栗子🌰 ,扣减库存
//获取商品id为1 的库存
$data = select quantity,update_time from goods where id =1;
//增加用户id=1的订单
insert into order(goodsid,userid) values(1 ,1);
//修改商品库存
update goods quantity=quantity-1 ,data=time() where id=1 and update_time = $data['time'];
从上面可以看出,使用update_time字段作为版本变更的依据,查出库存 => 比较时间戳(或版本) => 存入库存,在执行第三条sql语句的时候,如果 update_time字段发生了变化,语句就会返回执行失败,然后返回提示给 id=1的用户。
当然不是一定是使用date_time字段,也有很多人是用version作用控制版本的字段,每一次操作就+1
总结
其实从上面的例子,不难看出乐观锁在大并发的场景,用户大概率经常返回下单失败的提示,这样对用户的体验是很严重的打击的,而个人方案的建议是用redis缓存中间减少mysql的并发,在redis中操作缓存扣减库存,在用异步队列把库存扣减操作到mysql中,实现持久化。