数据库锁(共享,独占,悲观,乐观),for update

数据库有两种锁,共享锁和独占锁(也叫排它锁)。当数据对象被加上独占锁时,其他的事务不能对它读取和修改。加了共享锁的数据对象可以被其他事务读取,但不能修改,只有表级的共享锁没有行级共享锁。数据库利用这两种基本的锁类型来对数据库的事务进行并发控制。

早期数据库就是利用这两种锁来进行一致性维护的,查询加上共享锁,insert,delete,update写操作加上独占锁,这样就保证了在我读取的时候,数据肯定不会被修改,在我写操作时,肯定不会有读取的操作读取到不合理的数据,但是这样明显读写相互抑制了,性能很差,所以后来数据库采用了各种锁,加上快照技术,等等,让读写分离,不在相互抑制,当然这实现非常的麻烦,而且容易出现很多不合理的结果,有可能会破坏数据库的一致性,并且如果直接交给用户操作,明显很烦,所以隔离性就是这样来的,就像一个api,封装了,告诉你,你把数据库处理机制设置哪个级别,可以达到什么效果,避免哪些不合理,用户自己决定。

数据库事务中可能出现的不合理:1.脏读,读取到其它事务没有提交的数据;2.不可重复读(不能读到相同的数据内容),在同一个事务中,读取两次同一数据,发现结果竟然不一样(因为数据被其它事务修改了);3.幻读,在同一个事务中,读取两次同一数据,发现结果竟然不一样(因为数据被其它事务新增或者删除了),比如统计数量,打印数据之类的;4.丢失更新
注意:不可重复读和幻读的区别是:前者是指读到了已经提交的事务的更改数据(修改或删除),后者是指读到了其他已经提交事务的新增数据。对于这两种问题解决采用不同的办法,防止读到更改数据,只需对操作的数据添加行级锁,防止操作中的数据发生变化;二防止读到新增数据,往往需要添加表级锁,将整张表锁定,防止新增数据(oracle采用多版本数据的方式实现)。
注意:丢失更新有两种,一种是一个回滚的事务覆盖另一个提交的事务;另一种是两个提交的事务,一个覆盖另一个
在默认的数据库第二隔离级别下,脏读和第一类丢失更新是可以避免的,现在谈下其它三种不合理。不可重复读,如果我们读取只是为了在页面显示,其实这种情况完全是可以不管的,感觉很合理啊,如果是为了更新,处理方法有两种版本控制或者select for update,但是其实这样的情况完全是我们强行想出来的业务逻辑,一般我们是可以去避免的;幻读,同理,看是为了更新,还是显示在页面而已,例外的是如果是在报表系统中,打印,就只能处理规避好这样的情况;第二类丢失更新,处理方法也是和上述一样的。

select语句,在早期的数据库,是上锁的,也就是共享锁,如果select一行,那么会对该行加上共享锁,其他select可以进来共享读取,但是修改就会阻塞等待该行上的所有共享锁都被去掉
这样是保证在读取的时候,不被修改干扰,但是,这样会限制并发性能,如果读并发很高
写操作很难抢到排它锁,因为写操作只能等待该行数据上的共享锁全部解除,它才可以进行写操作,并且,进行写操作,所有的共享读操作也会被阻塞,为了提高数据库并发的能力,现在的数据库,都提供了一个新的机制,叫做MVCC,多版本并发控制,每一个修改,都会在副本中进行修改,类似copyOnWrite的List或者Set,而原来的读不受影响,一旦修改提交,其他的事务就可以读到已提交之后的数据,也就是说,哪怕现在有很多个并发读事务作用于一行,修改的操作依然可以允许,同时,哪怕有一个修改事务还没提交,改行按理说,处于排它锁状态,但是select依然可以读取,因为,修改的时候,是在副本中修改,只要该事务没有提交,所有的修改,都只是在副本上进行的,并没有影响到实际的数据,这样,就大大提高了数据库的并发能力
再也不存在传统的排他共享限制了,几乎任何时候,都可以并发读,并发读写,但是,对于排它锁
如果两个写操作先后作用于一行,第二个写会被阻塞,他们不能同时进行修改,否则,就会出现丢失更新了

一个修改覆盖另外一个修改,排它锁,在这种情况下,是有效的,所以,现在的数据库,在任何时候,都可以并发读,写的时候,也不受读的影响,写和写还是会存在排他影响

