关于乐观锁的概念介绍摘自https://www.cnblogs.com/sheseido/p/5038562.html,特此声明 !
一.下订单与库存的乐观锁机制
乐观锁,大多是基于数据版本 Version 记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来 实现。 读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提 交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( balance )为 $100 。
- 操作员 A 此时将其读出(version=1 ),并从其帐户余额中扣除 $50( $100-$50 )。
- 在操作员 A 操作的过程中,操作员 B 也读入此用户信息( version=1 ),并从其帐户余额中扣除 $20 ( $100-$20 )。
- 操作员 A 完成了修改工作,将数据版本号加一( version=2 ),连同帐户扣除后余额( balance=$50 ),提交至数据库更新,此时由于提交数据版本大 于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 。
- 操作员 B 完成了操作,也将版本号加一( version=2 )试图向数据库提交数 据( balance=$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 2 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须大于记 录当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。这样,就避免了操作员 B 用基于version=1 的旧数据修改的结果覆盖操作 员 A 的操作结果的可能。
从上面的例子可以看出,乐观锁机制避免了长事务中的数据库加锁开销(操作员 A和操作员 B 操作过程中,都没有对数据库数据加锁),大大提升了大并发量下的系统整体性能表现。 需要注意的是,乐观锁机制往往基于系统中的数据存储逻辑,因此也具备一定的局限性,如在上例中,由于乐观锁机制是在我们的系统中实现,来自外部系统的用户余额更新操作不受我们系统的控制,因此可能会造成脏数据被更新到数据库中。在系统设计阶段,我们应该充分考虑到这些情况出现的可能性,并进行相应调整(如将乐观锁策略在数据库存储过程中实现,对外只开放基于此存储过程的数据更新途 径,而不是将数据库表直接对外公开)。
下面我们来看看 代码,这里其实运用了前面所讲的接口抽出和服务之间的调用,但由于篇幅,我只将业务层的业务逻辑代码和其中关键sql贴出来,理解乐观锁也只需要这些代码,完整代码大家可以去我github上看:github。具体业务逻辑如下:
- 将传过来的订单信息放入order对象中
- 查询出当前操作对象的版本号
- 更新库存——如果步骤2查询的版本号与此时的数据库版本号一致,且库存大于0,则我们更新库存(数量减1,修改更新时间等),返回1。否则不更新库存返回0
- 如果更新库存结果为1(成功),则将第一步的order对象存入数据库(入库);为0则可能是库存为0或乐观锁机制生效(及版本号错误),我们通过查询库存来判断是那个原因,然后返回相应的值给用户。
package com.yy.order.service.impl;
import com.alibaba.dubbo.config.annotation.Reference;
import com.yy.order.constants.OrderStatus;
import com.yy.order.entity.Order;
import com.yy.order.mapper.OrderMapper;
import com.yy.order.service.OrderService;
import com.yy.store.service.api.StoreServiceApi;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.*;
@Service
public class OrderServiceImpl implements OrderService{
@Autowired
private OrderMapper orderMapper;
@Reference(version = "1.0.0",
application = "${dubbo.application.id}",
interfaceName = "com.bfxy.store.service.StoreServiceApi",
check = false,
timeout = 3000,
retries = 0
)
private StoreServiceApi storeServiceApi;
@Override
public boolean createOrder(String cityId, String platformId, String userId, String supplierId, String goodsId) {
boolean flag = true;
try {
Order order = new Order();
order.setOrderId(UUID.randomUUID().toString().substring(0, 32)); //订单号
order.setOrderType("1"); //订单的类型
order.setCityId(cityId);
order.setPlatformId(platformId);
order.setUserId(userId);
order.setSupplierId(supplierId);
order.setGoodsId(goodsId);
order.setOrderStatus(OrderStatus.ORDER_CREATED.getValue()); //订单的状态
order.setRemark("");
Date currentTime = new Date();
order.setCreateBy("admin"); //创建人
order.setCreateTime(currentTime); //时间
order.setUpdateBy("admin");
order.setUpdateTime(currentTime);
//乐观锁更新
// 查询当前的版本号
int currentVersion = storeServiceApi.selectVersion(supplierId, goodsId);
// 更新库存
int updateRetCount = storeServiceApi.updateStoreCountByVersion(currentVersion, supplierId, goodsId, "admin", currentTime);
if(updateRetCount == 1) {
// DOTO: 如果出现SQL异常入库失败, 那么要对"库存的数量"和"版本号"进行回滚操作(即catch住回滚)
orderMapper.insertSelective(order); //入库
}
else if (updateRetCount == 0){ // 没有更新成功 1 高并发时乐观锁生效 2 库存不足
flag = false; // 下单标识失败
int currentStoreCount = storeServiceApi.selectStoreCount(supplierId, goodsId); //查询库存
if(currentStoreCount == 0) {
//{flag:false , messageCode: 003 , message: 当前库存不足} -----返回给客户(实际操作)
System.err.println("-----当前库存不足...");
} else {
//{flag:false , messageCode: 004 , message: 乐观锁生效} -----再来一次(实际操作)
System.err.println("-----乐观锁生效...");
}
}
} catch (Exception e) {
e.printStackTrace();
// 具体捕获的异常是什么异常
flag = false;
}
return flag;
}
}
更新库存:updateStoreCountByVersion
<update id="updateStoreCountByVersion">
update t_store ts
set
store_count = store_count - 1, #库存减1
version = version + 1, #版本加1
update_by = #{updateBy,jdbcType=VARCHAR},
update_time = #{updateTime,jdbcType=TIMESTAMP}
where ts.supplier_id = #{supplierId,jdbcType=VARCHAR}
and
ts.goods_id = #{goodsId,jdbcType=VARCHAR}
and
ts.version = #{version,jdbcType=INTEGER} #版本号必须相同!!!
and
ts.store_count > 0 #库存必须大于0!!!
</update>
入库:insertSelective
<insert id="insertSelective" parameterType="com.yy.order.entity.Order" >
insert into t_order
<trim prefix="(" suffix=")" suffixOverrides="," >
<if test="orderId != null" >
order_id,
</if>
<if test="orderType != null" >
order_type,
</if>
<if test="cityId != null" >
city_id,
</if>
<if test="platformId != null" >
platform_id,
</if>
<if test="userId != null" >
user_id,
</if>
<if test="supplierId != null" >
supplier_id,
</if>
<if test="goodsId != null" >
goods_id,
</if>
<if test="orderStatus != null" >
order_status,
</if>
<if test="remark != null" >
remark,
</if>
<if test="createBy != null" >
create_by,
</if>
<if test="createTime != null" >
create_time,
</if>
<if test="updateBy != null" >
update_by,
</if>
<if test="updateTime != null" >
update_time,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides="," >
<if test="orderId != null" >
#{orderId,jdbcType=VARCHAR},
</if>
<if test="orderType != null" >
#{orderType,jdbcType=VARCHAR},
</if>
<if test="cityId != null" >
#{cityId,jdbcType=VARCHAR},
</if>
<if test="platformId != null" >
#{platformId,jdbcType=VARCHAR},
</if>
<if test="userId != null" >
#{userId,jdbcType=VARCHAR},
</if>
<if test="supplierId != null" >
#{supplierId,jdbcType=VARCHAR},
</if>
<if test="goodsId != null" >
#{goodsId,jdbcType=VARCHAR},
</if>
<if test="orderStatus != null" >
#{orderStatus,jdbcType=VARCHAR},
</if>
<if test="remark != null" >
#{remark,jdbcType=VARCHAR},
</if>
<if test="createBy != null" >
#{createBy,jdbcType=VARCHAR},
</if>
<if test="createTime != null" >
#{createTime,jdbcType=TIMESTAMP},
</if>
<if test="updateBy != null" >
#{updateBy,jdbcType=VARCHAR},
</if>
<if test="updateTime != null" >
#{updateTime,jdbcType=TIMESTAMP},
</if>
</trim>
</insert>