一、传统侵入式设计的弊端

1.1 真实业务场景

在零售连锁系统中,门店对配货单进行收货时存在双重收货机制:

机制1:店员手动收货

  • 门店店员在系统中点击”确认收货”按钮
  • 系统根据实际收货数量更新配货单明细
  • 更新配货单状态

机制2:系统自动收货

  • 定时任务每天中午12点扫描所有未收货的配货单
  • 如果配货单的”预计送达日期”已过,系统自动触发收货流程
  • 按照发货数量自动生成收货记录

并发冲突场景: 假设某配货单的预计送达日期是”2025-12-16”,在12月16日中午:

  • 12:00:03 - 定时任务扫描到该配货单,开始执行自动收货
  • 12:00:04 - 门店店员恰好在系统中点击”确认收货”

不加锁的后果:

  • 配货单明细的收货数量被重复更新(实际收货100件,但记录显示200件)
  • 生成两条收货记录

解决方案:使用分布式锁,同一配货单同一时刻只能执行一次收货操作(无论人工还是自动)。

1.2 传统写法示例

在传统的分布式锁实现中,我们通常会将锁逻辑直接写在业务方法内部:

@Slf4j
@Service
public class DeliveryReceiveService {
    
    @Autowired
    private RedissonClient redissonClient;
    
    @Autowired
    private DeliveryDocsMapper deliveryDocsMapper;
    
    @Autowired
    private DeliveryDocsItemMapper deliveryDocsItemMapper;
    
    /**
     * 店员手动收货(入口1)
     */
    public void confirmReceive(String docsNo, List<ReceiveItem> items) {
        // 调用统一的收货逻辑
        processReceive(docsNo, items);
    }
    
    /**
     * 系统自动收货(入口2)
     */
    public void autoReceive(String docsNo) {
        // 查询配货单明细
        List<DeliveryDocsItem> itemList = deliveryDocsItemMapper.selectByDocsNo(docsNo);
        
        // 按发货数量生成收货明细
        List<ReceiveItem> items = itemList.stream()
            .map(item -> new ReceiveItem(item.getMaterialId(), item.getShippedNum()))
            .collect(Collectors.toList());
        
        // 调用统一的收货逻辑
        processReceive(docsNo, items);
    }
    
    /**
     * 统一的收货处理逻辑(传统侵入式写法)
     * 核心业务方法,所有收货操作最终都调用这里
     */
    private void processReceive(String docsNo, List<ReceiveItem> items) {
        // 拼接锁的key:按配货单号加锁
        String lockKey = "delivery:receive:" + docsNo;
        RLock rLock = redissonClient.getLock(lockKey);
        
        try {
            // 尝试获取锁,获取不到直接失败(不等待)
            boolean locked = rLock.tryLock(0, TimeUnit.MILLISECONDS);
            if (!locked) {
                throw new RuntimeException("配货单正在收货中,请稍后再试");
            }
            
            // ========== 真正的业务逻辑开始 ==========
            // 1. 查询配货单
            DeliveryDocs docs = deliveryDocsMapper.selectByDocsNo(docsNo);
            if (docs == null) {
                throw new RuntimeException("配货单不存在");
            }
            
            // 2. 校验配货单状态
            if ("RECEIVED".equals(docs.getStatus())) {
                throw new RuntimeException("配货单已收货,请勿重复操作");
            }
            
            // 3. 更新明细的收货数量
            for (ReceiveItem item : items) {
                deliveryDocsItemMapper.updateReceiveNum(
                    docsNo, 
                    item.getMaterialId(), 
                    item.getReceiveNum()
                );
            }
            
            // 4. 更新配货单状态
            docs.setStatus("RECEIVED");
            docs.setReceiveTime(new Date());
            deliveryDocsMapper.updateById(docs);
            // ========== 真正的业务逻辑结束 ==========
            
        } catch (RuntimeException re) {
            throw re;
        } catch (Exception e) {
            log.error("收货失败, docsNo:{}", docsNo, e);
            throw new RuntimeException("收货失败,请稍后重试");
        } finally {
            // 释放锁
            if (rLock.isHeldByCurrentThread()) {
                rLock.unlock();
            }
        }
    }
}

1.3 传统写法的三大弊端

弊端1:代码重复

每个需要加锁的方法都要重复写20+行锁相关代码(获取锁、try-catch、finally释放锁),违反DRY原则

弊端2:业务逻辑被淹没

真正的业务代码被大量锁逻辑包裹,锁代码占比高达40-50%,可读性极差。

弊端3:维护成本高

  • 如果要修改锁的超时时间,需要改动所有方法(10个方法 = 10处修改)
  • 如果要添加锁获取失败的监控日志,需要改动所有方法
  • 新增需要加锁的方法时,必须复制粘贴20+行代码
  • 极易出现复制粘贴导致的bug(忘记修改lockKey、忘记释放锁等)

二、无侵入式设计原理

2.1 核心思想

将业务逻辑作为参数传入通用方法,通用方法负责锁的管理

2.2 实现机制

步骤1:封装通用工具类

为了让所有业务类都能复用,我们将锁逻辑封装为独立的工具类:

