zl程序教程

您现在的位置是:首页 >  其他

当前栏目

小白也能看懂的悲观锁,乐观锁(附生动案例)

案例 小白 乐观 悲观 能看懂
2023-09-11 14:16:57 时间

积分扣款功能

小美是新入职的程序媛,负责后台JAVA的开发工作。由于公司美女程序媛稀缺,领导特别安排小美在老李的工作组。老李是十多年的老程序员了,工作经验丰富,为人亲切,深受下属爱戴。

加入老李的工作组不久,老李安排小美做一个积分商城扣款的功能。

“不就是根据商品标价扣除下积分,然后保存下嘛,还不简单,我可是在学校年年拿奖学金的”!小美心想,于是很快动手开发起来。

public void buyGoods(int integral) {
    //取出用户信息
    User user = getById(1);
    //减去商品积分
    user.setIntegral(user.getIntegral() - integral);
    //保存数据到数据库
    updateById(user);
}

很快小美就写完代码了,测试下没问题就提交了,“又是悠闲的一天”,小美心想,“晚上去吃烤鱼吧”

生产事故

过了几天,老李在统计数据时发现用户的积分对不上,明明卖出去两件商品,怎么才扣了一份的钱,一定是扣款那边出了问题,经验丰富的老李很快发现了问题所在。

老李找到小美,小美信心满满的说,“李哥,不会吧,我这个代码可是自测了好多遍的,而且测试组那边也通过了,怎么会出问题呢”

BUG复现

老李加了几行代码复现了bug.

public void buyGoods(int integral) {
    //取出用户信息
    User user = getById(1);
    System.out.println(user.getIntegral());
    try {
        Thread.sleep(5000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    //减去商品积分
    user.setIntegral(user.getIntegral() - integral);
    //保存数据到数据库
    updateById(user);
}

这个时候如果两个扣款请求同时过来,第一个请求拿到的剩余积分是100,第二个请求也是100,都减去商品积分10然后保存,剩余积分就是90,只会扣最后一次的,这样就会造成扣款错误了。

而且这种问题极难发现,测试的时候一般没啥问题,只有极端情况或者遭受恶意攻击时才会出现

乐观锁的妙用

小美瞬间感觉脑袋嗡嗡的,“糟了,这个没学过啊,该怎么办”。

老李看出了小美的窘态,微笑着说,“没事,用乐观锁就能解决”

“乐观锁?没听过啊”,小美答到。

“就是通过版本号来控制事务的并发”

"如何控制呢?”小美问到

第一步,给数据加版本号

第二步,保存数据时判断版本号是否取出时的版本,如果是则说明数据没有改动,直接保存并且版本号加1,否则回退

//取出用户信息
User user = getById(1);
//减去商品积分
user.setIntegral(user.getIntegral() - integral);
int oldVersion = user.getVersion();
//数据有变动,版本加1
user.setVersion(oldVersion + 1);
//sql: update user set integral = #{integral}, version = #{version} where version = #{oldVersion} and id = #{id}
boolean change = update(user, new LambdaQueryWrapper<User>().eq(User::getId, user.getId()).eq(User::getVersion, oldVersion));
if (!change) {
    throw new ServiceFailException("操作频繁,请稍后再试");
}

小美测试了下,同时两个请求,一个成功了,一个操作失败

 “呀,想不到这么容易就解决了我一个大问题”,小美兴奋的拍手道。

悲观锁的妙用

 "但有个问题耶",小美说到,“如果同一时间有个数据改动频繁,那不是只有一个请求能够成功,其他都失败了么”。

“想不到你观察还挺仔细的”,老李高兴道,“这时就轮到悲观锁登场了”

“悲观锁?不太喜欢这个名字,不太想用耶”,小美有点沮丧

“只是一个名字,不用太在意”,老李说着,开始讲解起悲观锁的原理

悲观锁就是锁定你想要使用的资源,其他请求想要使用这个资源就必须排队,等这个资源释放了才能继续。

<select id="getByIdForUpdate" resultType="com.wkt.entity.User" parameterType="int">
    select * from user where id = #{id} for update
</select>
//排它锁需要开始事务,否则会失效
@Transactional(rollbackFor = Exception.class)
public void buyGoodsByPessimLock(int integral) {
    //使用排它锁实现悲观锁,锁定资源
    User user = getBaseMapper().getByIdForUpdate(1);
    //减去商品积分
    user.setIntegral(user.getIntegral() - integral);
    //由于同一时间只有一个请求可以访问该资源,可以放心保存
    updateById(user);
}

谁动了我的蛋糕

"我知道了",小美兴奋的拍手道,"就像我吃蛋糕吃到一半,有事走开,如果我是预期乐观的,相信不会有人动我的蛋糕,就走的时候数一下有几块蛋糕,回来的时候对一下数量,如果能对的上说明没人动过,如果对不上说明有人偷吃我的蛋糕了。如果我预期是悲观的,相信有人会来偷吃蛋糕,就走的时候把蛋糕锁抽屉里,除了我没人能打开,是不是这样?"

"挺形象的",老李尴尬的笑了笑,"看来以后不能偷吃她的蛋糕了”

参考项目(模块: SpringBoot-HelloWorld): https://gitee.com/huatin/java-test