悲观锁的应用:删除和结案
src/main/java/com/weiyu/service/capitalinfoservice.java
/**
* 删除资金信息(只能删除未结案的记录)
* <p>使用悲观锁防止并发修改,同时删除关联的附件文件(文件删除失败不影响数据库事务)。</p>
*
* @param id 主键id
* @throws businessexception 如果记录不存在或已结案
*/
@transactional(rollbackfor = exception.class)
public void deletebyid(integer id) {
// 1、悲观锁锁定行,避免并发修改(防止在检查状态后状态被其他事务修改)
capitalinfo entity = capitalinfomapper.selectforupdate(id);
if (entity == null) {
throw new businessexception("资金信息不存在");
}
if (objects.equals(entity.getcapitalstate(), capitalinfostateenum.closed.getvalue())) {
throw new businessexception("已结案的资金信息不能删除");
}
// 2、执行条件删除(lambdaquerywrapper 确保状态条件,防止代码层面被绕过)
lambdaquerywrapper<capitalinfo> querywrapper = new lambdaquerywrapper<>();
// 构造删除sql,delete from capitalinfomanage where (cim_id = ? and cim_state <> ?)
querywrapper
.eq(capitalinfo::getid, id)
// 已结案的不能删除
.ne(capitalinfo::getcapitalstate, capitalinfostateenum.closed.getvalue());
int deletedrows = capitalinfomapper.delete(querywrapper);
if (deletedrows != 1) {
// 理论上由于锁保护,不会出现不等于1的情况,但保留防御性检查
throw new businessexception("删除资金信息失败,影响行数:" + deletedrows);
}
log.info("删除资金信息成功,id={}", id);
// 3、删除关联文件(放在事务外,避免文件操作失败导致事务回滚)
// 由于数据库删除已成功,即使文件删除失败,也仅记录错误,不影响主流程
try {
fileservice.deletebybusinesstypekey("capitalinfo", string.valueof(id));
} catch (exception e) {
log.error("删除资金信息关联文件失败,id={}", id, e);
}
}
/**
* 结案资金信息(更新状态为已结案)
* <p>只有未结案的记录才能结案。</p>
*
* @param id 主键id
* @return 受影响的行数(始终为 1,失败则抛出异常)
* @throws businessexception 如果记录不存在或已结案
*/
@transactional(rollbackfor = exception.class)
public integer closecapitalinfo(integer id) {
// 1、悲观锁锁定行,避免并发修改(防止在检查状态后状态被其他事务修改)
capitalinfo entity = capitalinfomapper.selectforupdate(id);
if (entity == null) {
throw new businessexception("资金信息不存在");
}
if (entity.getcapitalstate() == capitalinfostateconstants.closed) {
throw new businessexception("资金信息已结案,不能重复结案");
}
// 2、执行更新
lambdaupdatewrapper<capitalinfo> updatewrapper = new lambdaupdatewrapper<>();
// 构造更新sql,update capitalinfomanage set cim_state=? where (cim_id = ? and cim_state <> ?)
updatewrapper
.set(capitalinfo::getcapitalstate, capitalinfostateconstants.closed)
.eq(capitalinfo::getid, id)
// 已结案的不能更新
.ne(capitalinfo::getcapitalstate, capitalinfostateconstants.closed);
// 这两种写法完全等价
// int updated = capitalinfomapper.update(null, updatewrapper);
int updated = capitalinfomapper.update(updatewrapper);
if (updated != 1) {
// 理论上由于锁保护,不会出现不等于1的情况,但保留防御性检查
throw new businessexception("结案失败,请重试");
}
log.info("结案资金信息成功,id={}", id);
return updated;
}src/main/java/com/weiyu/mapper/capitalinfomapper.java
package com.weiyu.mapper;
import com.baomidou.mybatisplus.core.mapper.basemapper;
import com.weiyu.model.capitalinfo;
import org.apache.ibatis.annotations.select;
import java.util.list;
/**
* 资金信息 mapper
*/
public interface capitalinfomapper extends basemapper<capitalinfo> {
/**
* 批量插入资金信息
*/
void insertbatch(list<capitalinfo> capitalinfos);
/**
* 悲观锁查询资金信息(锁定行,直到事务结束,适配 mssql)
* 使用 with (updlock, rowlock) 实现 sql server 行级悲观锁
* 仅查询 id 和 capitalstate 字段,提高性能
*/
@select("select cim_id as id, cim_state as capitalstate " +
"from capitalinfomanage with (updlock, rowlock) " +
"where cim_id = #{id}")
capitalinfo selectforupdate(integer id);
}
为什么不用悲观锁:更新不用悲观锁
src/main/java/com/weiyu/service/capitalinfoservice.java
/**
* 更新资金信息
* <p>仅更新有变化的字段(通过新旧值对比),未变化的字段会被设置为 null 以避免无效更新。</p>
* <p>只能更新未结案的记录,资金状态字段不可修改。</p>
*
* @param updaterequest 更新请求对象
* @return 受影响的行数(0 表示无变化)
* @throws businessexception 如果记录不存在或已结案
*/
public integer updatecapitalinfo(capitalinfoupdaterequest updaterequest) {
// 1、检查记录是否存在且未结案
capitalinfo storecapitalinfo = capitalinfomapper.selectbyid(updaterequest.getid());
if (storecapitalinfo == null) {
throw new businessexception("资金信息不存在");
}
if (storecapitalinfo.getcapitalstate() == capitalinfostateconstants.closed) {
throw new businessexception("已结案的资金信息不能更新");
}
capitalinfo capitalinfo = new capitalinfo();
beanutils.copyproperties(updaterequest, capitalinfo);
// 是否存在有修改过的属性
boolean hasmodifiedpropertie = false;
// 只更新有修改过的属性,通过数据新旧值对比,如果数据没有变化,设置为 null,就不会进行更新
// 资金序号
if (storecapitalinfo.getcapitalno() != null && capitalinfo.getcapitalno() != null &&
storecapitalinfo.getcapitalno().equals(capitalinfo.getcapitalno())) {
capitalinfo.setcapitalno(null);
} else {
hasmodifiedpropertie = true;
}
// 资金名称
if (storecapitalinfo.getcapitalname() != null && capitalinfo.getcapitalname() != null &&
storecapitalinfo.getcapitalname().equals(capitalinfo.getcapitalname())) {
capitalinfo.setcapitalname(null);
} else {
hasmodifiedpropertie = true;
}
// 资金类别
if (storecapitalinfo.getcapitaltype() != null && capitalinfo.getcapitaltype() != null &&
storecapitalinfo.getcapitaltype().equals(capitalinfo.getcapitaltype())) {
capitalinfo.setcapitaltype(null);
} else {
hasmodifiedpropertie = true;
}
// 指标预算总额,使用 compareto 比较 bigdecimal
if (storecapitalinfo.getcapitaltotal() != null && capitalinfo.getcapitaltotal() != null &&
storecapitalinfo.getcapitaltotal().compareto(capitalinfo.getcapitaltotal()) == 0) {
capitalinfo.setcapitaltotal(null);
} else {
hasmodifiedpropertie = true;
}
// 指标剩余额,使用 compareto 比较 bigdecimal
if (storecapitalinfo.getcapitalleavetotal() != null && capitalinfo.getcapitalleavetotal() != null &&
storecapitalinfo.getcapitalleavetotal().compareto(capitalinfo.getcapitalleavetotal()) == 0) {
capitalinfo.setcapitalleavetotal(null);
} else {
hasmodifiedpropertie = true;
}
// 指标可用总额,使用 compareto 比较 bigdecimal
if (storecapitalinfo.getcapitalvalidtotal() != null && capitalinfo.getcapitalvalidtotal() != null &&
storecapitalinfo.getcapitalvalidtotal().compareto(capitalinfo.getcapitalvalidtotal()) == 0) {
capitalinfo.setcapitalvalidtotal(null);
} else {
hasmodifiedpropertie = true;
}
// 指标类别
if (storecapitalinfo.getcapitalindextype() != null && capitalinfo.getcapitalindextype() != null &&
storecapitalinfo.getcapitalindextype().equals(capitalinfo.getcapitalindextype())) {
capitalinfo.setcapitalindextype(null);
} else {
hasmodifiedpropertie = true;
}
// 资金账户
if (storecapitalinfo.getcapitalaccount() != null && capitalinfo.getcapitalaccount() != null &&
storecapitalinfo.getcapitalaccount().equals(capitalinfo.getcapitalaccount())) {
capitalinfo.setcapitalaccount(null);
} else {
hasmodifiedpropertie = true;
}
// 资金来源
if (storecapitalinfo.getcapitalsource() != null && capitalinfo.getcapitalsource() != null &&
storecapitalinfo.getcapitalsource().equals(capitalinfo.getcapitalsource())) {
capitalinfo.setcapitalsource(null);
} else {
hasmodifiedpropertie = true;
}
// 指标来源
if (storecapitalinfo.getcapitalindexsource() != null && capitalinfo.getcapitalindexsource() != null &&
storecapitalinfo.getcapitalindexsource().equals(capitalinfo.getcapitalindexsource())) {
capitalinfo.setcapitalindexsource(null);
} else {
hasmodifiedpropertie = true;
}
// 资金年份
if (storecapitalinfo.getcapitalyear() != null && capitalinfo.getcapitalyear() != null &&
storecapitalinfo.getcapitalyear().equals(capitalinfo.getcapitalyear())) {
capitalinfo.setcapitalyear(null);
} else {
hasmodifiedpropertie = true;
}
// 备注信息
if (storecapitalinfo.getremark() != null && capitalinfo.getremark() != null &&
storecapitalinfo.getremark().equals(capitalinfo.getremark())) {
capitalinfo.setremark(null);
} else {
hasmodifiedpropertie = true;
}
// 资金状态不能修改
capitalinfo.setcapitalstate(null);
if (hasmodifiedpropertie) {
// 根据 id 更新,更新非空字段,null 值不更新
int updated = capitalinfomapper.updatebyid(capitalinfo);
if (updated != 1) {
throw new businessexception("更新资金信息失败,请重试");
}
log.info("更新资金信息成功,id={}", updaterequest.getid());
return updated;
}
return 0;
}在当前的 updatecapitalinfo 方法中,没有使用悲观锁(select ... for update),而 deletebyid 和 closecapitalinfo 使用了。这种设计主要基于对业务敏感度和并发风险的权衡。
1. 更新操作的特点
- 多字段更新:更新操作可能涉及资金序号、名称、金额等多个字段。
- 并发场景:两个线程可能同时更新同一记录的不同字段(如一个改金额,另一个改备注),或者一个线程更新后另一个线程基于旧值再次更新。
- 无破坏性:即使发生并发覆盖,最终数据仍然是一个合理的状态(只是后提交的覆盖了先提交的)。不会造成数据“丢失”或“状态错误”等严重后果,因为业务规则(如不能结案后再更新)已经在代码中通过前置检查保证了。
2. 为什么删除和结案用了悲观锁?
- 删除:一旦删除,记录消失,不可恢复。若并发删除,可能导致“先查后删”时状态被另一事务改变,引发误删或重复删除。因此必须用悲观锁锁定行,确保检查和删除的原子性。
- 结案:状态从“未结案”变为“已结案”,是不可逆的操作。若并发结案,两个事务同时检查状态,都认为可以结案,最终会重复更新(但状态还是 closed,无副作用)。但为避免“结案后又被其他操作修改”的场景,使用悲观锁可以确保结案时其他事务无法同时修改该记录,防止脏读。
3. 更新操作不使用悲观锁的合理性
- 性能考虑:悲观锁会锁住行,影响并发性能。在更新频繁但冲突概率较低的场景下,过度使用悲观锁会降低系统吞吐量。
- 业务容忍度:更新操作的并发冲突通常表现为“最后提交的覆盖之前的结果”,这在很多业务中是可以接受的(例如用户a和b同时编辑同一条记录,后保存的覆盖前保存的)。如果业务要求更严格的并发控制(如不允许覆盖),则应引入乐观锁(版本号)而非悲观锁。
4. 当前代码的潜在风险
当前 updatecapitalinfo 中,通过 selectbyid 获取旧数据,然后在内存中对比哪些字段发生了变化,最后执行 updatebyid(按主键更新,没有版本控制)。如果两个线程同时执行,可能出现:
这是典型的“丢失更新”问题。虽然不会破坏状态约束,但可能不符合用户预期。
- 线程a读取记录(state=0, name="a")。
- 线程b读取记录(state=0, name="a")。
- 线程a将 name 改为 "b",更新成功。
- 线程b将 remark 改为 "c",更新成功(此时 name 又变回 "a",覆盖了线程a的修改)。
5. 改进建议
若希望避免这种覆盖,可以引入乐观锁(在实体类增加 @version 字段),更新时使用 updatebyid 并检查版本号。这样当版本不匹配时,抛出异常,由上层处理重试。这种方式比悲观锁更轻量,适合更新场景。
6. 结论
当前设计是合理的:删除和结案使用悲观锁保证关键操作的原子性;更新操作因业务敏感度较低,且已通过前置状态检查防止违规操作,暂未使用锁。若未来需要更严格的并发控制,可以引入乐观锁。
到此这篇关于spring boot中悲观锁和乐观锁的应用讲解的文章就介绍到这了,更多相关spring boot 悲观锁和乐观锁内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论