乐观锁和悲观锁在实际开发中的应用
背景
在系统开发中,新增数据和更新数据是很常见的业务行为,有时更新数据还会附带新增数据的行为,面对这些业务行为,系统必须做到接口的幂等性和关联数据的一致性,下面先针对两种业务的常见错误写法和正确写法。
新增操作
常见业务场景:A系统给B系统同步业务数据,业务数据中有一个唯一字段businessCode作为幂等字段。
常见错误处理
先根据businessCode查询数据库,如果数据不为空,则新增数据,这种写法在没有什么并发的时候完全没问题,但是有并发量的时候就会产生脏数据,原因是在T1线程执行红色代码块的时候,T2线程以迅雷不及掩耳之势跑完了,这时T1也能正常插入,所以脏数据也就产生了。
优化处理
新增businessCode字段作为唯一索引,不需要查询数据库判断,直接交给数据库层面处理。
更新处理
常见业务:系统中有一张异常订单表A,订单表A有字段status,如果status为1,代表待处理,就能对该订单做异常登记处理,生成异常登记记录表B,并更新订单表status为2,代表已处理。
常见错误处理
用户做订单异常登记时,会回传订单id和登记信息作为接口参数,先根据订单id查询订单,再判断订单状态,如果订单状态为待处理,则根据订单id和登记信息生成异常登记记录并插入记录表B。
这种写法在没有什么并发的时候完全没问题,但是有并发量的时候就会产生脏数据,原因是在当T1线程执行红色代码块的时候,T2线程已经将订单状态修改了并新增了异常登记记录,T1线程就会插入脏数据。
优化处理
乐观锁方案
将更新语句改为update A set status = 2 where id = 1 and status = 1,并判断返回影响行数,如果影响行数不符合预期则抛异常让数据库回滚已经执行的sql语句。
优点:这种方案本质上是乐观锁的处理方案,适合读多写少的场景,可以根据更新语句返回的数据判断,做合适的业务处理,比如重试,抛异常等;
缺点:如果没有预期状态status值则无法完成这种操作,必须新增真正的乐观锁字段version,如果需要回滚的业务操作太多,则会加大数据库压力。
悲观锁方案 update
先根据订单id和预期状态更新订单,再判断影响行数是否等于预期行数,如果符合预期,则根据订单id和登记信息生成异常登记记录并插入记录表B。
优点:这种方案本质上是悲观锁的处理方案,适合读少写多的场景,当一个事务执行update的操作的时候,会在这行记录上加锁,阻塞其他事务的update操作;
缺点:如果订单表不止更新状态字段,且其他字段的更新需要额外的计算,且异常登记记录表也需要其他额外字段,这种方案就没法做了。
悲观锁方案 select for update
先使用select for update查询订单表A并加锁,在根据查询返回的字段进行逻辑计算,并生成异常登记记录,然后插入数据,最后更新订单状态,这种本质上也是悲观锁方案,适用于更新操作时需要复杂的逻辑计算。