当前位置 博文首页 > java1234的博客:“乐观锁”解决高并发下的幂等性问题(附java实

    java1234的博客:“乐观锁”解决高并发下的幂等性问题(附java实

    作者:[db:作者] 时间:2021-06-03 11:29

    什么是幂等性?

    幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。在编程中.一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。例如,“getUsername()和setTrue()”函数就是一个幂等函数. 更复杂的操作幂等保证是利用唯一交易号(流水号)实现. 我的理解:幂等就是一个操作,不论执行多少次,产生的效果和返回的结果都是一样的 。

    高并发下的幂等性问题

    这里以两个实例来看下高并发下的幂等性问题;

    一,购票实例

    购票实现流程如下:

    step1:查询是否有票,有票的话,继续下一步,否则提示无票,结束;

    step2:从用户账户扣除票款;

    step3:余票减一操作;

    这里的话,正常情况没问题,但是比如用户连续多点了几次,或者网络问题导致的再或者多人同时购买的时候的并发情况下,step1步骤会有两个或者多个线程同时进入,这时候判断都是有票的,然后继续进入step2,step3,这时候,就可能会出现余票负数,多卖的情况;

    二,充值实例

    充值实现流程如下:

    step1:用户输入充值金额,请求后端业务系统;

    step2:后端生成订单,订单状态是未支付,然后再请求第三方支付接口;

    step3:用户端确认支付;

    step4:第三方支付通过我方提供的回调接口异步通知支付结果;

    具体step4 demo代码如下:

    System.out.println("查询订单");
    Order order = orderMapper.getByOrderId(orderId); // 根据订单id获取订单
    if(order.getStatus()==0){ // 假如是未支付状态
      System.out.println("未支付状态");
      order.setStatus(1); // 设置支付成功状态
      System.out.println("更新支付状态...");
      orderMapper.update(order); // 更新支付状态
      System.out.println("账户充值...");
      userAccountMapper.addAmount(order.getAmount(),userAccount.getUserId()); // 账户充值
      System.out.println("充值完毕...");
      return true;
    }else{ // 已经支付成功,订单已处理
      System.out.println("发现订单已处理");
      return true;
    }
    

    这个第四步是有缺陷的,假如第三方支付系统问题或者网络问题,有多个线程同时执行进入

    Order order = orderMapper.getByOrderId(orderId);
    

    根据订单id查询订单信息,发现status状态都是未支付,所以都进入if里面,这时候就出现了账户重复充值的情况;

    幂等性问题总结

    只要更新数据是依赖读取的数据作为基础条件的,当遇到高并发的时候,就可能会出现幂等性问题;
    又比如在更新数据不依赖查询的数据的就不会有问题,例如修改用户的名称,多人同时修改,结果并不依赖于之前的用户名字,这就不会有并发更新问题。

    幂等性问题解决方案

    关于幂等性问题的解决方案,业界提供了很多解决方案,如单机系统的Java 同步锁,乐观锁,悲观锁,分布式锁,唯一性索引,token机制防止页面重复提交等,每种方案各有利弊;不过主流的话,还是乐观锁和分布式锁这两个方案;

    Java同步锁方案

    我们可以使用synchronized同步锁,把查询状态的代码和更新的代码放一个同步锁内,这样同一时刻只能有一个线程进入执行,等执行完其他线程才能进入,这样能解决幂等性问题,但是假如同步块里面的业务代码执行时间比较长,这样会严重影响用户体验,和系统的吞吐量。所以不是最佳方案;

    悲观锁方案

    悲观锁(Pessimistic Lock),顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。

    悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。

    Java synchronized 就属于悲观锁的一种实现,每次线程要修改数据时都先获得锁,保证同一时刻只有一个线程能操作数据,其他线程则会被block。

    数据库的悲观锁通过 for update 实现的;

    select * from t_order where orderId=#{orderId} for update
    

    悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,影响用户体验和系统吞吐量,所以一般也不采用。

    乐观锁方案

    乐观锁(Optimistic Lock),顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在提交更新的时候会判断一下在此期间别人有没有去更新这个数据。乐观锁适用于读多写少的应用场景,这样可以提高吞吐量。

    乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。

    乐观锁一般来说有以下2种方式:

    1. 使用数据版本(Version)记录机制实现,这是乐观锁最常用的一种实现方式。何谓数据版本?即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。

    2. 使用时间戳(timestamp)。乐观锁定的第二种实现方式和第一种差不多,同样是在需要乐观锁控制的table中增加一个字段,名称无所谓,字段类型使用时间戳(timestamp), 和上面的version类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。

    乐观锁方案在不影响系统性能的情况下,解决了高并发幂等性问题,所以被得到广泛使用。唯一的缺点就是对代码具有入侵性。

    分布式锁

    对于分布式系统,多个系统独立运行,所以同步锁肯定是不行的;对于分布式系统,可以用乐观锁或者分布式锁来解决幂等性问题;

    具体方案有:

    1. 基于缓存(Redis等)实现分布式锁;

    2. 基于Zookeeper实现分布式锁;

    (备注:下期我们会提供具体实现方案的视频教程,感谢关注)

    基于“乐观锁”解决幂等性视频教程

    感谢各位兄弟姐妹关注,锋哥为了大伙能更深刻的掌握“乐观锁”解决幂等性问题,专门录制了一期视频教程。主要以账户充值为例,采用IDEA开发工具,数据库Mysql5.7,demo基于springboot+mybatis架构,用JMeter测试工具模拟,高并发,来测试出幂等性问题,也就账户被重复充值的场景。然后通过基于状态机version字段的乐观锁解决方案,解决幂等性问题,也同时附有完整代码。

    纸上得来终觉浅,绝知此事要躬行。

    需要多实战练习和思考。

    B站视频教程在线地址

    关于锋哥

    【作者】:锋哥 【微信号】:java9568 (加好友,请备注CSDN)
    【公众号】:java1234。欢迎大家关注~
    【作者简介】:江苏师范大学计算机系,Java资深老司机,先后国网电力,一线很多家小公司撸码过;目前老家南通开工作室创业,目前房子,车子,老婆,孩子都搞定;希望和各位读者成为朋友;一起探讨java技术和java创业;