目录
数据库锁的种类一般分为两种:一种是悲观锁,一种乐观锁。
悲观锁
悲观锁(Pessimistic Lock)具有强烈的独占和排他特性,它指的是对数据被外界修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制。传统的关系数据库里用到了很多这种锁机制,比如行锁、表锁、读锁、写锁等,都是在操作之前先上锁。
悲观锁的隔离级别可以看做可重复读。
悲观锁按使用性质分类
共享锁(读锁、S锁)
事务A对对象T加共享锁,其他事务也只能对T加共享锁,多个事务可以同时读,但不能有写操作,直到A释放共享锁。
特点:多个事务可封锁同一个共享页;任何事务都不能修改该页;该页被读取完毕,共享锁立即被释放。
互斥锁(排它锁、独占锁、写锁、X锁)
事务A对对象T加互斥锁以后,其他事务不能对T加任何锁(即其他事物进入阻塞状态),只有事务A可以读写对象T直到A释放互斥锁。
特点:仅允许一个事务封锁此页;其他任何事务必须等到互斥锁被释放才能对该页进行访问;互斥锁一直到事务结束才能被释放。
更新锁(U锁)
更新锁用来预定要对此对象施加互斥锁,它允许其他事务读,但不允许再施加更新锁或互斥锁,当被读取的对象将要被更新时,则升级为互斥锁。
更新锁主要是用来防止死锁的,因为使用共享锁时,修改数据的操作分为两步,首先获得一个共享锁,读取数据,然后将共享锁升级为互斥锁,然后再执行修改操作。这样如果同时有两个或多个事务同时对一个对象申请了共享锁,在修改数据的时候,这些事务都要将共享锁升级为互斥锁。这些事务都不会释放共享锁而是一直等待对方释放,这样就造成了死锁。如果一个数据在修改前直接申请为更新锁,在数据修改的时候再升级为互斥锁,就可以避免死锁。
特点:用来预定要对此页施加互斥锁,它允许其他事务读,但不允许再施加更新锁或互斥锁;当被读取的页要被更新时,则升级为互斥锁。更新锁一直到事务结束时才能被释放。
自旋锁
自旋锁和互斥锁很像,一次只能有一个进程进入临界区,唯一不同的是自旋锁访问加锁资源时,会一直循环的查看是否释放锁。这样要比互斥锁效率高很多,但是仍然需要占用CPU。
特点:自旋锁需要设定一个自旋等待的最大时间,如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。除此之外,当自旋锁递归调用的时候会造成死锁现象,所以慎重使用自旋锁。
悲观锁按作用范围分类(按锁的粒度分类)
数据库能够确定那些行需要锁的情况下使用行锁,如果不知道会影响哪些行的时候就会使用表锁。
举个例子,一个用户表user,有主键id和用户生日birthday。当你使用update … where id=?这样的语句时,数据库明确知道会影响哪一行,它就会使用行锁;当你使用update … where birthday=?这样的的语句时,因为事先不知道会影响哪些行就可能会使用表锁。
行锁
行锁的作用范围是行级别,数据库能够确定那些行需要锁的情况下使用行锁,如果不知道会影响哪些行的时候就会使用表锁。举个例子,一个用户表user,有主键id和用户生日birthday当你使用update … where id=?这样的语句数据库明确知道会影响哪一行,它就会使用行锁,当你使用update … where birthday=?这样的的语句的时候因为事先不知道会影响哪些行就可能会使用表锁。
表锁
表锁的作用范围是整张表。
乐观锁
相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制,大多是基于版本号( Version )记录机制实现,而不需要借助数据库的锁机制。
乐观锁的实现
乐观锁的本质不是锁,其隔离级别可以看作为读未提交。乐观锁可以通过以下几种方式实现:
版本号
给数据增加一个版本标识,在数据库上就是表中增加一个version字段,每次更新把这个字段加1。读取数据的时候把version读出来,更新的时候比较一开始读取的version和现在的version,如果还是开始读取的version就可以更新了,如果现在的version比老的version大,说明有其他事务更新了该数据,并增加了版本号,这时候得到一个无法更新的通知,用户自行根据这个通知来决定怎么处理,比如重新开始一遍。
基于版本号实现乐观锁的关键是判断version和更新两个动作需要作为一个原子单元执行,否则在你判断可以更新以后正式更新之前有别的事务修改了version,这个时候你再去更新就可能会覆盖前一个事务做的更新。所以你可以使用update … where … and version=”old version”
这样的语句,根据返回结果是0还是非0来得到通知,如果是0说明更新没有成功,因为version被改了,如果返回非0说明更新成功。
待更新字段
基于待更新字段实现乐观锁的方式和版本号方式相似,只是不增加额外字段,直接使用有效数据字段做版本控制信息,因为有时候我们可能无法改变旧系统的数据库表结构。假设有个待更新字段叫count,先去读取这个count,更新的时候去比较数据库中count的值是不是我期望的值(即开始读的值),如果是就把我修改的count的值更新到该字段,否则更新失败。
悲观锁的优缺点
悲观锁的优点是能避免冲突的发生。
悲观锁的缺点是开销较大,而且加锁时间较长,对于并发的访问性支持不好。
乐观锁的优缺点
乐观锁的优点是避免了长事务中的数据库加锁解锁开销,大大提升了大并发量下的系统整体性能表现。
乐观锁的缺点是只能在提交数据时才发现业务事务将要失败,如果系统的冲突非常的多,而且一旦冲突就要因为重新计算提交而造成较大的代价的话,乐观锁也会带来很大的问题。
乐观锁与悲观锁的选择
乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。
悲观锁适用于多写的场景,这种情况下一般会经常产生冲突。
行锁的实现方式
InnoDB 行锁是通过给索引上的索引项加锁来实现的。这种行锁意味着只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁。