面试提问:在电商秒杀活动中,如何防止“超卖”现象的发生

前言

作为一名中高级开发者而言,针对目前的求职环境,单纯的背八股文已经无济于事了,更多的面试官更看重的是候选人在面对各类生产场景时的应对策略,逻辑思路以及实际经验,所以面试场景题就应运而生了。候选人需要透过各类场景理清底层本质,方能赢得面试官的青睐,顺利通过面试。

解题思路

像这种比较实际的生产问题,需要剖析这个问题背后的本质。这个问题的本质就是一个"在高并发高可用分布式环境中,如何保证数据的最终一致性"问题。

时间比较紧张的读者可以直接阅览面试回答话术技巧

理论基础

为什么我们要保证数据的最终一致性?

CAP理论

通常我们在架构分布式系统的时候,有3种特性选择:

  • Availability可用性(A)
  • Consistency一致性(C)
  • Partition Tolerance分区容错性(P)

目前我们的系统中,尚无法做到3种特性兼顾,我们通常的做法是3选2,通过其他技术手段来达到最后一种特性:

  • 高可用高并发场景:选择AP
  • 强一致性场景:选择CP
  • 最终一致性场景:在AP基础上,通过异步机制保证数据最终一致

BASE理论

  • Basically Available基本可用(BA):系统在正常情况下应保持可用,即使在部分故障或性能下降的情况下也能继续提供服务。这意味着系统应尽量避免完全不可用的情况,并保证基本的响应性能。
  • Soft State软状态(S):系统中的数据状态可以在一段时间内是不一致的,数据副本可能存在短暂的冲突或不同步。这种状态是暂时的,系统会通过后续的处理逐渐将数据状态调整为一致。
  • Eventually Consistent最终一致性(E):系统的数据最终会达到一致的状态,但在某个时间点上可能存在不一致的情况。不同节点的数据副本之间的同步需要一定的时间,最终一致性要求系统在一定时间范围内能够达到数据的一致性。

实现方案

我这里列举出我接触到的4种解决方案,如果有更好的方案欢迎补充,它们分别是:

  • 可靠消息最终一致性
  • TCC(Try-Confirm-Cancel)
  • Saga模式
  • 最大努力通知

可靠消息最终一致性(常用)

核心原理

通过消息队列异步解耦服务,确保消息可靠传递,最终实现跨服务数据一致性。

方式一:本地消息表

我们以订单业务为例,Service层负责记录订单信息,记录消息日志,异步发送事务信息。

Java 复制代码
@Slf4J
@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class OrderServiceImpl implements OrderService {
    
    private final OrderMapper orderMapper; 
    private final MessageLogMapper messageLogMapper;
    
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void createOrder(CreateOrderRequest req) {
        // <= 这里是逻辑代码
        // 1. 创建订单
        Order order = new Order();
        order.setCreateUserId(req.getCurrentUserId());
        order.setProductId(req.getProductId());
        order.setStatus(OrderStatus.CREATED);
        orderMapper.insert(order);
        // 2. 记录消息日志
        MessageLog meLog = new MessageLog();
        meLog.setMessageId(UUID.randomUUID().toString());
        meLog.setBusinessKey(String.valueOf(order.getId()));
        meLog.setMessageBody(JacksonUtil.toStr(order));
        meLog.setStatus(MessageLogStatus.PENDING); // <= 注意这个状态
        messageLogMapper.insert(meLog);
        // 3. 异步发送消息(事务提交后执行)
    }
}

