场景:
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 原因需要改进;
获取消息注意排序规则;避免底部消息永远执行不到的问题;