分布式开发6大核心专题 掌握企业级分布式项目方案 | 完结

0 阅读5分钟

Java分布式系统解决方案 掌握企业级分布式项目方案

对于 Java 工程师而言,从 CRUD 开发者进阶为架构师,关键在于能否驾驭“企业级分布式”场景。高并发、高可用、数据一致性是面试中的必考题,也是决定薪资分水岭的核心技能。

本文将基于一个典型的 “秒杀抢购 + 订单处理” 业务场景,抽丝剥茧,通过实战代码演示三大核心方案:分布式锁解决超卖MQ 异步解耦削峰、以及分布式事务保证数据一致性

一、 高并发库存扣减:Redis 分布式锁方案

在秒杀场景下,大量用户同时抢购同一商品。如果不加锁,会导致库存超卖(卖出的数量 > 实际库存);如果直接用数据库行锁,数据库会瞬间崩溃。

方案核心:利用 Redis 的 SETNX(Set if Not Exists)特性实现互斥锁,并设置合理的过期时间防止死锁。为了解决业务执行时间超过锁过期时间的问题,我们引入**“看门狗”机制**自动续期。

1. Redis 分布式锁核心实现(Lua 脚本 + 看门狗)

java

复制

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

public class RedisLockService {
    
    private final StringRedisTemplate redisTemplate;
    
    // 锁续期线程
    private Thread renewThread;
    private volatile boolean isRenew = false;

    public RedisLockService(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    /**
     * 加锁 Lua 脚本:保证原子性
     * KEYS[1]: 锁的 key
     * ARGV[1]: 锁的 value (UUID)
     * ARGV[2]: 过期时间 (毫秒)
     */
    private static final String LOCK_SCRIPT =
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "   return redis.call('pexpire', KEYS[1], ARGV[2]); " +
            "else " +
            "   return redis.call('set', KEYS[1], ARGV[1], 'PX', ARGV[2], 'NX'); " +
            "end";

    /**
     * 尝试加锁(带看门狗自动续期)
     */
    public boolean tryLock(String key, long expireTime, long waitTime) {
        String value = UUID.randomUUID().toString();
        long start = System.currentTimeMillis();

        while (true) {
            // 执行加锁
            DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(LOCK_SCRIPT, Long.class);
            Long result = redisTemplate.execute(redisScript, Collections.singletonList(key), value, String.valueOf(expireTime));

            if (result != null && result == 1) {
                // 加锁成功,启动后台线程自动续期
                startRenewThread(key, value, expireTime);
                return true;
            }

            // 超时判断
            if (System.currentTimeMillis() - start > waitTime) {
                return false;
            }
            
            try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); }
        }
    }

    /**
     * 启动看门狗线程:每隔 expireTime/3 时间续期一次
     */
    private void startRenewThread(String key, String value, long expireTime) {
        renewThread = new Thread(() -> {
            while (isRenew) {
                try {
                    Thread.sleep(expireTime / 3);
                    // 简单的续期逻辑:如果锁还是自己的,就重置时间
                    Boolean isSuccess = redisTemplate.expire(key, expireTime, TimeUnit.MILLISECONDS);
                    if (!isSuccess) break; 
                } catch (InterruptedException e) {
                    break;
                }
            }
        });
        isRenew = true;
        renewThread.setDaemon(true);
        renewThread.start();
    }

    /**
     * 释放锁 Lua 脚本:保证只有锁的持有者才能释放
     */
    private static final String UNLOCK_SCRIPT =
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "   return redis.call('del', KEYS[1]); " +
            "else " +
            "   return 0; " +
            "end";

    /**
     * 释放锁
     */
    public void unlock(String key, String value) {
        isRenew = false; // 停止续期
        if (renewThread != null) renewThread.interrupt();
        
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(UNLOCK_SCRIPT, Long.class);
        redisTemplate.execute(redisScript, Collections.singletonList(key), value);
    }
}

二、 流量削峰与解耦:RabbitMQ 延时队列实战

用户下单后,系统不仅要扣减库存,还要发送短信、增加积分、更新物流等。如果同步执行,响应时间极长且系统脆弱。

方案核心:使用 RabbitMQ 将“下单”与“后续处理”解耦。特别演示 “订单超时取消” 功能,利用死信队列(DLX)+ TTL 实现延时任务。

2. 消息配置与生产者

java

复制

import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class OrderMqConfig {

    public static final String ORDER_EXCHANGE = "order.exchange";
    public static final String ORDER_QUEUE = "order.queue";
    public static final String ORDER_DELAY_EXCHANGE = "order.delay.exchange";
    public static final String ORDER_DELAY_QUEUE = "order.delay.queue";

    @Autowired
    private RabbitTemplate rabbitTemplate;

    // 1. 定义普通业务交换机与队列
    @Bean
    public DirectExchange orderExchange() {
        return new DirectExchange(ORDER_EXCHANGE);
    }

    @Bean
    public Queue orderQueue() {
        return QueueBuilder.durable(ORDER_QUEUE).build();
    }

    @Bean
    public Binding orderBinding() {
        return BindingBuilder.bind(orderQueue()).to(orderExchange()).with("order.success");
    }

    // 2. 定义延时队列 (基于 Dead Letter Exchange)
    @Bean
    public DirectExchange delayExchange() {
        return new DirectExchange(ORDER_DELAY_EXCHANGE);
    }

    @Bean
    public Queue delayQueue() {
        return QueueBuilder.durable(ORDER_DELAY_QUEUE)
                .withArgument("x-message-ttl", 30000) // 消息存活30秒
                .withArgument("x-dead-letter-exchange", ORDER_EXCHANGE) // 过期后转发到业务交换机
                .withArgument("x-dead-letter-routing-key", "order.timeout") // 路由键
                .build();
    }

    @Bean
    public Binding delayBinding() {
        return BindingBuilder.bind(delayQueue()).to(delayExchange()).with("order.create");
    }
    
    // 发送延时消息
    public void sendDelayOrder(String orderId) {
        rabbitTemplate.convertAndSend(ORDER_DELAY_EXCHANGE, "order.create", orderId);
        System.out.println("订单 " + orderId + " 已发送,30分钟后检查支付状态");
    }
}