关于异步发送消息可以使用定时任务或事件监听方式实现

  1. 事件监听方式
  • 手动控制(直接在//TODO部分添加以下代码)
Java 复制代码
TransactionSynchronizationManager.registerSynchronization(
    () -> sendAsyncMessage(meLog)
);
  • 使用@TransactionalEventListener事件监听方式
Java 复制代码
/**
 * 定义一个事件类
 */
@Getter
@AllArgsConstructor
public calss OrderCreateEvent {
    private MessageLog log; // 虽说是订单创建事件,其实处理的是消息日志
}
Java 复制代码
/**
 * 在业务代码发布事件
 */
@Slf4J
@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class OrderServiceImpl implements OrderService {
    // ...其他组件
    // 添加事件发布组件
    private final ApplicationEventPublisher eventPublisher;
    
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void createOrder(CreateOrderRequest req) {
        // ...其他代码不变
        // 3. 异步发送消息(事务提交后执行)
        eventPublisher.publishEvent(new OrderCreateEvent(meLog));
    }
}
Java 复制代码
/**
 * 监听事件(在事务提交之后执行)
 */
@Component
public class OrderEventListener {
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    @Async // 可选注解,视实际需求决定是否异步
    public void onOrderCreated(OrderCreateEvent event) {
        sendAsyncMessage(event)
    }
}

注意:sendAsyncMessage视实际技术栈决定,可以是Kafka,RoketMQ等异步中间件

  1. 定时任务方式
Java 复制代码
/**
 * 消息发送定时任务
 */
@Slf4J
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class MessageSendTask {
     private fianl MessageLogMapper messageLogMapper;
     private final RocketMQTemplate mqTemplate; // 这里以Ali的RocketMQ作为队列中间件为例
     
     @Scheduled()
     public void sendPendingMessage() {
         List<MessageLog> pendingMessageLogs = messageLogMapper.selectByStatus(MessageLogStatus.PENDING);
         if(Objects.nonNull(pendingMessageLogs) && pendingMessageLogs.size() > 0) {
             pendingMessageLogs.foreach(e -> {
                 try {
                     // RocketMQ发送消息
                     rocketMQTemplate.convertAndSend("order_topic", e.getMessageBody());
                 } catch(Exception e) {
                     // 如果发送失败,记录重试次数
                     if(e.getRetryCount() > 3) {
                         e.setStatus(MessageStatus.FAILED);
                     } else {
                         e.setRetryCount(e.getRetryCount() + 1);
                     }
                     log.error(e.getMessage(), e);
                 }
                 // 更新消息状态
                 e.setStatus(MessageStatus.SENT);
                 messageLogMapper.updateById(e);
             });
         }
     }
 }

接着就是队列消费端 消费消息,达到最终一致性的结果

Java 复制代码
/**
 * 依旧以RocketMQ作为消息队列中间件为例
 */
@Component
@RocketMQMessageListener(topic = "order_topic", consumerGroup = "inventory_group")
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class InventoryConsumer implements RocketMQListener<String> {
    private final InventoryService inventoryService;
    
    @Override
    public void onMessage(String message) {
        Order order = JacksonUtil.parseObject(message, Order.class);
        // 幂等处理:检查是否已处理
        if(inventoryService.isOrderProccessed(order.getId())) {
            return;
        }
        // 扣减库存
        inventoryService.decreaseStock(order.getProductId(), 1);
        // 记录处理日志
        inventoryService.recordOrderProccessed(order.getId());
    }
}

一句话总结:本地消息表方式优势在于本地事务保证业务操作与消息记录的原子性;消息可靠性高,支持重试;代码侵入性低,适合中小团队。

方式二:事物消息(RocketMQ特色)

Java 复制代码
/**
 * 订单业务层
 */
@Slf4J
@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class OrderService {
    private final RocketMQTemplate rocketMQTemplate;
    
    /**
     * 使用RocketMQ事务消息
     */
    public void createOrderWithTransactionMessage(OrderRequest req) {
        // 1. 构建订单
        Order order = buildOrder(req);
        // 2. 构建并发送Half Message
        TransactionMQProducer producer = rocketMQTemplate.getProducer();
        Message<String> message = MessageBuilder.withPayload(JacksonUtil.toStr(order))
                    .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.APPLICATION_JSON_VALUE)
                    .build();
        TransactionSendResult sendResult = producer.sendMessageInTransaction("order_topic", message, null);
        // 3. 根据本地事务执行器执行结果提交或回滚消息
    }
}
Java 复制代码
/**
 * 本地事务执行器
 */
