RocketMQ事务消费和顺序消费详解

一、RocketMq有3中消息类型

1.普通消费

2. 顺序消费

3.事务消费

  • 顺序消费场景

在网购的时候,我们需要下单,那么下单需要假如有三个顺序,第一、创建订单 ,第二:订单付款,第三:订单完成。也就是这个三个环节要有顺序,这个订单才有意义。RocketMQ可以保证顺序消费。

  • rocketMq实现顺序消费的原理

produce在发送消息的时候,把消息发到同一个队列(queue)中,消费者注册消息监听器为MessageListenerOrderly,这样就可以保证消费端只有一个线程去消费消息

注意:是把把消息发到同一个队列(queue),不是同一个topic,默认情况下一个topic包括4个queue

单个节点(Producer端1个、Consumer端1个)

1、Producer.java 

package order;  

import java.util.List;  

import com.alibaba.rocketmq.client.exception.MQBrokerException;
import com.alibaba.rocketmq.client.exception.MQClientException;
import com.alibaba.rocketmq.client.producer.DefaultMQProducer;
import com.alibaba.rocketmq.client.producer.MessageQueueSelector;
import com.alibaba.rocketmq.client.producer.SendResult;
import com.alibaba.rocketmq.common.message.Message;
import com.alibaba.rocketmq.common.message.MessageQueue;
import com.alibaba.rocketmq.remoting.exception.RemotingException;  

/**
 * Producer,发送顺序消息
 */
public class Producer {
    public static void main(String[] args) {
        try {
            DefaultMQProducer producer = new DefaultMQProducer("order_Producer");
            producer.setNamesrvAddr("192.168.100.145:9876;192.168.100.146:9876;192.168.100.149:9876;192.168.100.239:9876");  

            producer.start();  

            // String[] tags = new String[] { "TagA", "TagB", "TagC", "TagD",
            // "TagE" };  

            for (int i = 1; i <= 5; i++) {  

                Message msg = new Message("TopicOrderTest", "order_1", "KEY" + i, ("order_1 " + i).getBytes());  

                SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
                    public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                        Integer id = (Integer) arg;
                        int index = id % mqs.size();
                        return mqs.get(index);
                    }
                }, 0);  

                System.out.println(sendResult);
            }  

            producer.shutdown();
        } catch (MQClientException e) {
            e.printStackTrace();
        } catch (RemotingException e) {
            e.printStackTrace();
        } catch (MQBrokerException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}  

2、Consumer.java

package order;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import com.alibaba.rocketmq.client.consumer.DefaultMQPushConsumer;
import com.alibaba.rocketmq.client.consumer.listener.ConsumeOrderlyContext;
import com.alibaba.rocketmq.client.consumer.listener.ConsumeOrderlyStatus;
import com.alibaba.rocketmq.client.consumer.listener.MessageListenerOrderly;
import com.alibaba.rocketmq.client.exception.MQClientException;
import com.alibaba.rocketmq.common.consumer.ConsumeFromWhere;
import com.alibaba.rocketmq.common.message.MessageExt;  

/**
 * 顺序消息消费,带事务方式(应用可控制Offset什么时候提交)
 */
public class Consumer1 {  

    public static void main(String[] args) throws MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("order_Consumer");
        consumer.setNamesrvAddr("192.168.100.145:9876;192.168.100.146:9876;192.168.100.149:9876;192.168.100.239:9876");  

        /**
         * 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费<br>
         * 如果非第一次启动,那么按照上次消费的位置继续消费
         */
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);  

        consumer.subscribe("TopicOrderTest", "*");  

        consumer.registerMessageListener(new MessageListenerOrderly() {
            AtomicLong consumeTimes = new AtomicLong(0);  

            public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
                // 设置自动提交
                context.setAutoCommit(true);
                for (MessageExt msg : msgs) {
                    System.out.println(msg + ",内容:" + new String(msg.getBody()));
                }  

                try {
                    TimeUnit.SECONDS.sleep(5L);
                } catch (InterruptedException e) {  

                    e.printStackTrace();
                }
                ;  

                return ConsumeOrderlyStatus.SUCCESS;
            }
        });  

        consumer.start();  

        System.out.println("Consumer1 Started.");
    }  

}  

结果如下图所示:

这个五条数据被顺序消费了

  • 多个节点(Producer端1个、Consumer端2个)

Producer.java

package order;  

import java.util.List;  

