线程-支持可靠取消操作的生产者消费者日志服务

本文介绍了如何实现一个支持可靠取消操作的生产者消费者日志服务。从Bad Way到Good Way,逐步优化,通过使用计数器和阻塞队列的中断特性,确保在取消操作时不会丢失日志数据。最后总结了这种设计思想在并发编程中的应用。

今天看了下《并发编程实战》,觉得日志服务的生产消费者demo挺有趣的,故自己实现了下;

Bad Way

以下为第一种,较差的实现方式

package hpsyche.log;

import org.junit.Test;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

/**
 * @author Hpsyche
 */
public class BadLogWriter {
    private final BlockingQueue<String> queue;
    private final LoggerThread logger;
    private final static int CAPACITY = 3;

    public BadLogWriter() {
        this.queue = new LinkedBlockingQueue<String>(CAPACITY);
        this.logger = new LoggerThread();
    }

    public void start() {
        if (!logger.isAlive()) {
            logger.start();
        }
    }

    public void log(String msg) throws InterruptedException {
        System.out.println(msg);
        queue.put(msg.split(":")[1]);
    }

    public void shutdown() {
        logger.interrupt();
    }


    private class LoggerThread extends Thread {
        @Override
        public void run() {
            try {
                while (true) {
                    System.out.println("取出日志:" + queue.take());
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    @Test
    public void test() {
        BadLogWriter log = new BadLogWriter();
        log.start();
        int i = 1;
        try {
            while (true) {
                Thread.currentThread().sleep(2000);
                log.log("队列加入日志:" + (i++));
                if (i == 3) {
                    log.shutdown();
                }
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

如出现意外情况,导致日志线程中断(例题中在i==3时,关闭LoggerThread)此时存在两个问题:

  1. 因为生产者并不是一个专门的线程,难以同时取消此生产者和消费者,如以上案例会出现生产者一直生产,直到阻塞队列LinkedBlockingQueue满,无法解除阻塞状态;

    console输出如下

队列加入日志:1
取出日志:1
java.lang.InterruptedException
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1220)
队列加入日志:2
取出日志:2
	at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
	at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:439)
	at hpsyche.log.BadLogWriter$LoggerThread.run(BadLogWriter.java:43)
队列加入日志:3
队列加入日志:4
  1. 那些正在等待写入的日志直接丢失;

NOT BAD WAY

鉴于以上情况,我们考虑设置一个状态码,来标识“请求是否已关闭”,

package hpsyche.log;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

import org.junit.Test;

/**
 * @author Hpsyche
 */
public class LogWriter {
    private final BlockingQueue<String> queue;
    private final LoggerThread logger;
    private final static int CAPACITY = 3;
    private boolean isShutdown;


    public LogWriter() {
        this.queue = new LinkedBlockingQueue<String>(CAPACITY);
        this.logger = new LoggerThread();
    }

    public void start() {
        if (!logger.isAlive()) {
            logger.start();
        }
    }

    public void log(String msg) throws InterruptedException {
        if (!isShutdown) {
            System.out.println(msg);
            queue.put(msg.split(":")[1]);
        } else {
//            Thread.currentThread().sleep(6000);
            throw new IllegalStateException("日志已关闭");
        }
    }

    public void setShutdown(boolean isShutdown) {
        this.isShutdown = isShutdown;
    }

    private class LoggerThread extends Thread {
        @Override
        public void run() {
            try {
                while (true) {
//                    Thread.currentThread().sleep(2000);
                    System.out.println("取出日志:" + queue.take());
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    @Test
    public void test() {
        LogWriter log = new LogWriter();
        log.start();
        int i = 1;
        while (true) {
            try {
                //把日志放入队列
                log.log("队列加入日志:" + (i++));

                if (i == 3) {
                    log.setShutdown(true);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

在i==3时,设置关闭状态码,通过if判断isShutdown来实现对生产者线程的控制,避免了第一种情况中的生产者一直生产的问题;

但是,此时无法解决第二个问题,即会丢失未消费的阻塞队列中的数据,在LoggerThread中加入sleep来测试,如下:

private class LoggerThread extends Thread {
    @Override
    public void run() {
        try {
            while (true) {
                Thread.currentThread().sleep(2000);
                System.out.println("取出日志:" + queue.take());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

此时错误信息如下:

队列加入日志:1
队列加入日志:2

java.lang.IllegalStateException: 日志已关闭

	...........
	at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
	at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)

此时阻塞队列中的日志直接丢失,可以通过一些技巧来降低这种情况的概率,如在生产者线程结束前,等待一段时间,但是治标不治本,还是有可能导致线程发生故障。

GOOD WAY

在书中提到了一种方式:

由于阻塞队列take能响应中断,故可以通过一个计数器来“保持”日志的输出与存储,生产者需要给计数器递增计数,而消费者线程需要通过计数器来确保消费全部被消息,当输出线程中断时,让通过interrupt让take也中断,此时让计时器去判断是否还需要循环存储日志。

(注意:在计数器的递增、递减和判断过程,都需要加入同步来防止线程问题。)

具体实现如下;

package hpsyche.log;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

import org.junit.Test;

/**
 * 日志类添加可靠的取消操作
 *
 * @author xiaof
 */
public class GoodLogWriter {
    private final BlockingQueue<String> queue;
    private final LoggerThread loggerThread;
    private boolean isShutdown;
    //如果线程停止提交任务,线程不能停,要把剩余的任务提交结束
    private int reservations;
    private final static int CAPACITY = 500;

    public GoodLogWriter() {
        //队列长度
        this.queue = new LinkedBlockingQueue<String>(CAPACITY);
        this.loggerThread = new LoggerThread();
    }

    public void start() {
        //判断这个线程是否已经启动
        if (!loggerThread.isAlive()) {
            loggerThread.start();
        }
    }

    public void log(String msg) throws InterruptedException {
        //System.out.println("放入2:"+msg);
        //放入日志队列并阻塞队列
        synchronized (this) {
            if (isShutdown)
                //一旦shutdown,存储线程直接断开,并不会加入阻塞队列中
                throw new IllegalStateException("日志开关没有打开");
            ++reservations;
        }
        System.out.println("放入:"+msg);
        queue.put(msg);
    }

    public void stop() {
        synchronized (this) {
            isShutdown = true;
        }
        //准备中断存储线程(Thread.currentThread().sleep(100000);会抛出异常,
        //同时由于此时还阻塞队列还存在元素,继续循环一次,确保日志不会丢失
        loggerThread.interrupt();
    }

    private class LoggerThread extends Thread {
        public void run() {
            try {
                int i=0;
                while (true) {
                    i++;
                    try {
                        //对日志类上锁
                        synchronized (GoodLogWriter.this) {
                            //除了判断中断外,还需要reservations == 0
                            if (isShutdown && reservations == 0) {
                                System.out.println(666);
                                break;//停止线程
                            }
                        }
                        System.out.println("第"+i+"次准备存储");
                        if(i==2){
                            Thread.currentThread().sleep(100000);
                        }
                        //取出日志信息
                        String msg = queue.take();
                        System.out.println("存储:"+msg);
                        //提交成功一条,对阻塞的数据计数减少一条
                        synchronized (GoodLogWriter.this) {
                            --reservations;
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            } finally {
                System.out.println("日志结束..........");
            }
        }
    }

    @Test
    public void test() {
        GoodLogWriter log = new GoodLogWriter();
        log.start();
        int i = 1;
        while (true) {
            try {
                Thread.currentThread().sleep(2000);
                //把日志放入队列
                log.log("日志:" + i++);
                Thread.currentThread().sleep(2000);

                if (i == 4) {
                    log.stop();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

代码中已给出详细的注释,只要关注的是reservations的变化,console输出结果如下:

1次准备存储
放入:日志:1
存储:日志:12次准备存储
放入:日志:2
放入:日志:33次准备存储java.lang.InterruptedException: sleep interrupted
存储:日志:2
	at java.lang.Thread.sleep(Native Method)4次准备存储
	at hpsyche.log.GoodLogWriter$LoggerThread.run(GoodLogWriter.java:73)
存储:日志:3
666
日志结束..........

java.lang.IllegalStateException: 日志开关没有打开

	at hpsyche.log.GoodLogWriter.log(GoodLogWriter.java:40)
	at hpsyche.log.GoodLogWriter.test(GoodLogWriter.java:101)

在存储第三次后存储线程被阻塞,此时调用loggerThread.interrupt();使得线程抛出InterruptedException,但由于reservations并不为0,线程会继续循环,直至所有数据,所以我们可以看到:放入了三次日志,也成功存储了三次日志。

总结

其实这个demo并不难理解,感觉其中的思想挺有趣的,貌似阻塞队列其中的put、take操作已经足够,不需要我们再提供计数器去保持什么状态了,但在本文三次操作中,可以看到通过计数与阻塞队列结合,可以实现一定程度的可靠的线程取消操作。

<think>嗯,用户问的是如何用RabbitMQ渐进式取消超时订单。首先,我需要理解“渐进式”在这里是什么意思。可能是指逐步处理订单取消,而不是一次性全部处理。比如,先检查是否超时,再触发取消流程,可能还涉及多次尝试或分阶段操作。 接下来,用户可能是在电商系统中,订单创建后需要在一定时间内支付,否则自动取消。传统的做法可能是用定时任务定期扫描数据库,但这样效率不高,特别是在订单量大的时候。使用RabbitMQ的延迟队列可以更高效地处理这种场景,因为消息会在设定的延迟时间后被投递,消费者处理取消逻辑。 用户之前的问题涉及微信支付回调和延时取消订单,所以这次可能希望进一步优化取消流程,比如分阶段提醒用户支付,或者在取消前进行多次检查。比如,先在订单创建后发送一个延迟消息,30分钟后检查是否支付,如果未支付则发送提醒,再延迟15分钟取消订单。这样可以分步骤处理,而不是直接取消。 需要确认RabbitMQ如何实现延迟消息。通常有两种方式:使用插件(如rabbitmq_delayed_message_exchange)或者通过TTL+死信队列。如果用户环境支持插件,那更简单;否则需要配置TTL和死信交换机。这里可能需要详细说明两种方法的配置步骤。 然后,要考虑如何设计队列和交换机。比如,使用延迟交换机,设置不同的延迟时间,或者为不同阶段的消息分配不同的路由键。比如,第一次延迟30分钟发送提醒,第二次延迟45分钟实际取消订单。 消费者部分需要处理不同阶段的消息,可能需要根据消息内容判断当前阶段,执行相应的操作,比如发送提醒邮件或取消订单。同时需要考虑幂等性,避免重复处理同一消息。 另外,异常处理很重要。比如,消费者处理失败时,如何重试或者进入死信队列。手动确认模式(manual acknowledgment)可以确保消息只在处理完成后被确认,避免消息丢失。 还需要考虑如何监控和调试,比如查看队列中的消息数量,延迟时间是否正确,以及日志记录是否充分。这些帮助用户在生产环境中维护系统。 最后,用户可能想知道如何测试这个流程,比如如何模拟延迟消息,验证取消逻辑是否正确执行。或者如何在不中断服务的情况下调整延迟时间或流程步骤。 总结步骤:配置延迟交换机,发送不同阶段的延迟消息,消费者处理各阶段操作,确保幂等和可靠性,监控和异常处理。同时,给出示例代码和配置,帮助用户具体实现。</think>在电商系统中,使用 **RabbitMQ** 实现 **渐进式取消超时订单** 的核心思路是:**通过延迟消息分阶段触发订单状态检查、用户提醒、最终取消订单**,避免一次性直接取消带来的用户体验问题。以下是完整实现方案: --- ### **1. 架构设计概览** ``` 订单创建 → 发送延迟消息(阶段1:30分钟后提醒支付) → 再次发送延迟消息(阶段2:45分钟后取消订单) → 消费者处理阶段逻辑(检查状态、发送提醒、取消订单) ``` --- ### **2. 核心流程设计** #### **2.1 延迟消息实现方式** - **方式1:RabbitMQ延迟交换机插件(推荐)** 安装 `rabbitmq_delayed_message_exchange` 插件,支持直接设置消息延迟时间。 ```bash rabbitmq-plugins enable rabbitmq_delayed_message_exchange ``` - **方式2:TTL + 死信队列(兼容方案)** 设置消息的 `TTL`(存活时间),并绑定到死信队列。 ```java @Bean public Queue orderDelayQueue() { return QueueBuilder.durable("order.delay.queue") .withArgument("x-dead-letter-exchange", "dlx.exchange") // 死信交换机 .withArgument("x-message-ttl", 30 * 60 * 1000) // TTL(30分钟) .build(); } ``` --- ### **3. 渐进式取消订单实现步骤** #### **3.1 阶段1:发送支付提醒** - **订单创建后发送延迟消息(如30分钟)**,触发支付提醒。 ```java public void createOrder(String orderId) { // 1. 创建订单逻辑... // 2. 发送延迟消息(阶段1:30分钟后提醒支付) MessageProperties props = new MessageProperties(); props.setDelay(30 * 60 * 1000); // 延迟30分钟 rabbitTemplate.send("order.delay.exchange", "remind.key", ("Remind Payment: " + orderId).getBytes(), props); } ``` - **消费者处理支付提醒逻辑**: ```java @Component public class OrderReminderConsumer { @RabbitListener(queues = "order.remind.queue") public void remindPayment(byte[] message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) { String orderId = new String(message); // 1. 检查订单是否已支付 if (orderService.isPaid(orderId)) { channel.basicAck(deliveryTag, false); return; } // 2. 发送支付提醒(短信/邮件) sendPaymentReminder(orderId); // 3. 手动确认 channel.basicAck(deliveryTag, false); } } ``` #### **3.2 阶段2:取消未支付订单** - **在阶段1消息处理后发送第二个延迟消息(如再延迟15分钟)**。 ```java private void sendPaymentReminder(String orderId) { // 1. 发送提醒逻辑... // 2. 发送阶段2消息(延迟15分钟) MessageProperties props = new MessageProperties(); props.setDelay(15 * 60 * 1000); // 延迟15分钟 rabbitTemplate.send("order.cancel.exchange", "cancel.key", ("Cancel Order: " + orderId).getBytes(), props); } ``` - **消费者处理订单取消逻辑**: ```java @Component public class OrderCancelConsumer { @RabbitListener(queues = "order.cancel.queue") public void cancelOrder(byte[] message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) { String orderId = new String(message); // 1. 再次检查订单是否已支付(幂等性) if (orderService.isPaid(orderId)) { channel.basicAck(deliveryTag, false); return; } // 2. 取消订单(释放库存、更新状态) orderService.cancelOrder(orderId); // 3. 手动确认 channel.basicAck(deliveryTag, false); } } ``` --- ### **4. 关键设计点** #### **4.1 幂等性保障** - **通过Redis记录已处理的订单ID**,防止重复提醒或取消。 ```java private boolean checkProcessed(String orderId, String prefix) { String key = prefix + orderId; if (redisTemplate.hasKey(key)) { return true; // 已处理过 } redisTemplate.opsForValue().set(key, "1", 1, TimeUnit.DAYS); return false; } ``` #### **4.2 消息可靠性** - **持久化队列与消息**:声明队列和交换机时设置 `durable: true`。 - **手动确认模式**:消费者处理完成后调用 `basicAck`,失败时重试或进入死信队列。 #### **4.3 动态调整延迟时间** - **通过配置中心动态调整延迟时间**(如30分钟提醒,15分钟后取消)。 ```yaml order: payment-remind-delay: 30 # 提醒延迟(分钟) payment-cancel-delay: 15 # 取消延迟(分钟) ``` --- ### **5. 完整流程图** ``` +-------------------+ +-------------------+ +---------------------+ | 创建订单请求 | | Spring Boot | | RabbitMQ | | | | | | | | 创建订单 |---->| 发送阶段1消息 |---->| 延迟30分钟 | | | | | | | +-------------------+ +-------------------+ +----------+----------+ | v +-------------------+ | 消费者线程 | | | | 1. 检查支付状态 | | 2. 发送支付提醒 | +-------------------+ | v +-------------------+ | Spring Boot | | | | 发送阶段2消息 | | 延迟15分钟 | +-------------------+ | v +-------------------+ | 消费者线程 | | | | 1. 再次检查状态 | | 2. 取消未支付订单 | +-------------------+ ``` --- ### **6. 注意事项** 1. **延迟消息插件兼容性**: - 若生产环境无法安装插件,优先使用 **TTL + 死信队列** 实现。 2. **订单状态检查**: - 每次处理消息时必须 **双重检查订单状态**,避免因网络延迟导致的重复处理。 3. **异常重试机制**: - 配置消费者重试策略(如最大重试3次),失败后进入死信队列。 --- ### **7. 监控与优化** - **消息堆积监控**:通过RabbitMQ管理插件(`rabbitmq-plugins enable rabbitmq_management`)查看队列长度。 - **日志追踪**:记录消息处理耗时、失败原因,便于定位问题。 - **性能优化**: - 启动多个消费者线程并行处理消息: ```java @Bean public SimpleMessageListenerContainer remindContainer(ConnectionFactory connectionFactory) { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); container.setQueueNames("order.remind.queue"); container.setConcurrentConsumers(5); // 并发消费者数量 return container; } ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值