@Slf4J
@RocketMQTransactionListener
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public calss OrderTransactionListener implements RocketMQLocalTransactionListener {
    private final OrderMapper orderMapper;
    
    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        try {
            // 执行本地事务:创建订单
            
        } catch(Exception e) {
            log.error(e.getMessage(), e)
            return RocketMQLocalTransactionState.ROLLBACK;
        }  
    }
    
    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
        // 回查本地事务状态
        Order order = JacksonUtil.parseObject(new String((byte[]) msg.getPayload()), Order.class);
        Order dbOrder = orderMapper.selectById(order.getId());
        return Objects.isNull(dbOrder) ? RocketMQLocalTransactionState.UNKNOWN : RocketMQLocalTransactionState.COMMIT;
    }
}

一句话总结:将整个订单的创建(包括但不限于库存删减等步骤)交给RocketMQ的事务去控制。

TCC(Try-Confirm-Cancel)

核心原理 :将分布式事务拆分成预留资源确认提交补偿回滚三个阶段。

XML 复制代码
<!--引入TCC需要的依赖-->
<dependency>
    <groupId>org.apache.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>2.5.0</version>
</denpendency>
Java 复制代码
/**
 * TCC接口定义
 */
public interface OrderTccService {
    /**
     * Try阶段:预留资源
     */
    @TwoPhaseBusinessAction(name="createOrder", commitMethod="confirm", rollbackMethod="cancel")
    boolean tryCreateOrder(@BusinessActionContextParameter(paramName="orderId") String orderId,
                           @BusinessActionCOntext(paramName="userId") String userId,
                           @BusinessActionContextParameter(paramName="productId") Long productId);
    
    /**
     * Confirm阶段:确认提交   
     */
    boolean confirm(BusinessActionContext context);
    
    /**
     * Cancel阶段:补偿回滚
     */
    boolean cancel(BusinessActionContext context);
}
Java 复制代码
/**
 * 订单服务TCC实现
 */
@Slf4J
@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class OrderTCCServiceImpl implements OrderTCCService {
    private final OrderMapper orderMapper;
    private final InventoryFeignClient inventoryClient;
    
    @Override
    @Transactional(rollback=Exceltion.class)
    public boolean tryCreateOrder(String orderId, String userId, Long productId) {
        // 1. 预留订单(状态为PENDING)
        Order order = buildOrder(orderId, userId, productId);
        order.setStatus(OrderStatus.PENDING);
        orderMapper.insert(order);
        // 2. 调用库存服务预留资源
        inventoryClient.tryReserveStock(productId, 1);
        return true;
    }
    
    @Override
    @Transanctional(rollback=Exception.class)
    pubilc boolean confirm(BusinessActionContext context) {
        String orderId = context.getActionContext("orderId");
        // 确认订单(状态改为CONFIRMED)
        Order order = orderMapper.selectById(orderId);
        order.setStatus(OrderStatus.CONFIRMED);
        orderMapper.updateById(order);
        // 确认库存扣减
        inventoryClient.confirmReserveStock(order.getProductId(), 1);
        return true;
    }
    
    @Override
    @Transactional(rollback=Exception.class)
    public boolean cancel(BusinessActionContext context) {
        String orderId = context.getActionContext("orderId");
        // 取消订单(状态标记为CANDELLED)
        orderMapper.deleteById(orderId);
        // 释放库存
        Long productId = context.getActionContext("productId");
        inventoryClient.calcelReserveStock(productId, 1);
        return true;
    }
}
Java 复制代码
/**
 * 库存服务TCC实现
 */
@Slf4J
@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class InventoryTCCServiceImpl implements InventoryTCCService {
    private final InventoryMapper inventoryMapper;
    
    @Override
    @Transactional(rollbackFor=Exception.class)
    pubic boolean tryReserveStock(Long productId, Integer count) {
        // 预留库存(冻结库存)
        Inventory inventory = inventoryMapper.selectById(productId);
        if (inventory.getAvailableStock() < count) {
            throw new BizException("库存不足");
        }
        inventory.setFrozenStock(inventory.getFrozenStock() + count);
        inventory.setAvailableStock(inventory.getAvailableStock() - count);
        inventoryMapper.updateById(inventory);
        return true;
    }
    
    @Override
    @Transactional(rollbackFor=Exception.class)
    public boolean cancelReserveStock(Long productId, Integer count) {
        // 释放库存(解冻)
        Inventory inventory = inventoryMapper.selectById(productId);
        inventory.setFrozenStock(inventory.getFrozenStock() - count);
        inventory.setAvailableStock(inventory.getAvailableStock() + count);
        inventoryMapper.updateById(inventory);
        return true;
    }
    
    @Override
    @Transactional(rollbackFor=Exception.class)
    public boolean confirmReserveStock(Long productId, Integer count) {
        // 释放库存并记录
        Inventory inventory = inventoryMapper.selectById(productId);
        inventory.setSoldStock(inventory.getSoldStock() + count);
        inventory.setFrozenStock(inventory.getFrozenStock() - count);
        inventoryMapper.updateById(inventory);
        return true;
    }
}

