悲观锁与乐观锁浅谈

本文详细介绍了悲观锁和乐观锁的概念及其在并发控制中的应用。悲观锁在修改数据前加锁,适合写多读少的场景,如Java的`synchronized`和数据库的行锁。乐观锁则在更新时检查冲突,适用于读多写少,可通过版本号字段或MySQL的MVCC实现。文中通过库存操作示例,演示了如何用SQL实现乐观锁,防止并发问题,确保数据一致性。

悲观锁、乐观锁是实现并发控制的两种思想,而不是指具体的某一种锁。

1、悲观锁

①悲观锁总是认为数据会被其他线程修改,所以在修改前强制加锁,使其他线程阻塞等待,具有强烈的独占和排他特性。
②传统的关系型数据库的行锁,表锁,读锁,写锁等,以及Java中synchronized关键字都是悲观锁的实现。
③悲观锁比较适用于写多读少的情况(多写场景)。

2、乐观锁

①乐观锁认为在一般情况下数据不会被其他线程修改,所以在修改前不会加锁,而是在数据提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回给用户错误的信息,让用户决定如何去做。
②乐观锁不会刻意使用数据库本身的锁机制,而是依据数据本身来保证数据的正确性,如加版本号字段、AtomicInteger类的CAS(比较并交换)、MySQL中InnoDB的MVCC(多版本控制)机制等都是乐观锁的实现。
③乐观锁比较适用于读多写少的情况(多读场景)。

下面以操作库存为例简单说明如何通过sql语句实现乐观锁。
测试表结构及数据如下:
在这里插入图片描述
在这里插入图片描述
更新库存:

<update id="updateCount1" parameterType="com.demo.custom.component.model.DO.TestDO">
    update T_TEST set COUNT = #{count} where ID = #{id}
</update>

以两个人(线程)同时操作库存模拟并发场景,在没有悲观锁的情况下,有可能会发生以下的情形:

@Test
public void test1() throws Exception {
    //小明查询库存
    TestDO xiaoming = testDao.selectById(1);

    //小李查询库存
    TestDO xiaoli = testDao.selectById(1);

    //小明增加5个库存
    xiaoming.setCount(xiaoming.getCount() + 5);
    if (testDao.updateCount1(xiaoming) > 0) {
        System.out.println("小明更新库存成功!");
    } else {
        System.out.println("小明更新库存失败!");
    }

    //小李扣减3个库存
    xiaoli.setCount(xiaoli.getCount() - 3);
    if (testDao.updateCount1(xiaoli) > 0) {
        System.out.println("小李更新库存成功!");
    } else {
        System.out.println("小李更新库存失败!");
    }
}

很明显,此时两个线程都能更新成功,但最终库存为7,不正确,而且也不能防止超卖问题。
优化sql后如下:

<update id="updateCount2" parameterType="com.demo.custom.component.model.DO.TestDO">
    update T_TEST set COUNT = COUNT + #{count} where ID = #{id} and COUNT >= -#{count}
</update>

单条更新语句在执行会加行锁,在提交前不会被其他线程更新,因此能保证数据正确,库存count做增量传入,由数据库自己计算,where COUNT >= -#{count}确保相减后不能小于0。
测试如下:

@Test
public void test2() throws Exception {
    //小明查询库存
    TestDO xiaoming = testDao.selectById(1);

    //小李查询库存
    TestDO xiaoli = testDao.selectById(1);

    //小明增加5个库存
    xiaoming.setCount(5);
    if (testDao.updateCount2(xiaoming) > 0) {
        System.out.println("小明更新库存成功!");
    } else {
        System.out.println("小明更新库存失败!");
    }

    //小李扣减3个库存
    xiaoli.setCount(-3);
    if (testDao.updateCount2(xiaoli) > 0) {
        System.out.println("小李更新库存成功!");
    } else {
        System.out.println("小李更新库存失败!");
    }
}

最后总库存为12,更新正确!不过以上写法要注意ID字段的索引问题,不然在更新会导致全表扫描,影响执行效率。
另外乐观锁最常见的实现是加一个version(版本号)字段,每次更新是都会以查出的值做判断条件,同步version做递增,sql如下:

<update id="updateCount3" parameterType="com.demo.custom.component.model.DO.TestDO">
    update T_TEST set COUNT = #{count}, VERSION = VERSION + 1 where ID = #{id} and VERSION = #{version}
</update>

测试方法如下:

@RequestMapping("/test3")
public void test3() throws Exception {
    count(0);
}

private void count(int retry) {
    TestDO xiaoli = testDao.selectById(1);
    int count = xiaoli.getCount() - 2;
    if (count >= 0) {
        //业务逻辑……
        xiaoli.setCount(count);
        if (testDao.updateCount3(xiaoli) > 0) {
            System.out.println("线程" + Thread.currentThread().getName() + "更新库存成功!");
        } else {
            int num = retry + 1;
            if (num <= 3) {//重试3次
                System.out.println("线程" + Thread.currentThread().getName() + "更新库存失败!重试第" + num + "次……");
                count(num);
            } else {
                System.out.println("线程" + Thread.currentThread().getName() + "更新库存失败!");
            }
        }
    }
}

测试前数据:
在这里插入图片描述
用jmeter在1s内发20个线程进行测试,结果如下:

线程http-nio-8090-exec-4更新库存成功!
线程http-nio-8090-exec-3更新库存失败!重试第1次……
线程http-nio-8090-exec-1更新库存失败!重试第1次……
线程http-nio-8090-exec-7更新库存失败!重试第1次……
线程http-nio-8090-exec-6更新库存失败!重试第1次……
线程http-nio-8090-exec-2更新库存失败!重试第1次……
线程http-nio-8090-exec-5更新库存失败!重试第1次……
线程http-nio-8090-exec-6更新库存失败!重试第2次……
线程http-nio-8090-exec-3更新库存失败!重试第2次……
线程http-nio-8090-exec-7更新库存成功!
线程http-nio-8090-exec-1更新库存失败!重试第2次……
线程http-nio-8090-exec-2更新库存成功!
线程http-nio-8090-exec-6更新库存失败!重试第3次……
线程http-nio-8090-exec-3更新库存成功!
线程http-nio-8090-exec-5更新库存失败!重试第2次……
线程http-nio-8090-exec-1更新库存失败!重试第3次……
线程http-nio-8090-exec-8更新库存失败!重试第1次……
线程http-nio-8090-exec-6更新库存成功!
线程http-nio-8090-exec-5更新库存失败!重试第3次……
线程http-nio-8090-exec-1更新库存失败!
线程http-nio-8090-exec-8更新库存失败!重试第2次……
线程http-nio-8090-exec-8更新库存成功!
线程http-nio-8090-exec-5更新库存失败!
线程http-nio-8090-exec-9更新库存成功!
线程http-nio-8090-exec-6更新库存成功!
线程http-nio-8090-exec-2更新库存成功!
线程http-nio-8090-exec-1更新库存成功!

在这里插入图片描述
注意:为防止ABA问题,version要通过数据库计算递增,另外如果是数字类型的字段,也可以上面一样传入增量值由数据库计算。

参考:

什么是乐观锁,什么是悲观锁(https://www.jianshu.com/p/d2ac26ca6525

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Alex·Guangzhou

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值