持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第11天,点击查看活动详情
背景
由于RocketMQ会出现重复消息,重复消息消费对于业务会造成影响,所以业务系统都需要考虑消息的幂等性。
简介
消息幂等性:消息重复消费多次与消息消费一次的结果都相同,并且多次消费对不会业务系统造成任何影响。
RocketMq产生重复消息场景
生产者发送消息时重复
当一条消息已被成功发送到Broker并完成持久化,此时出现了网络波动,从而导致Broker对生产者应答失败。此时生产者认为消息发送失败并尝试再次发送消息,此时Broker中就可能会出现两条内容相同的消息。
消费者消费失败
消费者消费失败,会重新把消费发回给Broker进行处理,Broker收到消息后会重新发送给消费者,所以会产生重复消息,该问题上一篇文章中已经提到了。
Rebalance时消息重复
当Consumer Group中的Consumer数量发生变化时,或其订阅的Topic的Queue数量发生变化时,会触发Rebalance,此时Consumer可能会收到曾经被消费过的消息。
解决方案
生成发送消息时重复解决方案
针对发送消息重复的场景,可以通过数据库唯一索引来进行业务保证。
针对上述的表结构需要将bizkey+type设置联合的唯一索引,确保每种业务类型的消息唯一性。
消费者重复消息解决方案
针对消费者重复消息的解决方案,可以采用如下的几种方案。
1.采用数据库乐观锁实现. 2.采用Redis的写入,因为Redis的写入操作支持天然的幂等性。
具体实现
生产者解决实现
public void sendOrderMessage(String topic,String bizKey,String type,String content)
{
logger.info("send add order message ");
Message message =new Message();
//业务的主键:可以为订单id、商品id等
message.setKeys(bizKey);
message.setTopic(topic);
String messageId=UUID.randomUUID().toString();
message.putUserProperty("messageId", messageId);
//创建订单
message.putUserProperty("type", type);
try
{
message.setBody(content.getBytes(RemotingHelper.DEFAULT_CHARSET));
}
catch (Exception e)
{
logger.error("message body getBytes error",e);
}
SysMessageInfo sysMessageInfo =new SysMessageInfo();
// 发送消息
rocketMQTemplate.asyncSend(topic, message,new SendCallback()
{
@Override
public void onSuccess(SendResult result)
{
logger.info("发送消息成功");
//业务日志记录成功
messageService.addProductMessge(sysMessageInfo);
}
@Override
public void onException(Throwable e)
{
logger.error("发送消息失败",e);
//业务进行日志记录失败
messageService.addProductMessge(sysMessageInfo);
}
});
}
消息插入接口
public void addProductMessge(SysMessageInfo sysMessageInfo)
{
try
{
// 查询数据是否存在,如果存在执行更新操作,否则执行新增操作
SysMessageInfo messageInfo = getMessage(sysMessageInfo);
if(messageInfo!=null)
{
//更新消息的时间和版本号
updateMessage(sysMessageInfo);
}
//执行新增操作
else
{
addMessage(sysMessageInfo);
}
}
catch (Exception e)
{
logger.error("addProductMessge error",e);
}
}
说明:通过数据库的唯一索引来保证消息重复,先查询消息是否存在,如果不存在则执行新增操作,否则执行更新操作。
消费者实现
private Logger logger =LoggerFactory.getLogger(getClass());
@Autowired
private MessageService messageService;
@Override
public void onMessage(MessageExt messageExt)
{
String msg=JSON.toJSONString(messageExt);
logger.info("send messsge succss content is:{}", msg);
//消息id
String messageId= messageExt.getProperty("messageId");
//业务key
String bizKey=messageExt.getKeys();
//类型
String type= messageExt.getProperty("type");
SysMessageInfo sysMessageInfo =new SysMessageInfo();
sysMessageInfo.setBizKey(bizKey);
sysMessageInfo.setType(type);
sysMessageInfo.setMessageId(messageId);
// 查看消息是否消费,true表示消息已经消费
if(messageService.checkReaptMesasge(sysMessageInfo))
{
logger.error("message already counsumer:messageId:{}",messageId);
return;
}
/**
* 业务处理逻辑,
* 可以通过业务状态的变化来判断消息是否消费
* 通过乐观锁来保证业务并发性
* 如果业务场景复杂可以采用分布式来实现
*/
invokeBiz(sysMessageInfo);
}
/**
* 处理
* @param sysMessageInfo
*/
public void invokeBiz(SysMessageInfo sysMessageInfo)
{
//更新消息状态
//业务接口
}
消息消费检查接口
@Override
public boolean checkReaptMesasge(SysMessageInfo sysMessageInfo)
{
//存在并发问题
SysMessageInfo dbMessage =getMessage(sysMessageInfo);
// 消息已消费
if(dbMessage!=null && "Consumed".equals(dbMessage.getStatus()))
{
return true;
}
return false;
}
说明:消息消费检查接口可能会存在并发问题,所以还是需要业务通过乐观锁或者流程状态来进一步判断重复消费。
总结
RocketMQ无法做到消息的幂等性,RocketMQ能够尽量保证消息不丢失,且遵循至少被消费一次的原则,具体的消息幂等还是需要结合实际的业务场景,通过相应的技术方案来保证。