在看这篇博客以前,我希望你对数据库悲观锁和乐观锁有一个基本的概念,对于这两个概念还比较陌生的小伙伴,可以查看我另一篇博客。希望对你有帮助!
浅谈InnoDB存储引擎下锁的分类无锁、偏向锁、轻量级锁、重量级锁,完整的锁升级!
synchronized锁升级包含了Java实现的乐观锁和悲观锁,建议学习。
笔者以为,锁细化出的悲观锁和乐观锁,用于在并发场景下在,在并发量和数据安全上的一个折中考虑。根据实际的业务场景,根据锁的不同特性选择不同的处理方式,往往能取得事半功倍的效果。
文章目录
- 1、乐观锁部分
- 2、乐观锁+自旋锁部分
悲观锁是事前锁,可以联想Java的synchronized锁,以及数据库的行锁表锁。如果我们的事务过多,每一个事务都会占据一个连接,过多的连接必然耗费数据库相当多的资源,不妥。再则锁定的时间过久,行锁的排他性,势必会影响需要修改该行数据的其他事务阻塞,不妥。
我们的乐观锁就很好的解决了这些问题
1、乐观锁部分
测试案例
数据库表
CREATE TABLE OPT_LOCK_STU(
ID INT, -- id号
NAME VARCHAR(12), -- 姓名
AGE VARCHAR(5), -- 年龄
VERID LONG, -- 版本号(乐观锁实现关键)
PRIMARY KEY(id)
);
INSERT INTO OPT_LOCK_STU VALUES (1,'默辨','12',1);
INSERT INTO OPT_LOCK_STU VALUES (2,'mobian','7',1);
INSERT INTO OPT_LOCK_STU VALUES (3,'pan','13',1);
COMMIT;
对应的实体类
//此注解是因为我本地使用了Lombok插件
@Data
@AllArgsConstructor
@NoArgsConstructor
public class OptLockStu {
private int id ;
private String name;
private String age;
private long verId;
}
测试方法
@Test
//数据库乐观锁测试用例:
public void optLockTest() {
System.out.println("测试用例开始...");
//1.查询出我们希望修改的值,希望代码简单,这里直接去数据库里面查询数据
OptLockStu optLockStu = optLockMapper.queryStu(1);
//2.对数据进行修改后再更新
optLockStu.setAge("22");
optLockStu.setName("mobian123");
//3.数据更新成功,返回true
flag = optLockMapper.updateStu(optLockStu);
//数据更新失败的提示处理
if(!flag) {
//抛出对应的异常
//throw new Exception()
}
System.out.println("测试用例结束...");
}
mapper接口
public interface OptLockMapper {
//更新数据
boolean updateStu(OptLockStu optLockStu);
//根据id查询对用的数据
OptLockStu queryStu(int id);
}
对应的mapper.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="pers.mobian.mapper.OptLockMapper">
<resultMap id="StuPojo" type="pers.mobian.entity.OptLockStu">
</resultMap>
<update id="updateStu" parameterType="pers.mobian.entity.OptLockStu">
UPDATE OPT_LOCK_STU SET
NAME = #{name},AGE = #{age},VERID = #{verId}+1
where ID = #{id} AND VERID = #{verId}
</update>
<select id="queryStu" resultMap="StuPojo" parameterType="int">
SELECT * FROM OPT_LOCK_STU WHERE ID = #{id}
</select>
</mapper>
重点SQL
UPDATE OPT_LOCK_STU SET
NAME = #{name},AGE = #{age},VERID = #{verId}+1
where ID = #{id} AND VERID = #{verId}
总结:
乐观锁与悲观锁的明显的区别就是加锁的时间,一个是在处理数据之前,比如数据库的行锁、表锁,Java的synchronized;一个在处理数据之后,比如这里的处理方式。
我们获取到对应数据时,我们怀乐观态度,我们认为别人不会修改我们的数据,所以数据大家共享,想怎么处理怎么处理。而我们只是会在真正处理数据后,才进行一个校验,比如我们这里在where条件后面使用了一个版本号verId来处理校验。
当我们的数据每发生一次修改,对应的版本号就会发生变化。第一个人先执行了这个SQL,那么对应的版本号verId就会发生变化(我们这里是+1)。当第二个人再执行这个SQL时,由于数据是来自SQL第一次执行前查询到的,所以对应的verId是来自没有+1操作前。当第二个人去执行SQL时,会因为verId匹配不上(第一个人+1后改变了verId的值),无法执行更新操作,返回一个false。即此次更新失败。
以上,就是乐观锁的实现原理。
补充:也可以使用时间戳来替换版本号,这也是最常见的两种处理方式
但我们会发现一个问题,那第二个人处理的数据怎么办?作废了?
于是我们引入了接下来的处理
2、乐观锁+自旋锁部分
我能想到的是结合Java的自旋锁进行。Java的CAS是典型的自旋锁,这里只抽取它的一个简单概念进行演示。
@Test
//数据库乐观锁+自旋锁测试用例:
public void optLockTest() {
System.out.println("测试用例开始...");
//1.定义一个状态位
boolean flag = false;
while (!flag) {
//2.查询出我们希望修改的值,希望代码简单,这里直接去数据库里面查询数据
OptLockStu optLockStu = optLockMapper.queryStu(1);
//3.对数据进行修改后再更新
optLockStu.setAge("22");
optLockStu.setName("mobian123");
//4.数据更新成功,返回true跳出循环。更新失败返回false,循环继续
flag = optLockMapper.updateStu(optLockStu);
if(!flag) {
//打印相关的提示语句
}
}
System.out.println("测试用例结束...");
}
我以为,这里理清楚这4点即可:
- 引入一个while循环,用来强迫我们的SQL一定要执行成功了才能出来
- 如果数据更新失败(不仅仅是因为verId的问题),那么我们就会重新查一遍数据,再次更新数据
- 使用一个while循环只是CAS锁的一个思想体现,理解这个概念就好。精力不足的没必要了解什么是CAS锁
- 这只是我能想到的一种实现方式,具体的实现种类有很多
综上:
我们就能够避免让其他人的数据浪费,继而更好的达到更新数据的效果
该部分的知识一定要和MySQL的隔离级别以及真实的场景联系起来理解,不然你很难想象到这个锁的优势以及作用