乐观锁/悲观锁及代码示例

479 阅读3分钟

乐观锁/悲观锁学习笔记,如有问题,欢迎指正

乐观锁

  • 认为读多写少,写操作的并发可能性低,取数据的时候认为不会修改,故不会加锁。更新数据时会判断数据是否被其它线程修改,如果被其它线程修改,则不进行更新;如果没有被其它线程修改,则进行更新。
  • 适用于读多写少的场景,提高系统吞吐量。
  • 使用场景:
  1. 原子类(如:AtomicLong)使用的 CAS
  2. 版本号机制

在数据库表中加 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 更新失败

可以看出只有一个线程更新成功

悲观锁

  • 认为并发写操作多,每次取数据时认为会被修改,故加锁。
  • 适用于写多读少的场景。
  • 使用场景
  1. Java synchronized
  2. 数据库的 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 不生效,则线程不安全。