某些情况下,你可能有需要遍历某个topic下所有消息的需求,这可以通过1.4.5新增的TopicBrowser
来实现,一个简单的例子:
topic ; browser sessionFactorycreateTopicBrowser(topic); it browseriterator(); (ithasNext()) { msg itnext(); outprintln( (msggetData())); }
通过createTopicBrowser
即可创建一个topic browser,这个方法同时有一个重载版本
createTopicBrowser( topic, maxSize, timeout, timeUnit);
,可以设置请求的buffer大小,超时等。
TopicBrowser
可以多次复用,每次iterator
方法返回的都是一个全新的迭代器,它按照broker id,partition号的顺序遍历消息。注意,返回的迭代器不支持删除,无法删除消息。
TopicBrowser.getPartitions()
方法可以用来获取该topic下的所有分区信息。
TopicBrowser.shutdown()
用来关闭topic browser,释放资源。
https://github.com/killme2008/Metamorphosis/wiki/Spring-Supports
这里包括一些MetaQ的高级应用,比如使用log4j appender发送消息作为日志框架,Twitter storm集成以及发送顺序消息等。
在一些场景里,你可能希望消费者只消费一个topic下的部分满足特定要求的消息,而不是全部消费。通常,我会建议使用消息属性(attribute)来过滤消息,在MessageListener
接收到消息的时候,判断message.getAttribute()
返回是否符合要求来决定是否消费。也就是将过滤做到客户端。这样的代价是客户端还是会拉取不想消费的消息,浪费带宽。从1.4.6开始,MetaQ同时提供服务端和客户端过滤消息的接口ConsumerMessageFilter
,用来过滤消息。
具体见订阅消息MessageConsumer的消息过滤一节。
同样,你需要实现ConsumerMessageFilter
接口,并将你的实现打包成jar文件,放到服务器Broker的provided目录,接下来,配置server.ini文件,假设你的实现是com.xxxx.MyMessageFilter
类,你想为消费分组log-processor
过滤topic是log
下的消息,那么你应该这样配置:
[topic=log]
group.log-processor=com.xxxx.MyMessageFilter
配置之后,重启Broker,消息过滤将立即生效。
总结来说,服务端消息过滤需要五个步骤:
实现ConsumerMessageFilter
接口,实现你的消息过滤器。
打包实现成jar文件,可以用maven等构建工具,也可以用eclipse导出,如果你的过滤器实现用到了第三方库,也请一起打包进jar包,或者拷贝到服务器的provided目录。
将打包后的jar和依赖包,拷贝到服务器的provided目录。
配置server.ini,找到你想过滤的topic配置,添加group.xxx=MyFilter
,其中xxx
是你的消费分组名称,而MyFilter
就是你的过滤器实现类名。
重启Broker,过滤即时生效。
首先,MetaQ会尽量避免消息重复,每个topic的每个分区都只会被一个consumer消费,但是在consumer做负载均衡的过程中,可能因为consumer列表的变更,导致分区分配规则不一致,从而导致部分消息会被重复消费。这种情况可以通过下列手段来避免:
合理设置订阅的maxSize,这个缓冲区大小,最好只是略大于你的最大的消息大小(包括消息头部20个字节)。比如你的最大消息是1024字节,那么建议maxSize可以设置成1044字节以上。如果有消息属性,这个值还应该加上消息属性的长度,并加上4个字节的大小。
通过1.4.6引入的MessageIdCache
接口的消息缓冲来去重。通过将消费过的消息id在缓冲中标示为已经处理,来避免重复消费。
我们重点介绍下MessageIdCache
,这个接口如下:
package com.taobao.metamorphosis.client.consumer; /** * Message id cache to prevent duplicated messages for the same consumer group. * * @author dennis<[email protected]> * @since 1.4.6 * */ public interface MessageIdCache { /** * Added key value to cache * * @param key * @param exists */ public void put(String key, Byte exists); /** * Get value from cache,it the item is exists,it must be returned. * * @param key * @return */ public Byte get(String key); }
默认1.4.6版本有一个实现ConcurrentLRUHashMap
,使用LRU算法维护一个缓存map。默认启用这个实现,固定大小为4096,可以通过metaq.consumer.message_ids.lru_cache.size
环境变量修改这个大小。这个实现是全局共享的,也就是所有的MessageConsumer
都使用同一个缓存来做消息去重。
默认的这个实现仍然是JVM级别的去重,如果你的消费者是分布式的,那么可能需要一个集中式的全局缓冲来去重,比如在example里我们提供了一个基于memcached的实现:
package com.taobao.metamorphosis.example.cache; import java.util.concurrent.TimeoutException; import net.rubyeye.xmemcached.MemcachedClient; import net.rubyeye.xmemcached.exception.MemcachedException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import com.taobao.metamorphosis.client.consumer.MessageIdCache; public class MemcachedMessageIdCache implements MessageIdCache { private final MemcachedClient memcachedClient; private int expireInSeconds = 60; private static final Log log = LogFactory.getLog(MemcachedMessageIdCache.class); public MemcachedMessageIdCache(MemcachedClient client) { this.memcachedClient = client; } public void setExpireInSeconds(int expireInSeconds) { this.expireInSeconds = expireInSeconds; } public int getExpireInSeconds() { return this.expireInSeconds; } @Override public void put(String key, Byte exists) { try { this.memcachedClient.set(key, this.expireInSeconds, exists); } catch (MemcachedException e) { log.error("Added message id cache failed", e); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } catch (TimeoutException e) { log.error("Added message id cache timeout", e); } } @Override public Byte get(String key) { try { return this.memcachedClient.get(key); } catch (MemcachedException e) { log.error("Get item from message id cache failed", e); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } catch (TimeoutException e) { log.error("Get item from message id cache timeout", e); } return null; } }
默认会将处理过的消息保存在memcached里,并设置过期时间为1分钟。你也可以实现自己的MessageIdCache
,实现后通过SimpleFetchManager.setMessageIdCache
这个静态方法设置进去就可以使用你的实现。
使用数据库事务去重
如果你消费消息的目的是操作某个数据库,比如将消息的内容写入数据库,或者根据消息内容更新数据库。那么通过将offset存储到同一个数据库,在消费消息的同时更新offset到数据库,也可以实现消息去重。
我们已经提供了MysqlOffsetStorage
,你也可以实现其他数据库的offset存储,在MessageListener.recieveMessages
方法接收消息的时候,你可以通过SimpleFetchManager.currentTopicRegInfo
静态方法,获取当前消费消息的offset信息,并在一个事务里同时更新offset和消费消息。
参见 使用Log4j发送消息
Maven引用MetaQ storm spout:
<dependency> <groupId>com.taobao.metamorphosis</groupId> <artifactId>metamorphosis-storm-spout</artifactId> <version>1.4.6.2</version> </dependency>
一个示范性的Topology(在example工程里):
package com.taobao.metamorphosis.example.storm; import static com.taobao.metamorphosis.example.Help.initMetaConfig; import java.util.Map; import backtype.storm.Config; import backtype.storm.LocalCluster; import backtype.storm.task.OutputCollector; import backtype.storm.task.TopologyContext; import backtype.storm.topology.OutputFieldsDeclarer; import backtype.storm.topology.TopologyBuilder; import backtype.storm.topology.base.BaseRichBolt; import backtype.storm.tuple.Tuple; import com.taobao.metamorphosis.client.consumer.ConsumerConfig; import com.taobao.metamorphosis.storm.scheme.StringScheme; import com.taobao.metamorphosis.storm.spout.MetaSpout; public class TestTopology { public static class FailEveryOther extends BaseRichBolt { OutputCollector _collector; int i = 0; @Override public void prepare(Map map, TopologyContext tc, OutputCollector collector) { this._collector = collector; } @Override public void execute(Tuple tuple) { this.i++; if (this.i % 2 == 0) { this._collector.fail(tuple); } else { this._collector.ack(tuple); } } @Override public void declareOutputFields(OutputFieldsDeclarer declarer) { } } public static void main(String[] args) { TopologyBuilder builder = new TopologyBuilder(); builder.setSpout("spout", new MetaSpout(new MetaConfig(), new ConsumerConfig("storm-spout"), new StringScheme()), 10); builder.setBolt("bolt", new FailEveryOther()).shuffleGrouping("spout"); Config conf = new Config(); // Set the consume topic conf.put(MetaSpout.TOPIC, "neta-test"); // Set the max buffer size in bytes to fetch messages. conf.put(MetaSpout.FETCH_MAX_SIZE, 1024 * 1024); LocalCluster cluster = new LocalCluster(); cluster.submitTopology("test", conf, builder.createTopology()); } }
MetaSpout
接收三个参数,首先是MetaClientConfig
和ConsumerConfig
,这跟配置一个普通的消息消费者没有什么区别,具体见前面的章节。第三个参数scheme
除了用于declareOutputFields
之外,还用来反序列化MetaQ的消息data:
//MetaSpout.java @Override public void nextTuple() { if (this.messageConsumer != null) { try { final MetaMessageWrapper wrapper = this.messageQueue.poll(WAIT_FOR_NEXT_MESSAGE, TimeUnit.MILLISECONDS); if (wrapper == null) { return; } final Message message = wrapper.message; this.collector.emit(this.scheme.deserialize(message.getData()), message.getId()); } catch (final InterruptedException e) { // interrupted while waiting for message, big deal } } }
默认提供了一个StringScheme
:
package com.taobao.metamorphosis.storm.scheme; import backtype.storm.spout.Scheme; import backtype.storm.tuple.Fields; import backtype.storm.tuple.Values; import java.io.UnsupportedEncodingException; import java.util.List; public class StringScheme implements Scheme { public List<Object> deserialize(byte[] bytes) { try { return new Values(new String(bytes, "UTF-8")); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } public Fields getOutputFields() { return new Fields("str"); } }
声明output fields为str
,并且认为消息的data是一个字符串。
Topology需要配置订阅的topic
和fetchSize
,最终提交到storm集群。