一句话总结:TCC适用于短事务场景,比如订单创建、支付扣款,可以完美适配最终一致性原则。

Saga模式(长事务)

核心原理:将长事务拆分为一系列本地事务(Step),每个Step执行完成后触发下一个Step;若某个Step失败,通过反向补偿事务回滚。

Java 复制代码
/**
 * Sage编排器
 */
@Slf4J
@Component
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class OrderSagaOrchestrator {
    private final OrderService orderService;
    private final InventoryService inventoryService;
    private final PaymentService paymentService;
    private final LogisticsService logisticsService;
    
    public void executeOrderSage(OrderRequest req) {
        String sagaId = UUID.randomUUID().toString();
        try {
            // Step 1:创建订单
            Order order = orderService.createOrder(req);
            recordSagaStep(sagaId, "CREATE_ORDER", order.getId());
            // Step 2:扣减库存
            inventoryService.decreaseStock(order.getProductId(), 1);
            recordSagaStep(sagaId, "DECREASE_STOCK", order.getId());
            // Step 3:支付
            paymentService.pay(order.getId, order.getAmount());
            recordeSagaStep(sagaId, "PAYMENT", order.getId());
            // Step 4:创建物流单
            logisticsService.createLogistics(order.getId());
            recordSagaStep(sagaId, "CREATE_LOGISTICS", order.getId());
        } catch(Exception e) {
            log.error(e.getMessage(), e);
            // 执行补偿
            compensateSaga(sagaId);
        }
    }
    
    /**
     * 补偿Saga
     */
    private void compensateSaga(String sagaId) {
        List<SagaStep> steps = getSataSteps(sagaId);
        // 逆序Saga步骤
        Collections.reverse(steps);
        steps.foreach(step -> {
            try {
                String bizKey = step.getBusinessKey();
                switch (step.getStepName) {
                    "CREATE_LOGISTICS" -> logisticsService.cancelLogistics(bizKey);
                    "PAYMENT" -> paymentService.refund(bizKey);
                    "DECREASE_STOCK" -> inventoryService.increaseStock(getProductIdByOrderId(bizKey), 1);
                    "CREATE_ORDER"-> orderService.cancelOrder(bizKey);
                }
            } catch(Exception e) {
                log.error("compensate fail: {} {}", step.getStepName , e.getMessage(), e)
            }
        });
    }
}

一句话总结:Saga模式适用于长事务场景,如订单全部流程,业务步骤较多,对数据实时性不敏感。

最大努力通知

核心原理:通过定时任务不断重试,直到操作成功或达到最大重试次数。

Java 复制代码
/**
 * 最大努力通知服务
 */
