面试题答案
一键面试以 RabbitMQ 为例的解决方案及原理
- 幂等性设计
- 扣钱操作:在扣钱逻辑中,为每次扣钱请求生成唯一标识(如 UUID)。当接收到扣钱消息时,首先根据这个唯一标识查询数据库中是否已经执行过该扣钱操作。如果已执行过,则直接返回成功结果,不再重复扣钱。例如,在数据库中创建一个扣钱操作记录表,记录每次扣钱请求的唯一标识、执行状态等信息。
- 记录订单状态:同样为记录订单状态的操作生成唯一标识。当处理记录订单状态的消息时,先查询该标识对应的操作是否已执行。若已执行,跳过操作,保证订单状态不会被重复更新。这是因为幂等性操作多次执行和执行一次的效果相同,避免了消息重复消费导致的数据不一致问题。
- 事务机制
- RabbitMQ 事务:可以使用 RabbitMQ 的事务机制。在发送消息前开启事务,先执行扣钱操作,若扣钱成功,再发送记录订单状态的消息,最后提交事务。如果在任何环节出现异常,如网络故障导致扣钱成功但消息发送失败,可回滚事务,确保扣钱操作回退,保证数据一致性。例如:
Channel channel = connection.createChannel();
channel.txSelect();
try {
// 执行扣钱操作
boolean moneyDeducted = deductMoney();
if (moneyDeducted) {
channel.basicPublish("", "orderStatusQueue", null, "order status message".getBytes("UTF - 8"));
}
channel.txCommit();
} catch (Exception e) {
channel.txRollback();
// 处理异常
} finally {
channel.close();
}
但事务机制会影响性能,因为事务会阻塞消息的发送,直到事务提交或回滚。 3. 消息确认机制 - 生产者确认:RabbitMQ 支持生产者确认机制(publisher confirm)。生产者发送消息后,RabbitMQ 会返回一个确认消息,告知生产者消息是否成功到达 Broker。如果生产者未收到确认消息,可重新发送消息。例如:
Channel channel = connection.createChannel();
channel.confirmSelect();
channel.addConfirmListener(new ConfirmListener() {
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
// 消息成功发送
}
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
// 消息发送失败,重新发送
}
});
channel.basicPublish("", "paymentQueue", null, "payment message".getBytes("UTF - 8"));
- **消费者确认**:消费者在处理完消息(如完成扣钱和记录订单状态)后,向 RabbitMQ 发送确认消息。RabbitMQ 只有在收到确认消息后才会将该消息从队列中删除。如果消费者在处理消息过程中出现网络故障等异常情况,未发送确认消息,RabbitMQ 会认为消息未被成功处理,会重新将消息投递给其他消费者或在一定时间后再次投递给该消费者,确保消息不会丢失,从而保证支付操作各环节的一致性。
4. 本地消息表 - 设计思路:在应用的数据库中创建一个本地消息表,记录要发送到 RabbitMQ 的消息以及消息的状态(如待发送、已发送、已确认等)。在执行扣钱操作前,先向本地消息表插入一条记录,状态设为待发送。扣钱成功后,将消息发送到 RabbitMQ,并更新本地消息表的状态为已发送。当收到 RabbitMQ 的确认消息或消费者成功处理消息并返回确认后,更新本地消息表状态为已确认。如果消息发送失败或消费者处理失败,可根据本地消息表的记录进行重试。 - 重试机制:通过定时任务扫描本地消息表中状态为已发送但未确认的记录,重新发送消息到 RabbitMQ。例如,每隔 5 分钟扫描一次,最多重试 3 次,若重试 3 次仍失败,则记录异常日志,人工介入处理。这样可以在网络故障等异常情况下,保证消息最终能被成功处理,确保支付操作的一致性。