三、 分布式事务:Seata TCC 模式实战

在微服务架构中,订单服务和库存服务属于不同的数据库。本地事务无法解决跨库一致性问题。

方案核心:使用 Seata 的 TCC(Try-Confirm-Cancel)模式。

  • Try:预留资源(库存冻结,余额冻结)。
  • Confirm:确认执行(库存扣减,余额扣款)。
  • Cancel:取消执行(库存回滚,余额解冻)。

3. 库存服务 TCC 接口实现

java

复制

import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;

@LocalTCC
public interface StockTccService {

    /**
     * Try 阶段:冻结库存
     * @TwoPhaseBusinessAction: 定义 TCC 的名称与提交/回滚方法名
     */
    @TwoPhaseBusinessAction(name = "stockTccAction", commitMethod = "commit", rollbackMethod = "rollback")
    boolean prepareDeductStock(
            @BusinessActionContextParameter(paramName = "productId") Long productId,
            @BusinessActionContextParameter(paramName = "count") Integer count,
            @BusinessActionContextParameter(paramName = "orderId") String orderId);

    /**
     * Confirm 阶段:真正扣减库存(删除冻结记录)
     */
    boolean commit(BusinessActionContext context);

    /**
     * Cancel 阶段:释放冻结库存(恢复可用库存)
     */
    boolean rollback(BusinessActionContext context);
}

// --- 实现类 ---
@Service
public class StockTccServiceImpl implements StockTccService {

    @Autowired
    private StockMapper stockMapper;
    @Autowired
    private StockFreezeMapper stockFreezeMapper;

    @Override
    @Transactional
    public boolean prepareDeductStock(Long productId, Integer count, String orderId) {
        // 1. 检查剩余库存是否足够
        Stock stock = stockMapper.selectById(productId);
        if (stock.getTotalStock() - stock.getFrozenStock() < count) {
            throw new RuntimeException("库存不足");
        }
        // 2. 增加冻结库存
        stockMapper.increaseFrozenStock(productId, count);
        // 3. 记录冻结日志(用于 Cancel 阶段幂等校验)
        StockFreeze freeze = new StockFreeze(orderId, productId, count);
        stockFreezeMapper.insert(freeze);
        return true;
    }

    @Override
    @Transactional
    public boolean commit(BusinessActionContext context) {
        String orderId = context.getActionContext("orderId").toString();
        Long productId = Long.parseLong(context.getActionContext("productId").toString());
        Integer count = Integer.parseInt(context.getActionContext("count").toString());

        // 幂等性检查:如果冻结记录不存在,说明已经 Commit 过了
        StockFreeze freeze = stockFreezeMapper.selectByOrderId(orderId);
        if (freeze == null) return true;

        // 1. 扣减真实库存
        stockMapper.deductStock(productId, count);
        // 2. 减少冻结库存
        stockMapper.decreaseFrozenStock(productId, count);
        // 3. 删除冻结记录
        stockFreezeMapper.deleteByOrderId(orderId);
        return true;
    }

    @Override
    @Transactional
    public boolean rollback(BusinessActionContext context) {
        String orderId = context.getActionContext("orderId").toString();
        Long productId = Long.parseLong(context.getActionContext("productId").toString());
        Integer count = Integer.parseInt(context.getActionContext("count").toString());

        // 幂等性检查
        StockFreeze freeze = stockFreezeMapper.selectByOrderId(orderId);
        if (freeze == null) return true; // 已回滚或未冻结

        // 1. 减少冻结库存(恢复)
        stockMapper.decreaseFrozenStock(productId, count);
        // 2. 删除冻结记录
        stockFreezeMapper.deleteByOrderId(orderId);
        return true;
    }
}

四、 总结:涨薪的关键在于体系化思维

上述代码不仅仅是 API 的调用,背后蕴含的是企业级开发的体系化思维

  1. 资源隔离与互斥:Redis 分布式锁解决了并发竞争问题,看门狗机制体现了对边界情况的考量。
  2. 异步与解耦:RabbitMQ 延时队列将复杂的业务逻辑在时间与空间上拆分,提升了系统的吞吐量与容错性。
  3. 一致性保证:TCC 模式虽然开发复杂,但它提供了最强的数据一致性保证,适用于资金、库存等核心敏感业务。

掌握这些方案,不仅能在面试中对答如流,更能让你在实际架构设计中游刃有余。这才是 Java 工程师突破薪资瓶颈的必由之路。