@Slf4J
@Component
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class MostEffortNotificationService {
    private final NotificationLogMapper notificationLogMapper;
    private final RestTemplate restTemplate;
    
    /**
     * 发送通知
     */
    public void sendNotification(String targetUrl, String payload) {
        NotificationLog nLog = new NotificationLog();
        nLog.setTargetUrl(targetUrl);
        nLog.setPayload(payload);
        nLog.setStatus(NotificationStatus.PENDING);
        nLog.setRetryCount(0);
        nLog.setMaxRetryCount(10);
        nLog.setNextRetryTime(new Date());
        notificationLogMapper.insert(log);
    }
    
    /**
     * 通知重试定时任务
     */
    @Scheduled(fixedDelay=5000)
    public void retryNotifications() {
        List<NotificationLog> pendingLogs = notificationLogMapper.selectPendingNotifications(new Date());
        pendingLogs.foreach(l -> {
            try {
                // 发送Http请求
                HttpHeaders headers = new HttpHeaders();
                headers.setContentType(MediaType.APPLICATION_JSON);
                HttpEntity<String> entity = new HttpEntity<>(l.getPayload(), headers);
                ResponseEntity<String> res = restTemplate.postForEntity(l.getTargetUrl, entity, String.class);
                if (res.getStatusCode.is2xxSuccessful()) {
                    // 通知成功
                    l.setStatus(NotificationStatus.SUCCESS);
                    notificationLogMapper.updateById(l);
                } else {
                    // 通知失败,增加重试次数
                    handleRetry(l);
                }
            } catch(Exception e) {
                log.error(e.getMessage(), e);
                handleRetry(l);
            }
        });
    }
    
    private void handleRetry(NotificationLog nLog) {
        nLog.setRetryCount(nLog.getRetryCount() + 1);
        if (nLog.getRetryCount() >= nLog.getMaxRetryCount()) {
            // 达到最大重试次数,标记为失败
            nLog.setStatus(NotificationStatus.FAILED);
        } else {
            // 计算下次重试时间(指数规避)
            long delay = (long) Math.pow(2, log.getRetryCount()) * 1000;
            nLog.setNextRetryTime(new Date(System.currentTimeMillis() + delay));
        }
        notificationLogMapper.updateById(nLog);
    }
}

关键技术

幂等性设计(!!)

为什么需要幂等性设计?

  • 网络超时导致的重复请求;
  • 消息队列的重复消费;
  • 用户误操作的重复提交;

实现方案

  • 数据库的唯一索引
Java 复制代码
/**
 * 数据库设计:订单表
 */
@Data
@Table(name = "order")
public class Order {
    @TableId(type = IdType.ASSIGN_UUID)
    private String id;
    @TableField
    private String requestId; // 数据库添加唯一索引
    ... // 其他字段
}
Java 复制代码
@Slf4J
@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class OrderServiceImpl implements OrderService {
    private final OrderMapper orderMapper;
    
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Order createOrder(CreateOrderRequest req) {
        // 1. 检查请求是否已处理
        Order existOrder = orderMapper.selectByRequestId(req.getRequestId);
        if (Objects.isNull(existOrder)) {
            return existOrder;
        }
        // 2. 创建订单
        Order order = new Order();
        order.setId(UUID.randomUUID().toString);
        order.setRequestId(req.getRequestId); // 设置幂等键
        order.setUserId(req.getUserId);
        order.setProductId(req.getProductId);
        order.setStatus(OrderStatus.CREATED);
        return order;
    }
}
  • Token机制
Java 复制代码
/**
 * 幂等Token管理器
 */
@Component
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class IdempotentTokenManager {
    private final RedisTemplate<String, String> redis;
    
    private static final String TOKEN_PREFIX = "idempotent:token:";
    private static final long TOKEN_EXPIRE_SECONDS = 300;
    
    /**
     * 生成Token
     */
    public String generateToken() {
        String token = UUID.randomUUID().toString();
        redis.opsForValue().set(
            TOKEN_PREFIX + token, 
            "1", 
            TOKEN_EXPIRE_SECONDS, 
            TimeUnit.SECONDS
        );
        return token;
    }
    
    /**
     * 验证并消耗Token
     */
    public boolean validateAndConsume(String token) {
        String key = TOKEN_PREFIX + token;
        // 下面的代码也可以用Lua脚本处理,高并发中更可靠
        Boolean exists = redis.hasKey(key);
        if (Objects.equals(Boolean.TRUE, exists)) {
            // 原子删除
            return redis.delete(key);
        }
        return false;
    }
}
Java 复制代码
/**
 * 创建幂等注解
 */
@Target(ElementType.Method)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
    String key() default "";
}
Java 复制代码
/**
 * 创建幂等切面
 */
