面试题:如何用数据库去实现一个乐观锁/悲观锁?(锁分类,死锁)
本文意在解析锁的概念,不只是解析题目,而是将锁弄清楚,知道大概的实现方案。
数据库锁的分类
数据库锁的分类有很多种。
分类1
从数据库系统角度分为三种:排他锁X、共享锁S、更新锁U。
排他锁(X锁):该锁也称为独占锁,用于数据修改操作,例如 INSERT、UPDATE 或 DELETE。确保不会同时同一资源进行多重更新。如果事务T对数据A加上排他锁后,则其他事务不能再对A加任何类型的锁。获准排他锁的事务既能读数据,又能修改数据。
共享锁(S锁):又称读锁,若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A(避免脏读),其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。
更新锁(U锁):也是一种读锁,但是是为了防止X与S的死锁,不允许S锁升级为X锁,考虑下面的事务执行情况:
T1 | T2 |
---|---|
read(A) | |
read(A) | |
write(A) | |
write(A) |
由于T1,T2都先后对数据A加了共享锁S,然后都想要写数据,就要把S锁升级为X锁,但是数据上有其他的事务加的S锁与X锁互斥无法升级,两事务都死锁等待对方释放S锁。
这里插句题外话:死锁的四个条件:
1.互斥条件,一个资源每次只能被一个进程使用(T1,T2两个事务);(在题外话:一般一个数据库连接是一个线程,这个连接上可以有多个事务,一般而言,一个线程执行一个事务)
2.请求与保持条件,一个进程因请求资源而阻塞时,对已获得的资源保持不放(不放弃S锁);
3.不剥夺条件,进程已获得的资源,在末使用完之前,不能强行剥夺(没有一方放弃资源A);
4.循环等待条件(于是死锁)
于是添加了U锁,定义S锁不能升级为X锁,只有U锁能升级为X锁。有S锁可以添加U锁,U锁不能添加任何锁。(不对称)
X | S | U | |
---|---|---|---|
X | x | x | x |
S | x | √ | √ |
U | x | x | x |
如此,两个进程若一开始都选择了S锁,后面若想升级为X锁,则必须先将自己的S锁放开(因为无法直接升级),那此时资源就被放开,后面谁能先加上X锁就看自己的速度了;若T1加的是U锁,那显然T2由于互斥不能再加S锁,智能等待写完之后再执行,此时两事务串行;若T1加S锁,T2加U锁,则T2可以升级X写数据,然后才能执行T1事务。(至于到底会加哪一个锁,我猜测数据库引擎会对事务整体语句考虑,后面有write会决定加U锁)。更新锁(U锁)并不能保证不会产生死锁,只是针对共享锁和排他锁提出了一种较为简单的解决方式。
分类2
从锁定的粒度分为全局锁、表级锁和行锁。其中,全局锁是锁定数据库中的所有表,表级锁是每次操作锁住整张表,行锁是每次操作锁住对应的行数据。不同的锁定粒度和类型,适用于不同的场景,需要根据实际情况进行选择。如MySQL的InnoDB引擎使用的是粒度较小的行级锁,而Oracle使用的是表级锁。
而悲观锁,乐观锁是数据库锁的实现方式,并不是锁本身。是解决并行访问数据加锁的不同方式,悲观锁是先认定会冲突,每次访问数据都要加锁,而乐观锁认为不会冲突,访问数据不加锁,事后再检查有没有其他事务访问该数据。
如何实现乐观锁
乐观锁是一种并发控制机制,它通过在数据表中添加一个版本号或时间戳字段,来实现对数据的控制。在数据库中,一种方式是在需要乐观锁控制的表中增加一个版本号字段,每次更新数据时,将版本号加1,同时在更新时判断版本号是否与当前版本号相同,如果相同则更新成功,否则更新失败。另一种方式是使用数据版本(Version)记录机制实现,即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的“version”字段来实现。当读取数据时,将版本号一同读出,之后更新数据时,判断当前版本号是否与读出的版本号相同,如果相同则更新成功,否则更新失败。我的理解就是一个是表级别的乐观锁,一个是行级别的乐观锁。
如何实现悲观锁
数据库中默认的加锁机制就是悲观锁。 悲观锁是一种悲观的加锁机制,它在修改数据之前会把数据锁住,以防止其他事务对数据的修改。在MySQL中,可以通过使用select… for update语句来实现悲观锁,该语句会锁定查询到的数据,以防止其他事务对数据的修改。