项目实战:一个由多线程引起的线程安全问题(附:解决方案)
时间:05-18来源:作者:点击数:
项目场景:
- 上游接口批量推送订单信息,订单外面还有运单信息(一个运单包含多个订单),订单和运单都允许有条件的修改。当接口收到推送过来的数据时,要先去查询这个订单对应的运单是否已经存在,不存在则直接把运单信息插入到数据库表中,存在要做判断运单的状态是否允许更新,不允许直接抛异常。
"submitList": [
{
"deliveryCode": "YD12345678"
"xdockOriginOrderDTOS": [
{
"deliveryCode": "YD12345678",
"orderCode": "DD12345677"
}
]
},
{
"deliveryCode": "YD12345678"
"xdockOriginOrderDTOS": [
{
"deliveryCode": "YD12345678",
"orderCode": "DD12345688"
}
]
}
]
问题描述
- 当上游一次推送属于一个运单下的两个订单过来的时候,如果没有在数据库表的运单号字段添加唯一索引,会出现两条运单号一样的数据。如果运单号字段添加了唯一索引,则会抛["\n### Error updating database. Cause: java.sql.SQLIntegrityConstraintViolationException: Duplicate entry '462479945432663' for key 'goods_xdock_package.goods_xdock_package_delivery_code_uindex'"]这个异常。
- 出现线程安全问题的批量新增运单的代码:
// submitList 结构见上面,只展示和问题相关的字段
public Boolean batchAddOriginOrder(List<XdockPackageDTO> submitList) {
for (XdockPackageDTO submitDTO : submitList) {
try {
this.addPackage(submitDTO);
} catch (Exception e) {
log.error(e.getMessage());
e.printStackTrace();
}
}
}
public Boolean addPackage(XdockPackageDTO xdockPackageDTO) {
if (Strings.isNotBlank(xdockPackageDTO.getDeliveryCode())) {
XdockPackage one = xdockPackageService.getOne(new LambdaQueryWrapper<XdockPackage>().eq(XdockPackage::getDeliveryCode, xdockPackageDTO.getDeliveryCode()));
if (Objects.nonNull(one)) {
Assert.isTrue(PackageStatusEnum.CREATED.getCode().equals(one.getStatus()), "运单已签收,不允许修改");
} else {
XdockPackage xdockPackage = XdockConvert.INSTANCE.xdockPackage2DTO(xdockPackageDTO);
xdockPackageService.save(xdockPackage);
}
}
}
原因分析:
- 上面addPackage()方法主要包含3个步骤:
- a. 查询数据库表中是否存在当前传入的运单号
- b. 判断查询结果是否为空
- c. 不为空就判断运单状态,为空就新增运单信息
- 循环遍历列表for (XdockPackageDTO submitDTO : submitList),创建两个线程(线程1和线程2)分别执行两条订单信息的创建。
- 线程1先去执行步骤a(a. 查询数据库表中是否存在当前传入的运单号),然后执行步骤b(b. 判断查询结果是否为空)。由于CPU的切换关系,此时CPU的执行权被切换到了线程2。线程1将步骤b的判断结果等信息保存到线程1的工作内存中,就处于就绪状态,线程2处于运行状态。
- 线程2也需要执行步骤a,由于线程1没有对运单号做新增操作。因此此时线程2执行步骤b查询到的结果也为空。
- 此时CPU的执行权切换到了线程1上。线程1将工作内存中的之前存储的查询结果等数据恢复,执行步骤c(c. 为空就新增运单信息),插入运单号。线程1执行完毕,线程1销毁,
- CPU执行线程2,线程2将工作内存中的之前存储的查询结果等数据恢复,执行步骤c(c. 为空就新增运单信息),插入运单号。如果没有在数据库表的运单号字段添加唯一索引,会出现两条运单号一样的数据。如果运单号字段添加了唯一索引,则会抛["\n### Error updating database. Cause: java.sql.SQLIntegrityConstraintViolationException: Duplicate entry '462479945432663' for key 'goods_xdock_package.goods_xdock_package_delivery_code_uindex'"]这个异常。
解决方案:
Lock 锁
private final ReentrantLock lock = new ReentrantLock();
public Boolean addPackage(XdockPackageDTO xdockPackageDTO) {
if (Strings.isNotBlank(xdockPackageDTO.getDeliveryCode())) {
lock.lock();
try {
XdockPackage one = xdockPackageService.getOne(new LambdaQueryWrapper<XdockPackage>().eq(XdockPackage::getDeliveryCode, xdockPackageDTO.getDeliveryCode()));
if (Objects.nonNull(one)) {
Assert.isTrue(PackageStatusEnum.CREATED.getCode().equals(one.getStatus()), "运单已签收,不允许修改");
} else {
XdockPackage xdockPackage = XdockConvert.INSTANCE.xdockPackage2DTO(xdockPackageDTO);
xdockPackageService.save(xdockPackage);
}
}catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
synchronized锁
synchronized (this) {
XdockPackage one = xdockPackageService.getOne(new LambdaQueryWrapper<XdockPackage>().eq(XdockPackage::getDeliveryCode, xdockPackageDTO.getDeliveryCode()));
if (Objects.nonNull(one)) {
Assert.isTrue(PackageStatusEnum.CREATED.getCode().equals(one.getStatus()), "运单已签收,不允许修改");
} else {
XdockPackage xdockPackage = XdockConvert.INSTANCE.xdockPackage2DTO(xdockPackageDTO);
xdockPackageService.save(xdockPackage);
}
}
synchronized 与 Lock 的区别
synchronized 和 Lock 的用法区别
- synchronized(隐式锁):在需要同步的对象中加入此控制,synchronized 可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。
- lock(显示锁):需要显示指定起始位置和终止位置。一般使用 ReentrantLock 类做为锁,多个线程中必须要使用一个 ReentrantLock 类做为对象才能保证锁的生效,且在加锁和解锁处需要通过 lock() 和 unlock() 显示指出。所以一般会在 finally 块中写 unlock() 以防死锁。
synchronized 和 Lock 性能区别
- synchronized 是托管给 JVM 执行的,而 Lock 是 Java 写的控制锁的代码。在 JDK 1.5 中,synchronize 是性能低效的。因为这是一个重量级操作,需要调用操作接口,导致有可能加锁消耗的系统时间比加锁以外的操作还多。相比之下使用 Java 提供的 Lock 对象,性能更高一些。但是到了 JDK 1.6,synchronize 进行很多优化,比如锁升级(无锁 → 偏向锁(JDK15已废弃) → 轻量级锁 → 重量级锁)等等。所以在 JDK 1.6 以上 synchronize 的性能并不比 Lock 差。
synchronized 和 lock 机制区别
- synchronized 原始采用的是 CPU悲观锁机制,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。
- Lock 用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作(Compare and Swap)。