@Aspect
@Component
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class IdempotentAspect {
    private final IdempotentTokenManager tokenMgr;
    
    @Arround("@annotation(idempotent)")
    public Object arround(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Trowable {
        // 获取请求中的Token
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        String token = request.getHeader("X-Idempotent-Token");
        if (StringUtils.isBlank(token)) {
            throw new BizException("缺少幂等Token");
        }
        // 验证Token
        if (!tokenMgr.validateAndConsume(token)) {
            throw new BizException("请求无效,重复请求");
        }
        return joinPoint.proceed();
    }
}
Java 复制代码
/**
 * Token机制使用示例
 */
@Slf4J
@RestController
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class OrderController {
    private final IdempotentTokenManager tokenMgr;
    private final OrderService orderService;
    
    /**
     * 在需要幂等操作的业务接口调用前,先获取Token
     */
    @GetMapper("/token")
    public String getToken() {
        return tokenMgr.generateToken();
    }
    
    /**
     * 幂等创建订单
     */
    @Idempotent
    @PostMapping("/order")
    public Order createOrder(@RequestBody CreateOrderRequest req) {
        return orderService.createOrder(req);
    }
}
  • 版本号机制(数据库乐观锁)
Java 复制代码
/**
 * 数据库设计库存表
 */
@Data
@Table(name = "inventory")
public class Inventory {
    
    @TableId(type = IdType.AUTO)
    private Long id;
    private Long productId;
    private Integer stock;
    @Version
    private Integer version; // 版本号
    ... // 其他字段
}
Java 复制代码
/**
 * 库存服务
 */
@Slf4J
@Service
@RequiredArgsConstructor(onConstructor = @_(@Autowired))
public class InventoryServiceImpl implements InventoryService {
    private final InventoryMapper inventoryMapper;
    
    /**
     * 乐观锁扣减库存
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean decreaseStock(Long productId, Integer count) {
        Inventory inventory = inventoryMapper.selectByProductId(productId);
        if (inventory.getStock() < count) {
            throw new BizException("库存不足");
        }
        // 通过乐观锁更新
        int updated = inventoryMapper.decreaseStockWIthVersion(
            productId,
            count,
            inventory.getVersion()
        );
        if (updated == 0) {
            throw new BizException("库存扣减失败,请重试");
        }
        return true;
    }
}
  • 分布式锁的应用
Java 复制代码
/**
 * 使用Redis作为分布式锁是目前比较常见的方案
 */
@Component
@RequiredArgsConstructor(onConstructor = @_(@Autowired))
public class DistributionLock {
    private final RedisTemplate<String, String> redis;
    
    private static final String LOCK_PREFIX = "lock:";
    private static final long LOCK_EXPIRE_SECONDS = 30;
    
    /**
     * 获取锁
     */
    public boolean tryLock(Stirng lockKey, String requestId, long expireTime) {
        String key = Lock_PREFIX + lockKey;
        // 使用 SETNX + EXPIRE 原子操作
        return redis.opsForValue().setIfAbsent(
            key,
            requestId,
            expireTime,
            TimeUnit.SECONDS
        );
    }
    
    /**
     * 释放锁(Lua脚本保证原子性操作)
     */
    public boolean releaseLock(String lockKey, String requestId) {
        String key = LOCK_PREFIX + lockKey;
        String script = """
            if redis.call('get', KEYS[1]) == ARGV[1] then
                return redis.call('del', KEYS[1])
            else 
                return 0
            end
        """;
        DefaultREdisScript<Long> reddisScript = new DefaultRedisScript<>();
        redisScript.setScriptText(script);
        redisScript.setResultType(Long.class);
        Long result = redis.execute(redisScript, Collections.singletonList(key), requestId);
        return result != null && result == 1;
    }
}
Java 复制代码
/**
 * 分布式锁使用示例
 */
@Slf4J
@Service
@RequiredArgsConstructor(onConstructor = @_(@Autowired))
public class OrderServiceImpl implements OrderService {
    private final DistributedLock distributedLock;
    private final OrderMapper orderMapper;
    
    /**
     * 创建订单(分布式锁保证幂等)
     */
    @Override
    public Order createOrder(CreateOrderRequest req) {
        String lockKey = "order:create:" + request.getUserId();
        String requestId = req.getRequestId();
        
        try {
            // 获取锁
            if (!distributedLock.tryLock(lockKey, requestId, 10)) {
                throw new BizException("操作进行中,请稍后重试");
            }
            
            // 检查是否已创建订单
            Order existingOrder = orderMapper.selectByRequestId(requestId);
            if (Objects.nonNull(existingOrder)) {
                return existingOrder;
            }
            // 创建订单
            Order order = new Order();
            order.setId(UUID.randomUUID().toString());
            order.setRequestId(requestId);
            order.setUserId(req.getUserId);
            order.setStatus(OrderStatus.CREATED);
            orderMapper.insert(order);
            return order;
        } finally {
            // 释放锁
            distributedLock.releaseLock(lockKey, requestId);
        }
    }
}

数据校验与对账

Java 复制代码
/**
 * 数据对账服务
 */
@Slf4J
@Service
@RequiredArgsConstructor(onConstructor = @_(@Autowired))
public class DataReconciliationServiceImpl implements DataReconciliationService {
    private final OrderMapper orderMapper;
    private final InventoryMapper inventoryMapper;
    private final PaymentMapper paymentMapper;
    
    /**
     * 对账定时任务
     */
    @Overide
    @Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行
    public void reconcileTaskJob() {
        List<Order> paidOrders = orderMapper.selectPaidOrdersYesterday();
        reconcileOrderAndInventory(paidOrders);
        
    }
    
    /**
     * 订单与库存对账
     */
    private void reconcileOrderAndInventory(List<Order> orders) {
        // 每个产品已支付的订单集合
        Map<Long, List<order>> paidProductOrders = orders.stream().collect(Collectors.groupingBy(Order::getProductId));
        List<Inventory> inventories = inventoryMapper.selectByProductIds(paidProductOrders.keySet());
        Map<Long, Inventory> inventoryMap = inventories.stream().collect(Collectors.toMap(Inventory::getProduct, Function.identity()));
        paidProductOrders.foreach((k, v) -> {
            // 检查库存是否已扣减
            Inventory inventory = inventoryMap.get(v);
            if (Objects.nonNull(inventory)) {
                Integer expectedStock = inventory.getTotalStock() - v.size();
                if (!Objects.equals(inventory.getAvailableStock(), expectedStock)) {
                    log.warn("库存数量不一致:productId={}, expected={}, actual={}", k, inventory.getAvailableStock(), expectedStock);
                    //TODO 这里可以插入一些告警服务来提醒管理员进行人工审查
                }
            } else {
                log.warn("订单库存不存在:productId={}", k);
                //TODO 这里可以插入一些告警服务来提醒管理员进行人工审查
            }
        });
    }
    
    /**
     * 订单与支付对账
     */
    private void reconcileOrderAndPayment(List<Order> orders) {
        Set<String> orderIdSet = orders.stream().collect(Collectors.toSet(Order::getId));
        List<Payment> payments = paymentMapper.selectByOrderIds(orderIdSet);
        Map<String, Payment> paymentMap = payments.stream().collect(Collectors.toMap(Payment::getOrderId, Function.identity()));
        orders.foreach(o -> {
            Payment payment = paymentMap.get(o.getId());
            if (Objects.nonNull(payment)) {
                // 这里忽略了优惠券等情况的处理
                if (!Objects.equals(payment.getAmount, o.getAmount())) {
                    log.warn("订单金额与支付金额不一致:orderId={},orderAmount={},paymentAmount={}", o.getId(), o.getAmount(), payment.getAmount());
                    //TODO 进入告警机制,等待人工处理 
                }
            } else {
                log.warn("订单无支付记录,orderId={}", o.getId());
                //TODO 进行自动补偿,或人工审核后做出相应处理
            }
        });
    }
}

监控与告警

核心原理 :通过Grafana等一些日志采集工具采集日志中打印的错误数据信息,反馈给管理员或者业务员进行后期处理,具体的采集配置不在这里列出。

链路追踪技术

链路追踪技术 可以配合Saga模式一起使用,兼容性很高。

Java 复制代码
/**
 * 分布式链路追踪器
 */
@Component
@RequiredArgsConstructor(onConstructor = @_(@Autowired))
public class TransactionTracer {
    // SkyWalking \ Zipkin
    private final Tracer tracer;
    
    /**
     * 记录事务步骤
     */
    public void recordStep(String transactionId, String stepName, String status) {
        Span span = tracer.buildSpan(stepName)
            .withTag("transaction.id", transactionId)
            .withTag("step.name", stepName)
            .withTag("step.status", status)
            .start();
        span.finish();
    }
}
Java 复制代码
/**
 * 链路器使用示例
 */
@Service
@RequiredArgsConstructor(onConstructor = @_(@Autowired))
public class OrderService {
    private final TransactionTracer tracer;
    
    @Override
    public void createOrder(CreateOrderRequest req) {
        String transactionId = UUID.randomUUID().toString();
        
        try {
            tracer.recordStep(transactionId, "CREATE_ORDER", "START");
            // 创建订单
            Order order = doCreateOrder(req);
            tracer.recordStep(transactionId, "CREATE_ORDER", "SUCCESS);
            // 发送消息
            tracer.recordStep(transactionId, "SEND_MESSAGE", "START");
            sendMessage(order);
            tracer.recordStep(transactionId, "SEND_MESSAGE", "SUCcESS");
        } catch(Exception e) {
            tracer.recordStep(transactionId, "CREATE_ORDER", "FAILURE");
            throw e;
        }
    }
}

面试回答话术技巧

当面试官问出这类最终一致性相关的问题,你要清楚以下几点:

  1. 在这个体系中你熟悉什么?
  2. 你用到了什么技术,这个技术是你必须熟悉的,实操过的?
  3. 你还知道其他相关技术可以实现吗?

首先 ,你必须懂得CAP理论最终一致性体系就是选择了AP模型,在可用性和分区容错的基础上,通过异步机制保证最终一致性。这符合BASE理论的思想。

其次 ,可以展开说说你项目中用到的什么技术方案去实现的,怎么实现的,遇到过什么样的问题,怎么解决的?因为现在最终一致性的技术实现已经很完善,面试官通常还很看重你解决问题的能力。上述实现方案中提到的5种方式(本地消息表消息中间件事务消息TCC模式Saga模式最大努力通知(不推荐))读者可以挑一个自己最熟悉,最有经验的去展开详说。

再次 ,如果你在回答过程中,提到了幂等性技术分布式锁等关键技术,并且能够详细描述他们的使用场景,这是一个加分项。

最后 ,你再说一下为整个最终一致性体系建立了完善的监控告警机制,包括消息积压,事务失败率,补偿失败等关键指标的监控。

举个例子 :在我负责的电商平台中,订单创建设计订单、库存、积分三个服务。我们采用本地消息表 + 幂等设计的方案:

  • 订单服务在本地事务中创建订单并记录消息;
  • 定时任务发送消息到RocketMQ;
  • 库存和积分服务消费消息,通过唯一索引保证幂等;
  • 建立对账任务,每天凌晨校验数据一致性;

通过这套方案,我们实现了99.99%的数据一致性,且系统吞吐量提升3倍。

相关推荐
青柠代码录4 小时前
【Redis】缓存击穿
后端
uzong4 小时前
《企业IT架构转型之道:阿里巴巴中台战略思想与架构实战》从业务痛点到架构革命,企业转型的底层逻辑(精华解读)
后端·架构
计算机学姐4 小时前
基于SpringBoot的在线学习网站平台【个性化推荐+数据可视化+课程章节学习】
java·vue.js·spring boot·后端·学习·mysql·信息可视化
南囝coding4 小时前
Claude Code 多 Agent 协作:Subagents 和 Agent Teams 怎么选?
前端·后端
uzong4 小时前
《大型网站技术架构》-大型网站技术架构背后的系统性思维(精华解读)
后端·架构
星晨雪海4 小时前
Spring Boot 常用注解
java·spring boot·后端
Leinwin4 小时前
实战教程:3步接入Azure OpenAI调用GPT-5,国内IP直连
后端·python·flask
rrrjqy4 小时前
深入浅出 RAG:基于 Spring AI 的文档分块 (Chunking) 策略详解与实战
java·人工智能·后端·spring
二月龙5 小时前
Python 异常处理机制:从基础语法到自定义异常的实战指南
后端