锁的基本概念
当多事务争取一个资源时,有可能导致数据不一致,这个时候需要一种机制限制,并且将数据访问顺序化,用来保证数据库数据的一致性,锁就是其中的一种机制。我们可以用商场的试衣间来做个比喻,商场里得每个试衣间都可供多个消费者使用,因此可能出现多个消费者同时试衣服需要使用试衣间,这时候就产生冲突了,为了避免冲突,试衣间装了锁(其实就是进去之后把门拴住),某一个试衣服的人在试衣间里把锁锁住了,其他顾客就不能再从外面打开了,只能等待里面的顾客,试完衣服,从里面把锁打开,外面的人才能进去(网上找到的比喻,非常形象)。不过我想要是并发了就尴尬了,哈哈。
锁的基本类型
数据库上的操作可以归纳为两种:读和写。
多个事务同时读取一个对象的时候,是不会有冲突的。同时读和写,或者同时写才会产生冲突。因此为了提高数据库的并发性能,通常会定义两种锁:共享锁和排它锁。
共享锁(Shared Lock,也叫S锁)
共享锁(S)表示对数据进行读操作。因此多个事务可以同时为一个对象加共享锁。(如果试衣间的门还没被锁上,顾客都能够同时进去参观)
产生共享锁的sql:select * from ad_plan lock in share mode;
共享锁的使用场景
SELECT ... LOCK IN SHARE MODE走的是IS锁(意向共享锁),即在符合条件的rows上都加了共享锁,这样的话,其他人可以读取这些记录,也可以继续添加IS锁,但是无法修改这些记录直到你这个加锁的过程执行完成(完成的情况有:事务的提交,事务的回滚,否则直接锁等待超时)。
SELECT ... LOCK IN SHARE MODE的应用场景适合于两张表存在关系时的写操作,拿mysql官方文档的例子来说,一个表是child表,一个是parent表,假设child表的某一列child_id映射到parent表的c_child_id列,那么从业务角度讲,此时我直接insert一条child_id=100记录到child表是存在风险的,因为刚insert的时候可能在parent表里删除了这条c_child_id=100的记录,那么业务数据就存在不一致的风险。正确的方法是再插入时执行select * from parent where c_child_id=100 lock in share mode,锁定了parent表的这条记录,然后执行insert into child(child_id) values (100)就不会存在这种问题了。
排他锁(Exclusive Lock,也叫X锁)
排他锁也叫写锁(X)。
排他锁表示对数据进行写操作。如果一个事务对对象加了排他锁,其他事务就不能再给它加任何锁了。(某个顾客把试衣间从里面反锁了,其他顾客想要使用这个试衣间,就只有等待锁从里面给打开了)
产生排他锁的sql: select * from ad_plan for update;看到了吧,for update出现了,所以for update 是排他锁,涨知识了。
排他锁的使用场景:
使用场景一:订单的商品数量
但是如果是同一张表的应用场景,举个例子,电商系统中计算一种商品的剩余数量,在产生订单之前需要确认商品数量>=1,产生订单之后应该将商品数量减1。
1 select amount from product where product_name='XX';
2 update product set amount=amount-1 where product_name='XX';
显然1的做法是是有问题,因为如果1查询出amount为1,但是这时正好其他session也买了该商品并产生了订单,那么amount就变成了0,那么这时第二步再执行就有问题。那么采用lock in share mode可行吗,也是不合理的,因为两个session同时锁定该行记录时,这时两个session再update时必然会产生死锁导致事务回滚。以下是操作范例(按时间顺序)
使用场景一:数据表的状态
如果存在一张表记录一个商品的状态,在订单的变化过程中,订单的状态是不断变化的,而且变化的过程中肯定也会有并发的问题,而且很多时候与其他系统有交互,会存在补偿的情况,所以并发的可能性很大,补偿或者为了增加状态修改的成功可能性,2次改变状态的情况也有,楼主就遇到了这种情况,真操蛋。于是看到有这样的for update写法。
1 update order set status = 1 where product_id = '1';
2 insert order_flow (..............) value (.........)
这样的情况下就有可能订单的状态已经更新完成了,但是补偿这些额外的消息把状态又更新为待处理或者插入了多条流水的情况(多条流水的可能性大,状态的那种可能补偿滞后)。这个时候就可以使用select .... from order where order_id = '1' for update,先锁住要修改状态的表,这样就不会别人操作了,自己先后面把流水插入,然后更新状态,完美。但是加了锁之后性能就很慢了,担心性能影响,而且有可能存在死锁的情况,后面我就修改为流水表中增加一个唯一索引,这样插入流水报错就是已经处理过的记录了。这样就不会存在性能问题。
通过对比,lock in share mode适用于两张表存在业务关系时的一致性要求,for update适用于操作同一张表时的一致性要求。