乐观锁/悲观锁学习笔记,如有问题,欢迎指正
乐观锁
- 认为读多写少,写操作的并发可能性低,取数据的时候认为不会修改,故不会加锁。更新数据时会判断数据是否被其它线程修改,如果被其它线程修改,则不进行更新;如果没有被其它线程修改,则进行更新。
- 适用于读多写少的场景,提高系统吞吐量。
- 使用场景:
- 原子类(如:AtomicLong)使用的 CAS
- 版本号机制
在数据库表中加 version 字段,取数据时取出 version,更新时需要对比 version 是否一致。 大致sql如下:
update t_table set status = #{status}, version = version + 1 where id = #{id} and version = #{version}
示例
启用10个线程更新表中同一条数据
@Test
public void optimisticLock() throws InterruptedException {
for (int i = 0; i < 10; i++) {
Thread myThread = new Thread(new MyThread());
myThread.setName("Thread" + i);
myThread.start();
}
// 等待线程执行完并在控制台输出结果
Thread.sleep(5000);
}
class MyThread implements Runnable {
@Override
public void run() {
MemberDO memberDO = memberDOMapper.selectByPrimaryKey(1L);
log.info(Thread.currentThread().getName() + " 获取到的version:{}", memberDO.getVersion());
memberDO.setIsDelete((byte) 2);
int re = memberDOMapper.updateWithVersion(memberDO);
if (re > 0) {
log.error(Thread.currentThread().getName() + " 更新成功");
} else {
// todo 自行重试或其它异常处理机制
log.error(Thread.currentThread().getName() + " 更新失败");
}
}
}
mapper里的 updateWithVersion 的写法是:
update
t_member
set
is_delete = #{isDelete}, version = version + 1
where
id = #{id}" and version = #{version}")
观察控制台输出:
2021-04-14 10:59:38.158 INFO 8840 --- [ Thread0 ] tech.spring.SpringBootBaseTest : Thread0 获取到的version:1
2021-04-14 10:59:38.158 INFO 8840 --- [ Thread6 ] tech.spring.SpringBootBaseTest : Thread6 获取到的version:1
2021-04-14 10:59:38.158 INFO 8840 --- [ Thread5 ] tech.spring.SpringBootBaseTest : Thread5 获取到的version:1
2021-04-14 10:59:38.158 INFO 8840 --- [ Thread9 ] tech.spring.SpringBootBaseTest : Thread9 获取到的version:1
2021-04-14 10:59:38.158 INFO 8840 --- [ Thread8 ] tech.spring.SpringBootBaseTest : Thread8 获取到的version:1
2021-04-14 10:59:38.158 INFO 8840 --- [ Thread2 ] tech.spring.SpringBootBaseTest : Thread2 获取到的version:1
2021-04-14 10:59:38.158 INFO 8840 --- [ Thread1 ] tech.spring.SpringBootBaseTest : Thread1 获取到的version:1
2021-04-14 10:59:38.158 INFO 8840 --- [ Thread3 ] tech.spring.SpringBootBaseTest : Thread3 获取到的version:1
2021-04-14 10:59:38.158 INFO 8840 --- [ Thread7 ] tech.spring.SpringBootBaseTest : Thread7 获取到的version:1
2021-04-14 10:59:38.158 INFO 8840 --- [ Thread4 ] tech.spring.SpringBootBaseTest : Thread4 获取到的version:1
2021-04-14 10:59:38.178 ERROR 8840 --- [ Thread5 ] tech.spring.SpringBootBaseTest : Thread5 更新失败
2021-04-14 10:59:38.179 ERROR 8840 --- [ Thread6 ] tech.spring.SpringBootBaseTest : Thread6 更新失败
2021-04-14 10:59:38.179 ERROR 8840 --- [ Thread4 ] tech.spring.SpringBootBaseTest : Thread4 更新失败
2021-04-14 10:59:38.180 ERROR 8840 --- [ Thread3 ] tech.spring.SpringBootBaseTest : Thread3 更新失败
2021-04-14 10:59:38.180 ERROR 8840 --- [ Thread9 ] tech.spring.SpringBootBaseTest : Thread9 更新成功
2021-04-14 10:59:38.180 ERROR 8840 --- [ Thread1 ] tech.spring.SpringBootBaseTest : Thread1 更新失败
2021-04-14 10:59:38.182 ERROR 8840 --- [ Thread8 ] tech.spring.SpringBootBaseTest : Thread8 更新失败
2021-04-14 10:59:38.182 ERROR 8840 --- [ Thread2 ] tech.spring.SpringBootBaseTest : Thread2 更新失败
2021-04-14 10:59:38.184 ERROR 8840 --- [ Thread7 ] tech.spring.SpringBootBaseTest : Thread7 更新失败
2021-04-14 10:59:38.185 ERROR 8840 --- [ Thread0 ] tech.spring.SpringBootBaseTest : Thread0 更新失败
可以看出只有一个线程更新成功
悲观锁
- 认为并发写操作多,每次取数据时认为会被修改,故加锁。
- 适用于写多读少的场景。
- 使用场景
- Java synchronized
- 数据库的 select * from t_xxx for update;
示例
编写一个 service 方法,加上事务。使用 for update 的sql查询语句来查询数据,查询到数据后,延迟1s再提交事务。
@Override
@Transactional(rollbackFor = Exception.class)
public void testPessimisticLock() {
long begin = System.currentTimeMillis();
MemberDO memberDO = memberDOMapper.selectByIdForUpdate(1L);
if (null != memberDO && null != memberDO.getId()) {
log.error(Thread.currentThread().getName() + " 获得数据");
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.error(Thread.currentThread().getName() + " 耗时: " + (System.currentTimeMillis() - begin));
}
select id from t_member where id = #{id} for update
编写一个单测,使用5个线程并发调用service
@Test
public void pessimisticLock() throws InterruptedException {
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(new MyPessimisticThread());
thread.setName("Thread" + i);
thread.start();
}
// 等待线程执行完并在控制台输出结果
Thread.sleep(10000);
}
class MyPessimisticThread implements Runnable {
@Override
public void run() {
orderOperService.testPessimisticLock();
}
}
运行单测,观察控制台输出
2021-04-14 14:11:44.377 ERROR 6348 --- [ hread3] t.s.s.order.impl.OrderOperServiceImpl : Thread3 获得数据
2021-04-14 14:11:45.380 ERROR 6348 --- [ hread3] t.s.s.order.impl.OrderOperServiceImpl : Thread3 耗时: 1275
2021-04-14 14:11:45.383 ERROR 6348 --- [ hread2] t.s.s.order.impl.OrderOperServiceImpl : Thread2 获得数据
2021-04-14 14:11:46.388 ERROR 6348 --- [ hread2] t.s.s.order.impl.OrderOperServiceImpl : Thread2 耗时: 2283
2021-04-14 14:11:46.390 ERROR 6348 --- [ hread0] t.s.s.order.impl.OrderOperServiceImpl : Thread0 获得数据
2021-04-14 14:11:47.393 ERROR 6348 --- [ hread0] t.s.s.order.impl.OrderOperServiceImpl : Thread0 耗时: 3288
2021-04-14 14:11:47.396 ERROR 6348 --- [ hread1] t.s.s.order.impl.OrderOperServiceImpl : Thread1 获得数据
2021-04-14 14:11:48.408 ERROR 6348 --- [ hread1] t.s.s.order.impl.OrderOperServiceImpl : Thread1 耗时: 4303
2021-04-14 14:11:48.409 ERROR 6348 --- [ hread4] t.s.s.order.impl.OrderOperServiceImpl : Thread4 获得数据
2021-04-14 14:11:49.425 ERROR 6348 --- [ hread4] t.s.s.order.impl.OrderOperServiceImpl : Thread4 耗时: 5320
可以看出,同一时间,只有一个线程能取到数据,其余线程进入等待。最后一个线程(Thread4)耗时最长
注意
for update 需要在事务中进行。如上代码,如果将 testPessimisticLock() 方法上的 @Transactional 注解去除,则多个线程可以同时取出数据,for update 不生效,则线程不安全。