可靠消息案例

场景:

A项目更新用户信息;调用B项目把信息更新;非强一致性;最终一致

当前使用方式:发送Http通知B项目

问题:

1,http调用失败怎么办要不要重试?

2,多久重试一次比较好?

方案:

设计消息表,来源于网络:

CREATE TABLE `rp_transaction_message` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT,
  `request_id` varchar(64) NOT NULL DEFAULT '' COMMENT '请求ID',
  `sequence_no` varchar(64) NOT NULL DEFAULT '' COMMENT '对应业务标识',
  `editor` varchar(255) DEFAULT NULL COMMENT '修改者',
  `creater` varchar(255) DEFAULT NULL COMMENT '创建者',
  `message_body` longtext COMMENT '消息内容',
  `message_data_type` int(1) DEFAULT NULL COMMENT '消息数据类型',
  `consumer_queue` varchar(100) DEFAULT NULL COMMENT '消费队列',
  `areadly_dead` int(1) NOT NULL DEFAULT '0' COMMENT '是否死亡 0 活动 1 非活动',
  `status` int(1) NOT NULL DEFAULT '0' COMMENT '状态 0待确认1确认2发送中3完成',
  `remark` varchar(255) DEFAULT NULL COMMENT '备注',
  `type` int(1) NOT NULL COMMENT '1 mq 2 httpget 3 httppost',
  `target_uri` longtext COMMENT '目标地址',
  `retry_count` int(1) NOT NULL DEFAULT '0' COMMENT '重试次数',
  `message_send_time` datetime DEFAULT NULL COMMENT '发送时间',
  `edit_time` datetime DEFAULT NULL,
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `base_version` int(11) NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`),
  KEY `IDX_SequenceNo` (`sequence_no`),
  KEY `IDX_MessageSendTime` (`message_send_time`)
) ENGINE=InnoDB AUTO_INCREMENT=278 DEFAULT CHARSET=utf8;

A项目不负责直接发送消息,把消息入库;

建立负责发送消息项目,这里使用 Grails 建立 定时器;

需要做的事情:

循环读取数据消息;

利用redis验证消息是否发送过,设置过期时间

/**
 * NX – 只有键key不存在的时候才会设置key的值
 * XX – 只有键key存在的时候才会设置key的值
 */
public String set(String key, String value, String nxxx, String expx, long time)
class RMessageListenerSendJob {

    def rpTransactionMessageService;
    def redisService

    static triggers = {
        simple name: 'sendRmessageJob', startDelay: 3000, repeatCount: 0
    }

    def execute() {
        while (true) {
            try{
                log.debug("当前HTTP连接数:"+RMessageQueue.currentHTTPCount.get())
                List<RpTransactionMessage> tmList = rpTransactionMessageService.findConfirmMessageList();
                log.info(String.format("数据集合大小:%s", tmList.size()))
                if (tmList==null || tmList.size()==0){
                    Thread.sleep(5000L)
                }
                List<RpTransactionMessage> sendList=new ArrayList<RpTransactionMessage>()
                for(RpTransactionMessage tm :tmList){
                    if (RedisConcurrencyLockUtil.lock(redisService,"reliable_news:"+tm.id,"1",20 * 60L)) {
                        sendList.add(tm);
                        rpTransactionMessageService.startProc(tm);
                    }
                }
                if (tmList==null || tmList.size()==0){
                    Thread.sleep(5000L)
                }
                for(RpTransactionMessage tm :sendList){
                    this.reSendMessage(tm);
                }
            }catch (Exception e){
                e.printStackTrace()
            }
        }
    }
}

如果插入成功证明消息没有发送过,然后修改消息状态为发送中

然后调用写好的http异步请求发送消息;(建议使用:async-http-client 1.9.40

因为使用异步发送http需要控制发送数量防止堆积影响性能,有时候并不是线程越多越好;

def reSendMessage(RpTransactionMessage tm) {
	Map<String, String> param = new HashMap<String, String>()
	int messageId = tm.id
	param.put("data", tm.messageBody)
	if (tm.type == RpTransactionMessageService.URL_TYPE_GET) {
		while(RMessageQueue.currentHTTPCount.get() > 200){
			log.warn("当前HTTP连接数超过200,暂停发送")
			Thread.sleep(100L)
		}
		String url = tm.targetUri + "?" + tm.messageBody
		httpGet(url, param, messageId)
	}else if (tm.type == RpTransactionMessageService.MQ_TYPE) {
		log.debug("Send Mq tag:" + tm.consumerQueue+",body:"+tm.messageBody);
		sendMq(tm.consumerQueue,tm.messageBody,tm.id)
	}
};
public static AtomicInteger currentHTTPCount = new AtomicInteger();

通过回调来更新消息重试次数和状态;回到出现异常重试次数要累加同时删除redis

def httpGet(String uri, Map<String, String> params, int messageId) {
	RMessageQueue.currentHTTPCount.incrementAndGet()
	rpTransactionMessageService.asyncHttpClient.prepareGet(uri).execute(new AsyncCompletionHandler<Response>() {
		@Override
		public Response onCompleted(Response response) throws Exception {
			callback(response, messageId);
			return response;
		}
		@Override
		public void onThrowable(Throwable t) {
			String msg = t.getMessage()
			callbackExceptionHandle(msg, messageId)
		}
	})
}
def callback(Response response, int messageId) {
        String content = StringUtils.EMPTY;
        try {
            int code = response.getStatusCode()
            content = response.getResponseBody()
            if (content.length() > 255) {
                content = content.substring(0, 255);
            }
            log.info(String.format("状态码:%s,消息ID:%s", code, messageId))
            if (code == 200) {
                def result = rpTransactionMessageService.updateMessageStatus(messageId,
                        rpTransactionMessageService.STATUS_DONE, "system", content)
                log.debug("200更新库结果:" + result)
                return
            }
            rpTransactionMessageService.reNumber(messageId, content)
        } catch (Exception e) {
            log.error("请求失败,原因:" + e.message,e);
            rpTransactionMessageService.reNumber(messageId, "回调处理异常:" + e.message)
        } finally {
            String rediskey = RpTransactionMessageService.CACHE_NAME + ":" + messageId;
            def delStatus = redisService.withRedis { Jedis redis ->
                return redis.del(rediskey)
            }
            log.debug(String.format("callback-redis删除状态:%s,消息ID:%s", delStatus, messageId))
        }
    }

重试间隔算法

/**
 * 计算下次请求间隔时间
 * 公式 RetryTime**=PreviousRetryTime+(** 0.08 *LoginTimeout)
 * @return x ( ms )
 */
def nextRetryTime(int retryCount) {
	float previousRetryTime = (float) (this.RETRY_PER * retryCount) * this.MAX_TIMEOUT;
	float retryTimeS = (float) (previousRetryTime + (this.RETRY_PER * this.MAX_TIMEOUT));
	int millis = (int) (retryTimeS * 1000);
	return millis;
}

途中遇到问题:

刚开始使用定时器1秒执行一次(每次用不同的线程执行),发下有并发问题;而且和while(true)效果是一样的(也有并发问题 = =);

导致 刚从数据读取数据还没有来得及发送更新状态,又被查询出来了;(导致数据库消息的重试次数超出预期)

改成拉取1000条,发送完,在获取一千条;但是还是有问题,消费完在拉取,会有一段时间队列是空的;(浪费)

我们的目的不就是为了防止消息重发发送么,然后就采取的redis验证消息;然后就可以不间断的发消息了;

里面获取和发送都有限制,如果不限制 内存波动大;因为 new 原因需要改进;

获取消息注意排序规则;避免底部消息永远执行不到的问题;

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值