import com.alibaba.rocketmq.client.exception.MQBrokerException;
import com.alibaba.rocketmq.client.exception.MQClientException;
import com.alibaba.rocketmq.client.producer.DefaultMQProducer;
import com.alibaba.rocketmq.client.producer.MessageQueueSelector;
import com.alibaba.rocketmq.client.producer.SendResult;
import com.alibaba.rocketmq.common.message.Message;
import com.alibaba.rocketmq.common.message.MessageQueue;
import com.alibaba.rocketmq.remoting.exception.RemotingException;  

/**
 * Producer,发送顺序消息
 */
public class Producer {
    public static void main(String[] args) {
        try {
            DefaultMQProducer producer = new DefaultMQProducer("order_Producer");
            producer.setNamesrvAddr("192.168.100.145:9876;192.168.100.146:9876;192.168.100.149:9876;192.168.100.239:9876");  

            producer.start();  

            // String[] tags = new String[] { "TagA", "TagB", "TagC", "TagD",
            // "TagE" };  

            for (int i = 1; i <= 5; i++) {  

                Message msg = new Message("TopicOrderTest", "order_1", "KEY" + i, ("order_1 " + i).getBytes());  

                SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
                    public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                        Integer id = (Integer) arg;
                        int index = id % mqs.size();
                        return mqs.get(index);
                    }
                }, 0);  

                System.out.println(sendResult);
            }
            for (int i = 1; i <= 5; i++) {  

                Message msg = new Message("TopicOrderTest", "order_2", "KEY" + i, ("order_2 " + i).getBytes());  

                SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
                    public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                        Integer id = (Integer) arg;
                        int index = id % mqs.size();
                        return mqs.get(index);
                    }
                }, 1);  

                System.out.println(sendResult);
            }
            for (int i = 1; i <= 5; i++) {  

                Message msg = new Message("TopicOrderTest", "order_3", "KEY" + i, ("order_3 " + i).getBytes());  

                SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
                    public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                        Integer id = (Integer) arg;
                        int index = id % mqs.size();
                        return mqs.get(index);
                    }
                }, 2);  

                System.out.println(sendResult);
            }  

            producer.shutdown();
        } catch (MQClientException e) {
            e.printStackTrace();
        } catch (RemotingException e) {
            e.printStackTrace();
        } catch (MQBrokerException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}  

Consumer1.java

/**
 * 顺序消息消费,带事务方式(应用可控制Offset什么时候提交)
 */
public class Consumer1 {  

    public static void main(String[] args) throws MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("order_Consumer");
        consumer.setNamesrvAddr("192.168.100.145:9876;192.168.100.146:9876;192.168.100.149:9876;192.168.100.239:9876");  

        /**
         * 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费<br>
         * 如果非第一次启动,那么按照上次消费的位置继续消费
         */
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);  

        consumer.subscribe("TopicOrderTest", "*");  

        /**
         * 实现了MessageListenerOrderly表示一个队列只会被一个线程取到
         *,第二个线程无法访问这个队列
         */
        consumer.registerMessageListener(new MessageListenerOrderly() {
            AtomicLong consumeTimes = new AtomicLong(0);  

            public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
                // 设置自动提交
                context.setAutoCommit(true);
                for (MessageExt msg : msgs) {
                    System.out.println(msg + ",内容:" + new String(msg.getBody()));
                }  

                try {
                    TimeUnit.SECONDS.sleep(5L);
                } catch (InterruptedException e) {  

                    e.printStackTrace();
                }
                ;  

                return ConsumeOrderlyStatus.SUCCESS;
            }
        });  

        consumer.start();  

        System.out.println("Consumer1 Started.");
    }  

}  

Consumer2.java

/**
 * 顺序消息消费,带事务方式(应用可控制Offset什么时候提交)
 */
public class Consumer2 {  

    public static void main(String[] args) throws MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("order_Consumer");
        consumer.setNamesrvAddr("192.168.100.145:9876;192.168.100.146:9876;192.168.100.149:9876;192.168.100.239:9876");  

        /**
         * 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费<br>
         * 如果非第一次启动,那么按照上次消费的位置继续消费
         */
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);  

        consumer.subscribe("TopicOrderTest", "*");  

        /**
         * 实现了MessageListenerOrderly表示一个队列只会被一个线程取到
         *,第二个线程无法访问这个队列
         */
        consumer.registerMessageListener(new MessageListenerOrderly() {
            AtomicLong consumeTimes = new AtomicLong(0);  

            public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
                // 设置自动提交
                context.setAutoCommit(true);
                for (MessageExt msg : msgs) {
                    System.out.println(msg + ",内容:" + new String(msg.getBody()));
                }  

                try {
                    TimeUnit.SECONDS.sleep(5L);
                } catch (InterruptedException e) {  

                    e.printStackTrace();
                }
                ;  

                return ConsumeOrderlyStatus.SUCCESS;
            }
        });  

        consumer.start();  

        System.out.println("Consumer2 Started.");
    }  

}  