@Slf4j
@Component
@RequiredArgsConstructor
public class DistributedLockUtil {
    
    private final RedissonClient redissonClient;
    
    /**
     * 执行带分布式锁的业务逻辑(不等待)
     * 
     * @param lockKey 锁的key
     * @param task    要执行的业务逻辑
     */
    public void executeWithLock(String lockKey, Runnable task) {
        executeWithLock(lockKey, 0, TimeUnit.MILLISECONDS, task);
    }
    
    /**
     * 执行带分布式锁的业务逻辑(可设置等待时间)
     * 
     * @param lockKey     锁的key
     * @param waitTime    等待锁的时间
     * @param timeUnit    时间单位
     * @param task        要执行的业务逻辑
     */
    public void executeWithLock(String lockKey, long waitTime, TimeUnit timeUnit, Runnable task) {
        RLock rLock = redissonClient.getLock(lockKey);
        try {
            boolean locked = rLock.tryLock(waitTime, timeUnit);
            if (locked) {
                task.run();
            } else {
                throw new BusinessException("正在处理中,请稍后再试");
            }
        } catch (BusinessException be) {
            throw be;
        } catch (Exception e) {
            log.error("执行失败, lockKey:{}", lockKey, e);
            throw new BusinessException("操作失败,请稍后重试");
        } finally {
            if (rLock.isHeldByCurrentThread()) {
                rLock.unlock();
            }
        }
    }
}

步骤2:业务类注入工具类

@Service
@RequiredArgsConstructor
public class DeliveryReceiveService {
    
    private final DistributedLockUtil distributedLockUtil;  // 注入工具类
    private final DeliveryDocsMapper deliveryDocsMapper;
    
    /**
     * 店员手动收货
     */
    public void confirmReceive(String docsNo, List<ReceiveItem> items) {
        String lockKey = "delivery:receive:" + docsNo;
        // 直接调用工具类方法
        distributedLockUtil.executeWithLock(lockKey, () -> processReceive(docsNo, items));
    }
}

好处

  • ✅ 全局复用:所有Service类都可以注入DistributedLockUtil使用
  • ✅ 统一维护:修改锁逻辑只需改一个类
  • ✅ 更加清晰:业务类不再有锁相关的代码
  • ✅ 灵活配置:支持自定义等待时间,适应不同业务场景

步骤3:业务方法保持纯净

/**
 * 统一的收货处理逻辑(纯业务逻辑,无锁代码)
 */
private void processReceive(String docsNo, List<ReceiveItem> items) {
    // 1. 查询配货单
    DeliveryDocs docs = deliveryDocsMapper.selectByDocsNo(docsNo);
    if (docs == null) {
        throw new RuntimeException("配货单不存在");
    }
    
    // 2. 校验配货单状态
    if ("RECEIVED".equals(docs.getStatus())) {
        throw new RuntimeException("配货单已收货,请勿重复操作");
    }
    
    // 3. 更新明细的收货数量
    for (ReceiveItem item : items) {
        deliveryDocsItemMapper.updateReceiveNum(
            docsNo, 
            item.getMaterialId(), 
            item.getReceiveNum()
        );
    }
    
    // 4. 更新配货单状态
    docs.setStatus("RECEIVED");
    docs.setReceiveTime(new Date());
    deliveryDocsMapper.updateById(docs);
}

步骤4:调用时使用Lambda表达式

/**
 * 店员手动收货(对外接口)
 */
public void confirmReceive(String docsNo, List<ReceiveItem> items) {
    String lockKey = "delivery:receive:" + docsNo;
    
    // 使用Lambda表达式将业务逻辑封装为Runnable传入
    distributedLockUtil.executeWithLock(lockKey, () -> processReceive(docsNo, items));
}

2.3 Lambda表达式的魔法

Lambda表达式本质

() -> processReceive(docsNo, items)

等价于创建一个匿名类实例:

new Runnable() {
    @Override
    public void run() {
        processReceive(docsNo, items);
    }
}

执行流程图


三、无侵入式设计的优势

对比维度 传统侵入式 无侵入式 提升
锁逻辑复用性 ❌ 每个方法重复 ✅ 统一封装 100%复用
可维护性 ❌ 修改影响所有方法 ✅ 只修改通用方法 维护点从N个降为1个
可测试性 ❌ 难以单独测试业务逻辑 ✅ 业务方法独立可测 测试复杂度降低

四、总结

4.1 技术要点

  1. 函数式接口:Runnable(无返回值)
  2. Lambda表达式() -> method() 简化匿名类创建
  3. 高阶函数:将函数作为参数传递

4.2 适用场景

  • ✅ 分布式锁管理

4.3 核心价值

价值维度 说明
代码简洁 业务方法只保留业务逻辑,非业务代码统一封装
高度复用 通用方法可被所有业务方法复用
易于维护 修改锁逻辑只需改一处,影响范围可控
职责清晰 业务逻辑与基础设施逻辑完全分离
测试友好 业务方法可独立测试,无需Mock锁

通过函数式编程和Lambda表达式,我们实现了真正的无侵入式设计,让代码更加优雅、简洁、易维护。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