select for update,这种select可不是简单的读取了,它必须要求我们在查询一行数据的时候,其他写事务,不能对该行数据进行操作,我们要保证,在该事务整个过程中,其他并发事务,都不能动这一行数据那就用select for update,将该行数据设置排它锁,相当于是一个update操作了并且select for update不会阻塞select,但是select for update会阻塞select for update也就是排它锁会阻塞排它锁,你去存钱,存了5000,你老婆也去存钱,存了5000你希望最后的结果是10000,如果说两个人同时对一个账户存钱,两个update操作同一个账号,一定是一个先,一个后,如果同时进行,必然有一个覆盖另一个好了;再说取钱,你老婆去取钱,你也去取钱,账户余额本来是5000的你们俩同时点了查询余额,嗯,没问题,俩人看到的都是5000,这个时候,你取了5000,你老婆也取了5000,如果说,你们取钱是基于该余额来进行后续操作,你们俩看到的都是5000就应该每个人都能取5000,上面两种情况,明显在实际中是不允许存在的。我们查询余额的目的是什么?,难道就是为了看看余额是多少吗并不是,查的这个余额,是我进行下一步取款操作的基础,那么,当我进行一个select for update我老婆还能select for update吗?,因为互斥了,排他,我老婆必须等待,等待我这个select for update锁定解除,她才可以进去,当她进去的时候,余额为0了,因为她这个select操作,是在我的事务提交之后进行查询的嗯,所以就不会再出现取两次钱的情况。但是,我在取款select for update,我老婆只是为了查询余额,它进行一个select操作,这是允许的她只是看一下,屏幕上显示而已,无伤大雅,哪怕我把余额减少到0了,如果在可重复读的select中我老婆第一次看到的是5000,第二次看到的还是5000,哪怕,你已经取走了,我老婆看到的也是5000不担心这种数据延时,因为他不是 for update,他只是看下而已,为了view显示,无所谓。

从另一种层次上来讲,select for update是一种悲观的态度,它认为在我读取数据然后更新的时候
因为并发的原因,肯定会造成丢失更新,上面的存取钱例子还可以用一种乐观的方式来检验并处理
给数据一个类似版本号的标志,在我查询的时候就把这个版本号一起查出来,
然后update时把这个版本号条件带进去,验证数据是否过期了,这样我们就可以防止丢失更新
这也就是悲观锁和乐观锁的概念。

优缺点明显:select for update(悲观锁),是直接把数据加上排它锁,只要这个锁没有释放,其它任何对同一
数据对象的写操作,都将被阻塞,在并发高的时候,这样很多用户的操作就会比较慢,对系统这是灾难
,但是不管怎样,用户的操作始终是会成功的,只是时间问题而已;版本号检验更新(乐观锁),这种在
我们select时,并没有加锁,所以其它的,所以在select到update的过程中,其它任何写操作都不会被阻塞,
系统反应给用户就会相对使用悲观锁快很多,但是问题也有,用户的请求是有可能因为并发数据更改后,update失败,假如这个丢失更新操作并发量很大,就会造成大批量的失败,比如100人请求,那么就只有1人成功,
其它99人全部失败,这个对用户的体验简直是灾难,而且很可能在高并发的情况下,同一个用户很多次请求都
会失败,完全看运气了。
所以,当高并发(不管是对同一数据对象的写事务还是上述的那种丢失更新的事务)时候,很明显使用悲观锁要好点,一般来说select后就是update了,我们假想的select之后喝水耽误时间的情况很少的,虽然慢了,但是始终能成功,乐观锁的大批量失败是我们绝对无法容忍的;当低并发时,用乐观锁比较好。

for update:给数据对象加上锁
     表锁: 
     线程1:select * from user for update; 
     线程2:update user set name=’123’ 堵塞 
                update user set name=’123’ where id = 1 堵塞 

                user表被锁定,其他进程无法修改表中数据,只能查询。

     行锁: 
     线程1:select * from user where id = 1 for update; 
     线程2:update user set name=’123’ 堵塞 
                update user set name=’123’ where id = 1 堵塞 
                update user set name=’123’ where id = 2 正常 
                user表中id=1数据被锁定,其他进程无法修改user该条数据

猜你喜欢

转载自blog.csdn.net/blossomfzq/article/details/80651217
今日推荐