先启动Consumer1和Consumer2,然后启动Producer,Producer会发送15条消息
Consumer1消费情况如图,都按照顺序执行了

Consumer2消费情况如图,都按照顺序执行了

二、事务消费

这里说的主要是分布式事物。下面的例子的数据库分别安装在不同的节点上。

事物消费需要先说说什么是事务。比如说:我们跨行转账,从工商银行转到建设银行,也就是我从工商银行扣除1000元之后,我的建设银行也必须加1000元。这样才能保证数据的一致性。假如工商银行转1000元之后,建设银行的服务器突然宕机,那么我扣除了1000,但是并没有在建设银行给我加1000,就出现了数据的不一致。因此加1000和减1000才行,减1000和减1000必须一起成功,一起失败。

再比如,我们进行网购的时候,我们下单之后,订单提交成功,仓库商品的数量必须减一。但是订单可能是一个数据库,仓库数量可能又是在另个数据库里面。有可能订单提交成功之后,仓库数量服务器突然宕机。这样也出现了数据不一致的问题。

使用消息队列来解决分布式事物:

现在我们去外面饭店吃饭,很多时候都不会直接给了钱之后直接在付款的窗口递饭菜,而是付款之后他会给你一张小票,你拿着这个小票去出饭的窗口取饭。这里和我们的系统类似,提高了吞吐量。即使你到第二个窗口,师傅告诉你已经没饭了,你可以拿着这个凭证去退款,即使中途由于出了意外你无法到达窗口进行取饭,但是只要凭证还在,可以将钱退给你。这样就保证了数据的一致性。

如何保证凭证(消息)有2种方法:

1、在工商银行扣款的时候,余额表扣除1000,同时记录日志,而且这2个表是在同一个数据库实例中,可以使用本地事物解决。然后我们通知建设银行需要加1000给该用户,建设银行收到之后给我返回已经加了1000给用户的确认信息之后,我再标记日志表里面的日志为已经完成。

2、通过消息中间件

原文地址:http://www.jianshu.com/p/453c6e7ff81c

RocketMQ第一阶段发送Prepared消息时,会拿到消息的地址,第二阶段执行本地事物,第三阶段通过第一阶段拿到的地址去访问消息,并修改消息的状态。

细心的你可能又发现问题了,如果确认消息发送失败了怎么办?RocketMQ会定期扫描消息集群中的事物消息,如果发现了Prepared消息,它会向消息发送端(生产者)确认,Bob的钱到底是减了还是没减呢?如果减了是回滚还是继续发送确认消息呢?RocketMQ会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。

例子:

Consumer.java

package transaction;  

import java.util.List;  

import com.alibaba.rocketmq.client.consumer.DefaultMQPushConsumer;
import com.alibaba.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import com.alibaba.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import com.alibaba.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import com.alibaba.rocketmq.client.exception.MQClientException;
import com.alibaba.rocketmq.common.consumer.ConsumeFromWhere;
import com.alibaba.rocketmq.common.message.MessageExt;  

/**
 * Consumer,订阅消息
 */
public class Consumer {  

    public static void main(String[] args) throws InterruptedException, MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("transaction_Consumer");
        consumer.setNamesrvAddr("192.168.100.145:9876;192.168.100.146:9876;192.168.100.149:9876;192.168.100.239:9876");
        consumer.setConsumeMessageBatchMaxSize(10);
        /**
         * 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费<br>
         * 如果非第一次启动,那么按照上次消费的位置继续消费
         */
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);  

        consumer.subscribe("TopicTransactionTest", "*");  

        consumer.registerMessageListener(new MessageListenerConcurrently() {  

            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {  

                try {  

                    for (MessageExt msg : msgs) {
                        System.out.println(msg + ",内容:" + new String(msg.getBody()));
                    }  

                } catch (Exception e) {
                    e.printStackTrace();  

                    return ConsumeConcurrentlyStatus.RECONSUME_LATER;// 重试  

                }  

                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;// 成功
            }
        });  

        consumer.start();  

        System.out.println("transaction_Consumer Started.");
    }
}  

Producer.java

package transaction;  

import com.alibaba.rocketmq.client.exception.MQClientException;
import com.alibaba.rocketmq.client.producer.SendResult;
import com.alibaba.rocketmq.client.producer.TransactionCheckListener;
import com.alibaba.rocketmq.client.producer.TransactionMQProducer;
import com.alibaba.rocketmq.common.message.Message;  

