Mysql乐观锁和悲观锁
前言
https://blog.csdn.net/xz0125pr/article/details/51698507
https://www.cnblogs.com/laoyeye/p/8097684.html
https://www.cnblogs.com/boblogsbo/p/5602122.html
https://blog.csdn.net/top_code/article/details/56842746
不使用锁会产生的问题
1.1、 丢失更新:
一个事务的更新覆盖了其它事务的更新结果,就是所谓的更新丢失。例如:用户A把值从6改为2,用户B把值从2改为6,则用户A丢失了他的更新。
1、2、 脏读:
当一个事务读取其它完成一半事务的记录时,就会发生脏读取。例如:用户A,B看到的值都是6,用户B把值改为2,用户A读到的值仍为6。
1、悲观锁(先关闭数据库自动提交功能)
悲观锁(Pessimistic Lock),顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。
悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。
1.1、悲观锁例子
Java synchronized 就属于悲观锁的一种实现,每次线程要修改数据时都先获得锁,保证同一时刻只有一个线程能操作数据,其他线程则会被block。
1.2、注意事项:
1.2.1、行锁和表锁
MySQL InnoDB默认Row-Level Lock,
只有「明确」地指定主键,MySQL 才会执行Row lock (只锁住被选取的数据)
否则MySQL 将会执行Table Lock (将整个数据表给锁住)。
1.2.2、索引和上面的主键是一致的
使用索引也会影响数据库的锁定级别,只要是明确指定的索引,也是只会锁住被选取的数据,否则就会将整个数据表锁住
1、3、共享锁和排它锁
共享锁又称为读锁,简称S锁,顾名思义,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。update,insert,delete语句会自动加排它锁的原因
排他锁又称为写锁,简称X锁,顾名思义,排他锁就是不能与其他所并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据就行读取和修改。
上面两种都可以直接通过select …from…查询数据,因为普通查询没有任何锁机制
1.3.1、共享锁
SELECT * from city where id = "1" lock in share mode;
然后在另一个查询窗口中,对id为1的数据进行更新
update city set name="666" where id ="1";
此时,操作界面进入了卡顿状态,过几秒后,也提示错误信息
[SQL]update city set name="666" where id ="1";
[Err] 1205 - Lock wait timeout exceeded; try restarting transaction
那么证明,对于id=1的记录加锁成功了,在上一条记录还没有commit之前,这条id=1的记录被锁住了,只有在上一个事务释放掉锁后才能进行操作,或用共享锁才能对此数据进行操作。
update city set name="666" where id ="1" lock in share mode;
[Err] 1064 - You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'lock in share mode' at line 1
加上共享锁后,也提示错误信息了,通过查询资料才知道,对于update,insert,delete语句会自动加排它锁的原因
于是,我又试了试SELECT * from city where id = "1" lock in share mode;这下成功了。
1.3.2、排它锁
排它锁与共享锁相对应,就是指对于多个不同的事务,对同一个资源只能有一把锁。
与共享锁类型,在需要执行的语句后面加上for update就可以了
1、3、使用方法
SELECT … LOCK IN SHARE MODE
SELECT … FOR UPDATE
1.3、缺点
因为悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。如果加锁的时间过长,其他用户长时间无法访问,影响了程序的并发访问性,同时这样对数据库性能开销影响也很大,特别是对长事务而言,这样的开销往往无法承受。所以与悲观锁相对的,我们有了乐观锁。
2、乐观锁
乐观锁(Optimistic Lock),顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在提交更新的时候会判断一下在此期间别人有没有去更新这个数据。
使用场景:
乐观锁适用于读多写少的应用场景,这样可以提高吞吐量。
乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。
2.1、乐观锁一般来说有以下2种方式:
2.1. 使用数据版本(Version)记录机制实现
这是乐观锁最常用的一种实现方式。何谓数据版本?即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。
2.2. 使用时间戳(timestamp)。
乐观锁定的第二种实现方式和第一种差不多,同样是在需要乐观锁控制的table中增加一个字段,名称无所谓,字段类型使用时间戳(timestamp), 和上面的version类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。
2.2、乐观锁例子
Java JUC中的atomic包就是乐观锁的一种实现,AtomicInteger 通过CAS(Compare And Set)操作实现线程安全的自增。
2.3、乐观锁举例
银行两操作员同时操作同一账户。
比如A、B操作员同时读取一余额为1000元的账户,A操作员为该账户增加100元,B操作员同时为该账户扣除50元,A先提交,B后提交。最后实际账户余额为1000-50=950元,但本该为1000+100-50=1050。这就是典型的并发问题。
乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本(Version)记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。
读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
对于上面修改用户帐户信息的例子而言,假设数据库中帐户信息表中有一个version字段,当前值为1;而当前帐户余额字段(balance)为1000元。假设操作员A先更新完,操作员B后更新。
a、操作员A此时将其读出(version=1),并从其帐户余额中增加100(1000+100=1100)。<br/>
b、在操作员A操作的过程中,操作员B也读入此用户信息(version=1),并从其帐户余额中扣除50(1000-50=950)。
c、操作员A完成了修改工作,将数据版本号加一(version=2),连同帐户增加后余额(balance=1100),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,数据库记录version更新为2。<br/>
d、操作员B完成了操作,也将版本号加一(version=2)试图向数据库提交数据(balance=950),但此时比对数据库记录版本时发现,操作员B提交的数据版本号为2,数据库记录当前版本也为2,不满足 “提交版本必须大于记录当前版本才能执行更新 “的乐观锁策略,因此,操作员B的提交被驳回。<br/>
这样,就避免了操作员B用基于version=1的旧数据修改的结果覆盖操作员A的操作结果的可能。
2.4、举例测试(和上面的例子不一样)
2.4.1、建表语句
CREATE TABLE `person` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`pwd` varchar(255) DEFAULT NULL,
version bigint(20) default 0.
PRIMARY KEY (`id`)
)
2.4.2、repository
import com.hlj.springboot.dome.common.bean.Person;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Transactional
public interface PersonRepository extends CrudRepository<Person,Long> {
List<Person> findAll();
@Modifying
@Query(
value = "UPDATE person p set p.name = ?1 ,p.version = p.version + 1 WHERE p .id = ?2 and p.version = ?3",nativeQuery = true
)
int updateLockTest(String name,Long id ,Long version);
}
2.4.3、controller测试
}
//http://localhost:8080/updateLockTest?name=healerjean&id=1&version=5
@GetMapping("updateLockTest")
@ResponseBody
public ResponseBean updateLockTest( String name,Long id ,Long version){
try {
for(int i = 1; i<30 ;i++ ){
new Thread( ()->{
int m = personRepository.updateLockTest(name,id,version);
System.out.println(m);
}).start();
}
return ResponseBean.buildSuccess();
}catch (Exception e){
return ResponseBean.buildFailure(e.getMessage());
}
}
如果满意,请打赏博主任意金额,感兴趣的在微信转账的时候,添加博主微信哦, 请下方留言吧。可与博主自由讨论哦
支付包 | 微信 | 微信公众号 |
---|---|---|