内容平台消息队列异步静态化实现
一、重构前系统面对的问题
随着分公司站点的建设,后台稿件的数量越来越多,加上自动签发的需求,
大量的静态化请求非常容易造成系统宕机,于是引入消息队列做异步处理,平滑请求处理曲线。
二、静态化消息发送端工程
首先并不是全部的静态化请求都需要通过消息队列来处理,在项目配置文件中添加了一个static_mq的参数控制是否静态化。
(1)处理请求,并对静态化相关的数据进行封装
收到静态化请求后,后台首先进行判断,是否开启消息队列静态化,
确认后实例化一个MQ的线程,将相关的静态化信息保存到线程信息中,然后将线程提交给线程池调度,如果未开启消息队列,则按照原有的逻辑进行静态化。
以首页静态化为例:
1 2 3 4 5 6 7 8 9 | String isMQStatic = ConfUtil.getString( "static_mq" ); if (isMQStatic != null && "true" .equals(isMQStatic.trim())) { // 如果启用了消息队列,则发起一个mq的线程执行 IndexStaticPageThread indexStaticPage = new IndexStaticPageThread(site.getId(), jmsTemplate); // 将线程提交到线程池,提高执行的效率 StaticThreadPool.threadPool().execute(indexStaticPage); } else { staticPageSvc.index(site); } |
栏目和稿件的类似,但是需要将静态化的信息添加到线程实例中,下面分别是栏目消息封装:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | if (isMQStatic != null && "true" .equals(isMQStatic.trim())) { // 增加MQ的消息发送方式 begin Map<Object, Object> paramMap = new HashMap<Object, Object>(); paramMap.put( "siteId" , site.getId()); paramMap.put( "contextPath" , request.getContextPath()); paramMap.put( "isStaticFirst" , staticFirst); paramMap.put( "isContainChild" , containChild); paramMap.put( "startDate" , startDate); ChannelStaticPageThread channelStaticPage = new ChannelStaticPageThread(channelId, jmsTemplate, paramMap); StaticThreadPool.threadPool().execute(channelStaticPage); // 增加MQ的消息发送方式 end } else { count = staticPageSvc.channel(staticFirst, site.getId(), channelId, startDate, containChild, request); } |
稿件消息封装:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | if (isMQStatic != null && "true" .equals(isMQStatic.trim())) { Map<Object, Object> paramMap = new HashMap<Object, Object>(); paramMap.put( "siteId" , siteId); paramMap.put( "startDate" , startDate); paramMap.put( "endDate" , endDate); paramMap.put( "needRegenerateAll" , needRegenerateAll); paramMap.put( "subCommand" , "pages" ); paramMap.put( "isContainChild" , true ); ChannelStaticPageThread channelStaticPage = new ChannelStaticPageThread(channelId, jmsTemplate, paramMap); StaticThreadPool.threadPool().execute(channelStaticPage); } else { count = staticPageSvc.content(siteId, channelId, startDate, endDate, needRegenerateAll); } |
(2)发送消息到ActiveMQ中
消息线程通过传入的静态化信息构造并且实例化以后,在线程的执行体中做具体的消息绑定和发送工作。
下面以ChannelStaticPageThread为例来看一下具体是如何对消息队列进行封装和发送的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | public class ChannelStaticPageThread implements Runnable { private static final Logger log = LoggerFactory.getLogger(ChannelStaticPageThread. class ); private Integer channelId; private JmsTemplate jmsTemplate; private Map<Object, Object> paramMap; private boolean needRegenerateAll, isStaticFirst, isContainChild = false ; // 构造方法 public ChannelStaticPageThread(Integer channelId, JmsTemplate jmsTemplate, Map<Object, Object> paramMap) { this .channelId = channelId; this .jmsTemplate = jmsTemplate; this .paramMap = paramMap; } public void run() { log.info( "thread...send message,channel static,channel_id:" + channelId); // 需要根据不同的情形,调用不同的queue String QueueName = "xhCloud.STATIC.AUTO" ; Integer siteId = (Integer) paramMap.get( "siteId" ); String contextPath = (String) paramMap.get( "contextPath" ); Date startDate = (Date) paramMap.get( "startDate" ); Date endDate = (Date) paramMap.get( "endDate" ); if (paramMap.get( "isStaticFirst" ) != null ) isStaticFirst = (Boolean) paramMap.get( "isStaticFirst" ); if (paramMap.get( "isContainChild" ) != null ) isContainChild = (Boolean) paramMap.get( "isContainChild" ); if (paramMap.get( "needRegenerateAll" ) != null ) needRegenerateAll = (Boolean) paramMap.get( "needRegenerateAll" ); // 用包装过的taskNode发送 ChannelTaskEntity taskEntity = new ChannelTaskEntity(); taskEntity.setCommand( "channel" ); String subCommand = (String) paramMap.get( "subCommand" ); if (subCommand != null && subCommand.trim().length() > 0 ) { taskEntity.setSubCommand(subCommand); } else { taskEntity.setSubCommand( "default" ); } taskEntity.setChannelId(channelId); taskEntity.setSiteId(siteId); taskEntity.setContextPath(contextPath); taskEntity.setStaticFirst(isStaticFirst); taskEntity.setContainChild(isContainChild); taskEntity.setStartDate(startDate); taskEntity.setEndDate(endDate); taskEntity.setRegenerateAll(needRegenerateAll); InstantTaskNode<ChannelTaskEntity> instTaskNode = new InstantTaskNode<ChannelTaskEntity>(); instTaskNode.setMainPriority(TaskMainPriority.MEDIUM); instTaskNode.setTaskType(TaskType.INSTANT); instTaskNode.setEntity(taskEntity); jmsTemplate.convertAndSend(QueueName, instTaskNode); log.info( "send success!channelid:" + channelId + " subCommand:" + subCommand); } } |
这里的JmsTemplate来自于org.springframework.jms.core.JmsTemplate,
是一个消息传递的标准,具体的分析可以查看博文。
为了区分消息的优先级,在MQ中创建了若干个不同的队列,
不同的任务通过不同的消息队列发送;
1 2 3 4 | jms.queue. static .auto=xhCloud.STATIC.AUTO jms.queue. static .medium=xhCloud.STATIC.MEDIUM jms.queue. static .high=xhCloud.STATIC.HIGH jms.queue. static .critical=xhCloud.STATIC.CRITICAL |
(3)对线程池的处理
代码中维护了一个用于处理静态化操作的线程池,
是通过java.util.concurrent.ThreadPoolExecutor来创建的。
看一下具体的代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | public class StaticThreadPool { private static int corePoolSize; // 池中所保存的线程数,包括空闲线程 private static int maximumPoolSize; // 池中允许的最大线程数 private static int keepAliveTime; // 当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。 private static int workQueue; // 执行前用于保持任务的队列。此队列仅保持由 execute 方法提交的 Runnable // 任务。 private static ThreadPoolExecutor executor = null ; private static String resource = "activeMQStaticThreadPool" ; private static ResourceBundle rb = null ; public static ThreadPoolExecutor threadPool() { if (rb == null ) { rb = ResourceBundle.getBundle(resource); if (rb != null ) { corePoolSize = Integer.parseInt(rb.getString( "corePoolSize" )); maximumPoolSize = Integer.parseInt(rb.getString( "maximumPoolSize" )); keepAliveTime = Integer.parseInt(rb.getString( "keepAliveTime" )); workQueue = Integer.parseInt(rb.getString( "workQueue" )); } else { System.out.println( "thread pool property initialized failed!" ); } } if (executor == null ) { System.out.println( " Initialize StaticThreadPool Executor" ); executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(workQueue), new ThreadPoolExecutor.AbortPolicy()); } return executor; } } |
可以看到通过读取配置文件的形式设置了线程池的相关参数,
下面是具体的配置文件:
1 2 3 4 5 6 7 8 | #线程池维护线程的最少数量 corePoolSize= 10 #线程池维护线程的最大数量 maximumPoolSize= 50 #线程池维护线程所允许的空闲时间 keepAliveTime= 60000 #线程池所使用的缓冲队列数 workQueue= 10 |
线程池的任务队列使用了java.util.concurrent.ArrayBlockingQueue,
这是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
在代码中添加了一个threadPool()的静态方法,
通过传入配置文件中的参数,返回ThreadPoolExecutor实例,
使用了如下的构造方法:
1 | new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, milliseconds,runnableTaskQueue, handler); |
下面简单对参数进行说明:
corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。
runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。
maximumPoolSize(线程池最大大小):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是如果使用了无界的任务队列这个参数就没什么效果。
ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。
RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。
以下是JDK1.5提供的四种策略。
AbortPolicy:直接抛出异常。
CallerRunsPolicy:只用调用者所在线程来运行任务。
DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
DiscardPolicy:不处理,丢弃掉。
当然也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化不能处理的任务。
keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。
TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。
另外这里线程池的创建可以看做是单例的应用。
(4)编写并提供首页/栏目/详情页等静态化的Dubbo接口
根据目前系统中对静态化请求类型的划分,对外提供了
staticIndexPageService、staticChannelPageService、
staticContentPageService等静态化服务。
下面是截取provider.xml的配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <!-- 用dubbo协议在20880端口暴露服务 --> < dubbo:protocol name="dubbo" port="${dubbo.port}" /> <!-- 使用zookeeper注册中心暴露服务地址 --> < dubbo:registry protocol="zookeeper" address="192.168.75.138:2181,192.168.75.139:2181,192.168.75.140:2181,192.168.75.141:2181,192.168.75.142:2181"/> <!-- 静态化服务 --> < dubbo:service interface="com.bingyue.dubbo.StaticChannelPageService" ref="staticChannelPage" timeout="50000" version="2.2.006" /> < bean id="staticChannelPage" class="com.xinhuanet.statictpl.dubbo.impl.StaticChannelPageServiceImpl" /> < dubbo:service interface="com.bingyue.dubbo.StaticIndexPageService" ref="staticIndexPage" timeout="50000" version="2.2.006" /> < bean id="staticIndexPage" class="com.bingyue.dubbo.impl.StaticIndexPageServiceImpl" /> |
简单看一下Dubbo接口的编写:
1 2 3 | public interface StaticContentPageService { public TaskEntity[] staticizeContentPage(Integer contentId, boolean isCheckedBeforePublished, boolean isBatchChange) throws StaticPageException, UnrecoverableException; } |
这个Dubbo接口是对原有静态化服务的封装,最终的实现还是通过项目最初的静态化服务。
三、静态化消息处理端工程
回顾完了静态化消息发送端的设计和实现,再来看一下消费端工程是如何实现的。
消费端工程是一个Maven管理的多项目聚合工程,加上后期的功能扩展,形成了现在一个较大的工程:
(1)父工程下各个Maven项目的作用如下:
static-beans:
主要封装了基础的消息实体,主要是TaskEntity等几个抽象类,自定义了程序执行期间相关的异常,依赖static-utils项目。
下面以TaskEntity为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | public abstract class TaskEntity implements Serializable { private final static Logger logger = LoggerFactory .getLogger(TaskEntity. class ); private static final long serialVersionUID = 6139912897589483499L; final ObjectMapper mapper = ObjectMapperFactory.get(); TaskEntityType entityType; String command; // 静态化执行类型 String subCommand; // 静态化的二级执行类型,对于属性页静态化的任务,即为属性的类型 public String getSubCommand() { return subCommand; } public void setSubCommand(String subCommand) { this .subCommand = subCommand; } public String getCommand() { return command; } public void setCommand(String command) { this .command = command; } public TaskEntityType getEntityType() { return entityType; } public void setEntityType(TaskEntityType entityType) { this .entityType = entityType; } public String toJson() { try { return mapper.writeValueAsString( this ); } catch (JsonProcessingException e) { logger.error( "[{}]:[{}]" , e.getClass().getName(), e.getMessage()); } return this .getCommand() + "," + this .getSubCommand(); } } |
static-cachemanager:
配置了Infinispan 缓存模式,主要就是包装了org.infinispan.manager.DefaultCacheManager,代码很简单:
1 2 3 | public class OptimizeCacheManager { public static DefaultCacheManager cacheManager = new DefaultCacheManager(); } |
关于Infinispan ,简单了解一下就好,这里使用的是Infinispan的本地模式。
Infinispan 是个开源的数据网格平台。它公开了一个简单的数据结构(一个Cache)来存储对象。
看一下pom.xml的配置:
1 2 3 4 5 6 7 8 9 10 11 12 | < dependencies > < dependency > < groupId >org.infinispan</ groupId > < artifactId >infinispan-cachestore-leveldb</ artifactId > < version >7.0.2.Final</ version > </ dependency > < dependency > < groupId >org.infinispan</ groupId > < artifactId >infinispan-spring</ artifactId > < version >7.0.2.Final</ version > </ dependency > </ dependencies > |
static-dubboinvoke:
测试项目,主要测试Dubbo服务端提供的静态化接口是否有效。
static-event:
继承org.springframework.context.ApplicationEvent,实现了一个
TaskProcessErrorEvent,用来监听消息处理中的错误事件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public class TaskProcessErrorEvent<T extends TaskEntity> extends ApplicationEvent { private static final long serialVersionUID = -4883903824786622364L; TaskNode<T> taskNode; public TaskProcessErrorEvent(TaskNode<T> source) { super (source); this .taskNode = source; } public TaskNode<T> getTaskNode() { return taskNode; } public void setTaskNode(TaskNode<T> taskNode) { this .taskNode = taskNode; } } |
依赖static-beans项目。
static-event-channelhandler:
继承ApplicationListener,实现了一个ChannelTaskOccurredEventHandler。
static-event-handler:
类似static-event-channelhandler项目。
static-executor:
维护了一个任务调度的线程池,
实现方式和前面发起静态化线程的线程池类似,不同的是
这里使用Executors获得线程池实例的方法,
封装了两个线程池,一个是
java.util.concurrent.ThreadPoolExecutor
java.util.concurrent.ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor是ThreadPoolExecutor的子类,
被用来执行定时任务。
看一下代码:
1 2 3 4 5 6 7 | public class OptimizeScheduledExecutor { public static ScheduledThreadPoolExecutor executor = (ScheduledThreadPoolExecutor) Executors.newScheduledThreadPool( 1 ); public static ThreadPoolExecutor excutorBatch = (ThreadPoolExecutor) Executors.newSingleThreadExecutor(); static { OptimizeScheduledExecutor.executor.setMaximumPoolSize( 1 ); } } |
static-process:
定义了消息转换和发送等的接口。
static-utils:
项目工具类。
static-xuan-beans:
这个项目在代码层面是继承static-beans工程的,
其中继承抽象类TaskEntity实现了针对首页、栏目和稿件静态化的三个消息实体对象:
ChannelTaskEntity、IndexTaskEntity和ContentTaskEntity。
继承抽象包装类TaskEntityWrapper实现了
ChannelTaskEntityWrapper、ContentTaskEntityWrapper和IndexTaskEntityWrapper。
继承ApplicationEvent实现了ChannelTaskOccurredEvent。
其他的mobile等项目是后期扩展添加,不在我们的讨论范围中。
下面重点看一下static-starter模块,分析一下相关流程,这是消费端项目启动的入口。
(2)从MQ中接收消息并处理
下面我们分析static-starter项目的配置和启动。
(3)相关的配置文件
ActiveMQ配置文件,application.properties:
1 2 3 4 5 6 7 | jms.broker.url=failover:(tcp://192.168.xx.xx:61616?wireFormat.maxInactivityDuration=0,tcp://192.168.xx.xx:61617?wireFormat.maxInactivityDuration=0)&maxReconnectDelay=1000 jms.username=test jms.password=test jms.queue.static.auto=STATIC.AUTO jms.queue.static.medium=STATIC.MEDIUM jms.queue.static.high=STATIC.HIGH jms.queue.static.critical=STATIC.CRITICAL |
Dubbo消费端配置文件,consumer.xml:
Redis配置文件,供jesque读取,redis.properties;
1 2 3 4 5 6 7 | redis.host=192.168.xx.xx redis.port=6379 redis.timeout=5000 redis.password=**** redis.namespace=resque redis.database=0 jesque.queue.name=bingyue |
配置jesque信息,jesque.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | < bean id="config" class="net.greghaines.jesque.Config"> < constructor-arg value="${redis.host}" /> < constructor-arg value="${redis.port}" /> < constructor-arg value="${redis.timeout}" /> < constructor-arg value="${redis.password}" /> < constructor-arg value="${redis.namespace}" /> < constructor-arg value="${redis.database}" /> </ bean > < bean id="jedisPool" class="redis.clients.jedis.JedisPool"> < constructor-arg > < bean class="net.greghaines.jesque.utils.PoolUtils" factory-method="getDefaultPoolConfig" /> </ constructor-arg > < constructor-arg value="${redis.host}" /> < constructor-arg value="${redis.port}" /> < constructor-arg value="${redis.timeout}" /> < constructor-arg value="${redis.password}" /> < constructor-arg value="${redis.database}" /> </ bean > < bean id="jesqueClient" class="net.greghaines.jesque.client.ClientImpl"> < constructor-arg ref="config" /> < constructor-arg value="true" /> </ bean > < bean id="failureDAO" class="net.greghaines.jesque.meta.dao.impl.FailureDAORedisImpl"> < constructor-arg ref="config" /> < constructor-arg ref="jedisPool" /> </ bean > < bean id="keysDAO" class="net.greghaines.jesque.meta.dao.impl.KeysDAORedisImpl"> < constructor-arg ref="config" /> < constructor-arg ref="jedisPool" /> </ bean > < bean id="queueInfoDAO" class="net.greghaines.jesque.meta.dao.impl.QueueInfoDAORedisImpl"> < constructor-arg ref="config" /> < constructor-arg ref="jedisPool" /> </ bean > < bean id="workerInfoDAO" class="net.greghaines.jesque.meta.dao.impl.WorkerInfoDAORedisImpl"> < constructor-arg ref="config" /> < constructor-arg ref="jedisPool" /> </ bean > < bean id="queueName" class="java.lang.String"> < constructor-arg value="${jesque.queue.name}"/> </ bean > |
配置加载MQ连接池等,mq-config.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 | < amq:connectionFactory brokerURL="${jms.broker.url}" id="amqConnectionFactory" userName="${jms.username}" password="${jms.password}"> < amq:redeliveryPolicyMap > < amq:redeliveryPolicyMap > < amq:defaultEntry > <!-- default policy, 5 times with 10s delay each --> < amq:redeliveryPolicy maximumRedeliveries="3" initialRedeliveryDelay="1000" /> </ amq:defaultEntry > < amq:redeliveryPolicyEntries > <!-- three times with exponential back-off, that is, 1s, 2s, 4s, 8s. "queue" references to the "physicalName" defined in amq:queue --> < amq:redeliveryPolicy queue="${jms.queue.static.auto}" maximumRedeliveries="3" initialRedeliveryDelay="1000" backOffMultiplier="3" useExponentialBackOff="true" maximumRedeliveryDelay="60000" /> <!-- another policy mapping --> < amq:redeliveryPolicy queue="${jms.queue.static.high}" maximumRedeliveries="3" initialRedeliveryDelay="2000" /> </ amq:redeliveryPolicyEntries > </ amq:redeliveryPolicyMap > </ amq:redeliveryPolicyMap > </ amq:connectionFactory > <!-- Default Destination Queue Definition 设置预读取条数 --> < bean id="destinationAuto" class="org.apache.activemq.command.ActiveMQQueue"> < constructor-arg index="0" value="${jms.queue.static.auto}?consumer.prefetchSize=1" /> </ bean > < bean id="destinationMedium" class="org.apache.activemq.command.ActiveMQQueue"> < constructor-arg index="0" value="${jms.queue.static.medium}?consumer.prefetchSize=1" /> </ bean > < bean id="destinationHigh" class="org.apache.activemq.command.ActiveMQQueue"> < constructor-arg index="0" value="${jms.queue.static.high}?consumer.prefetchSize=1" /> </ bean > < bean id="destinationCritical" class="org.apache.activemq.command.ActiveMQQueue"> < constructor-arg index="0" value="${jms.queue.static.critical}?consumer.prefetchSize=1" /> </ bean > <!-- Text message converter --> < bean id="textMessageConverter" class="test.task.process.jms.internal.TextTaskMessageConverter"> </ bean > <!-- Map message converter --> < bean id="mapMessageConverter" class="test.task.process.jms.internal.MapTaskMessageConverter"> </ bean > <!-- Message Receiver Definition --> < bean id="messageListener" class="org.springframework.jms.listener.adapter.MessageListenerAdapter"> < constructor-arg > < bean class="test.task.process.handle.internal.DefaultHandleTaskMessageEntry"> < property name="taskMessageSender" ref="textTaskMessageSender" /> </ bean > </ constructor-arg > < property name="defaultListenerMethod" value="processTask" /> <!-- property name="messageConverter"><null/></property --> < property name="messageConverter" ref="textMessageConverter" /> </ bean > < bean id="errorHandler" class="test.task.process.handle.internal.DefaultTaskProcessErrorHandler" /> < bean id="taskMessageListenerContainerAuto" class="org.springframework.jms.listener.DefaultMessageListenerContainer"> < property name="sessionAcknowledgeMode" value="2" /> < property name="connectionFactory" ref="amqConnectionFactory" /> < property name="destination" ref="destinationAuto" /> < property name="messageListener" ref="messageListener" /> < property name="concurrentConsumers" value="8" /> < property name="errorHandler" ref="errorHandler" /> </ bean > < bean id="taskMessageListenerContainerMedium" class="org.springframework.jms.listener.DefaultMessageListenerContainer"> <!-- property name="sessionTransacted" value="true"/ --> < property name="sessionAcknowledgeMode" value="2" /> <!-- 2-client-ack 0 transaction --> < property name="connectionFactory" ref="amqConnectionFactory" /> < property name="destination" ref="destinationMedium" /> < property name="messageListener" ref="messageListener" /> < property name="concurrentConsumers" value="1" /> < property name="errorHandler" ref="errorHandler" /> <!-- property name="maxMessagesPerTask" value="1" / --> </ bean > < bean id="taskMessageListenerContainerHigh" class="org.springframework.jms.listener.DefaultMessageListenerContainer"> <!-- property name="sessionTransacted" value="true"/ --> < property name="sessionAcknowledgeMode" value="2" /> <!-- 2-client-ack 0 transaction --> < property name="connectionFactory" ref="amqConnectionFactory" /> < property name="destination" ref="destinationHigh" /> < property name="messageListener" ref="messageListener" /> < property name="concurrentConsumers" value="1" /> < property name="errorHandler" ref="errorHandler" /> <!-- property name="maxMessagesPerTask" value="1" / --> </ bean > < bean id="taskMessageListenerContainerCritical" class="org.springframework.jms.listener.DefaultMessageListenerContainer"> <!-- property name="sessionTransacted" value="true"/ --> < property name="sessionAcknowledgeMode" value="2" /> <!-- 2-client-ack 0 transaction --> < property name="connectionFactory" ref="amqConnectionFactory" /> < property name="destination" ref="destinationCritical" /> < property name="messageListener" ref="messageListener" /> < property name="concurrentConsumers" value="1" /> < property name="errorHandler" ref="errorHandler" /> <!-- property name="maxMessagesPerTask" value="1" / --> </ bean > < bean id="textJmsTemplate" class="org.springframework.jms.core.JmsTemplate"> < property name="connectionFactory" ref="amqConnectionFactory" /> < property name="messageConverter" ref="textMessageConverter" /> </ bean > < bean id="mapJmsTemplate" class="org.springframework.jms.core.JmsTemplate"> < property name="connectionFactory" ref="amqConnectionFactory" /> < property name="messageConverter" ref="mapMessageConverter" /> </ bean > < bean id="textTaskMessageSender" class="test.task.process.handle.internal.DefaultTaskMessageSender"> < property name="jmsTemplate" ref="textJmsTemplate" /> </ bean > < bean id="mapTaskMessageSender" class="test.task.process.handle.internal.DefaultTaskMessageSender"> < property name="jmsTemplate" ref="mapJmsTemplate" /> </ bean > |
最后是Spring CTX启动加载文件,spring-config.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | < bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> < property name="locations"> < list > < value >application.properties</ value > < value >redis.properties</ value > </ list > </ property > </ bean > < bean id="priorityChannel" class="test.task.process.bean.internal.DefaultTaskPriorityChannel"> < property name="AUTO_CHANNEL" value="${jms.queue.static.auto}" /> < property name="MEDIUM_CHANNEL" value="${jms.queue.static.medium}" /> < property name="HIGH_CHANNEL" value="${jms.queue.static.high}" /> < property name="CRITICAL_CHANNEL" value="${jms.queue.static.critical}" /> </ bean > < bean class="test.task.process.utils.ApplicationContextRegister" /> < import resource="process-mapping.xml" /> < bean class="test.task.process.utils.ProcessEntityMap"> < property name="processMap" ref="processMap" /> < property name="processWrapperMap" ref="processWrapperMap" /> </ bean > < import resource="mq-config.xml" /> < import resource="consumer.xml" /> < import resource="jesque.xml" /> < bean id="taskEventHandler" class="test.task.event.listener.TaskProcessErrorEventHandler"></ bean > < bean id="channelTaskEventHandler" class="test.task.event.listener.ChannelTaskOccurredEventHandler"></ bean > |
(4)启动和部署方式
最后的打包方式是jar,启动脚本:
1 | java - cp "libs/*:xinhua-static-starter-0.0.1.jar" Main |
(5)ActiveMQ集群配置
静态化消息吞吐量较大,单个MQ实例不能满足要求,系统配置了多台MQ的集群,
消费端使用了MQ内置的failover断线重连机制:
1 | jms.broker.url=failover:(tcp: //192 .168.xx.xx:61616?wireFormat.maxInactivityDuration=0,tcp: //192 .168.xx.xx:61617?wireFormat.maxInactivityDuration=0)&maxReconnectDelay=1000 |
采用的是单台物理机上配置多台ActiveMQ实例开放不同端口的伪集群。
(6)配置Dubbo消费端,调用系统的静态化Dubbo服务
四、使用消息队列异步后对系统的优化
对静态化请求的解耦和异步处理,很好的平滑了高并发场景下短时间内大量的服务请求,比如稿件模板修改以后的批量静态化,
经常有上万个静态化的请求,提高了系统的可用性和稳定性,对一些对优先级要求比较高的请求,通过加入到不同的队列中来处理,
兼顾了及时性和异步性。
本文来自博客园,作者:邴越,转载请注明原文链接:https://www.cnblogs.com/binyue/p/3751903.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南