/**
 * 发送事务消息例子
 *
 */
public class Producer {
    public static void main(String[] args) throws MQClientException, InterruptedException {  

        TransactionCheckListener transactionCheckListener = new TransactionCheckListenerImpl();
        TransactionMQProducer producer = new TransactionMQProducer("transaction_Producer");
        producer.setNamesrvAddr("192.168.100.145:9876;192.168.100.146:9876;192.168.100.149:9876;192.168.100.239:9876");
        // 事务回查最小并发数
        producer.setCheckThreadPoolMinSize(2);
        // 事务回查最大并发数
        producer.setCheckThreadPoolMaxSize(2);
        // 队列数
        producer.setCheckRequestHoldMax(2000);
        producer.setTransactionCheckListener(transactionCheckListener);
        producer.start();  

        // String[] tags = new String[] { "TagA", "TagB", "TagC", "TagD", "TagE"
        // };
        TransactionExecuterImpl tranExecuter = new TransactionExecuterImpl();
        for (int i = 1; i <= 2; i++) {
            try {
                Message msg = new Message("TopicTransactionTest", "transaction" + i, "KEY" + i,
                        ("Hello RocketMQ " + i).getBytes());
                SendResult sendResult = producer.sendMessageInTransaction(msg, tranExecuter, null);
                System.out.println(sendResult);  

                Thread.sleep(10);
            } catch (MQClientException e) {
                e.printStackTrace();
            }
        }  

        for (int i = 0; i < 100000; i++) {
            Thread.sleep(1000);
        }  

        producer.shutdown();  

    }
}  

TransactionExecuterImpl .java --执行本地事务

package transaction;  

import com.alibaba.rocketmq.client.producer.LocalTransactionExecuter;
import com.alibaba.rocketmq.client.producer.LocalTransactionState;
import com.alibaba.rocketmq.common.message.Message;  

/**
 * 执行本地事务
 */
public class TransactionExecuterImpl implements LocalTransactionExecuter {
    // private AtomicInteger transactionIndex = new AtomicInteger(1);  

    public LocalTransactionState executeLocalTransactionBranch(final Message msg, final Object arg) {  

        System.out.println("执行本地事务msg = " + new String(msg.getBody()));  

        System.out.println("执行本地事务arg = " + arg);  

        String tags = msg.getTags();  

        if (tags.equals("transaction2")) {
            System.out.println("======我的操作============,失败了  -进行ROLLBACK");
            return LocalTransactionState.ROLLBACK_MESSAGE;
        }
        return LocalTransactionState.COMMIT_MESSAGE;
        // return LocalTransactionState.UNKNOW;
    }
}  

TransactionCheckListenerImpl--未决事务,服务器回查客户端(目前已经被阉割啦)

package transaction;  

import com.alibaba.rocketmq.client.producer.LocalTransactionState;
import com.alibaba.rocketmq.client.producer.TransactionCheckListener;
import com.alibaba.rocketmq.common.message.MessageExt;  

/**
 * 未决事务,服务器回查客户端
 */
public class TransactionCheckListenerImpl implements TransactionCheckListener {
    // private AtomicInteger transactionIndex = new AtomicInteger(0);  

    //在这里,我们可以根据由MQ回传的key去数据库查询,这条数据到底是成功了还是失败了。
    public LocalTransactionState checkLocalTransactionState(MessageExt msg) {
        System.out.println("未决事务,服务器回查客户端msg =" + new String(msg.getBody().toString()));
        // return LocalTransactionState.ROLLBACK_MESSAGE;  

        return LocalTransactionState.COMMIT_MESSAGE;  

        // return LocalTransactionState.UNKNOW;
    }
}  

producer端:发送数据到MQ,并且处理本地事物。这里模拟了一个成功一个失败。Consumer只会接收到本地事物成功的数据。第二个数据失败了,不会被消费。

Consumer只会接收到一个,第二个数据不会被接收到

时间: 2024-10-11 04:06:13

RocketMQ事务消费和顺序消费详解的相关文章

(转)CAS (4) —— CAS浏览器SSO访问顺序图详解(CAS Web Flow Diagram by Example)

CAS (4) —— CAS浏览器SSO访问顺序图详解(CAS Web Flow Diagram by Example) tomcat版本: tomcat-8.0.29 jdk版本: jdk1.8.0_65 nginx版本: nginx-1.9.8 cas版本: cas4.1.2cas-client-3.4.1 参考来源: jasig.github.io:CAS protocol CAS (1) —— Mac下配置CAS到Tomcat(服务端) CAS (2) —— Mac下配置CAS到Tomc

【转】web.xml 中的listener、 filter、servlet 加载顺序及其详解

web.xml 中的listener. filter.servlet 加载顺序及其详解 原文链接 http://www.cnblogs.com/JesseV/archive/2009/11/17/1605015.html 在项目中总会遇到一些关于加载的优先级问题,近期也同样遇到过类似的,所以自己查找资料总结了下,下面有些是转载其他人的,毕竟人家写的不错,自己也就不重复造轮子了,只是略加点了自己的修饰. 首先可以肯定的是,加载顺序与它们在 web.xml 文件中的先后顺序无关.即不会因为 filt

web.xml 中的listener、 filter、servlet 加载顺序及其详解

转自:http://zhxing.iteye.com/blog/399668 在项目中总会遇到一些关于加载的优先级问题,近期也同样遇到过类似的,所以自己查找资料总结了下,下面有些是转载其他人的,毕竟人家写的不错,自己也就不重复造轮子了,只是略加点了自己的修饰. 首先可以肯定的是,加载顺序与它们在 web.xml 文件中的先后顺序无关.即不会因为 filter 写在 listener 的前面而会先加载 filter.最终得出的结论是:listener -> filter -> servlet 同

(转载)web.xml 中的listener、 filter、servlet 加载顺序及其详解

首先可以肯定的是,加载顺序与它们在 web.xml 文件中的先后顺序无关.  但不会因为 filter 写在 listener 的前面而会先加载 filter.  最终得出的结论是:listener -> filter -> servlet 同时还存在着这样一种配置节:context-param,它用于向 ServletContext 提供键值对,即应用程序上下文信息.我们的 listener, filter 等在初始化时会用到这些上下文中的信息,那么 context-param 配置节是不是

(转)web.xml 中的listener、 filter、servlet 加载顺序及其详解

在项目中总会遇到一些关于加载的优先级问题,近期也同样遇到过类似的,所以自己查找资料总结了下,下面有些是转载其他人的,毕竟人家写的不错,自己也就不重复造轮子了,只是略加点了自己的修饰. 首先可以肯定的是,加载顺序与它们在 web.xml 文件中的先后顺序无关.即不会因为 filter 写在 listener 的前面而会先加载 filter.最终得出的结论是:listener -> filter -> servlet 同时还存在着这样一种配置节:context-param,它用于向 Servlet

转:web.xml 中的listener、 filter、servlet 加载顺序及其详解

在项目中总会遇到一些关于加载的优先级问题,刚刚就遇到了一个问题,由于项目中使用了quartz任务调度,quartz在web.xml中是使用listener进行监听的,使得在tomcat启动的时候能马上检查数据库查看那些任务未被按时执行,而数据库的配置信息在是在web.xml中使用servlet配置的,导致tomcat启动后在执行quartz任务时报空指针,原因就是servlet中的数据库连接信息未被加载.网上查询了下web.xml中配置的加载优先级: 首先可以肯定的是,加载顺序与它们在 web.

web.xml 中的listener、 filter、servlet 加载顺序及其详解【转】

在项目中总会遇到一些关于加载的优先级问题,刚刚就遇到了一个问题,由于项目中使用了quartz任务调度,quartz在web.xml中是使用listener进行监听的,使得在tomcat启动的时候能马上检查数据库查看那些任务未被按时执行,而数据库的配置信息在是在web.xml中使用servlet配置的,导致tomcat启动后在执行quartz任务时报空指针,原因就是servlet中的数据库连接信息未被加载.网上查询了下web.xml中配置的加载优先级: 首先可以肯定的是,加载顺序与它们在 web.

web.xml 中元素加载顺序及其详解

一.概述 1.启动一个WEB项目的时候,WEB容器会去读取它的配置文件web.xml,读取<listener>和<context-param>两个结点. 2.紧接着,容器创建一个ServletContext(servlet上下文),这个web项目的所有部分都将共享这个上下文. 3.容器将<context-param>转换为键值对,并交给servletContext. 4.容器创建<listener>中的类实例,创建监听器 二 . load-on-startu

C/C++---printf/cout 从右至左压栈顺序实例详解

__cdecl压栈顺序实例 明白计算:计算是从右到左计算的 栈和寄存器变量:x++,是将计算结果存放到栈空间,最后是要出栈的:而++x和x是将计算结果直接存放到某个寄存器变量中(是同一个),所以计算完最后输出时,++x和x的结果总是相同的. ??用个小例子来说明下: int x=5; printf("%d %d\n",x,x++); int y=5; printf("%d %d\n",y++,y); int z=5; printf("%d %d %d\n&