SpringBoot项目:RabbitMQ&MongoDB整合后台消息模块

RabbitMQ&MongoDB整合后台消息模块

1. 消息模块设计原理

消息和用户是一对一关系,例如通过一对一才能知道哪个消息被用户已读,但MYSQL是无法支持海量数据库存储的。所以我们选择使用MongoDB存储消息记录(海量低价值的数据),MongoDB没有表结构,只有集合。message存储消息主体(正文、日期、发送人),message_re存储接收人(接收人、用户是否阅读等数据)

如果瞬间写入海量记录,数据库正常的CRUD会受到影响,我们需要使用消息队列实现削峰填谷,即把消息发送到消息队列(RabbitMQ),当用户登陆时再把消息提取出来保存到MongoDB。

消息日积月累,MongoDB也会有撑不下来的时候,我们需要搞冷热数据分离,热数据定期归档,根据数据被使用的频率可以划分为热数据和冷数据,例如,一年内的数据被看做事热数据,超过一年的数据被当作冷数据。每天定期把冷数据从MongoDB_1转移到MongoDB_2,这样MongoDB_1的数据量减少就提高了速度,MongoDB_2存放数据量很大,但冷数据很少被使用,仅仅充当归档库而已。冷数据定期销毁,释放存储空间。假设五年以上的冷数据就被当成超期数据,删除超期数据之后归档库的空间就增加了。

2. 消息发送和收取

引入消息队列MQ,在Web系统中,先CRUD消息主体(不包含接收人等信息)到MongoDB,用异步多线程把消息的ref集合发送到消息队列。每一个消息的topic则对应一个用户ID。

当用户登录系统后,Web系统利用异步线程方式从消息队列中把消息接收回来,把ref消息写入到MongoDB里。

3. RabbitMQ入门

RabbitMQ与Kafka不同,RabbitMQ既支持消息异步收发,又支持同步收发。能适应各种业务场景的优点便显现出来了。

将rabbitmq.tar.gz文件上传到CentOS系统,把镜像导入Docker环境

docker load < rabbitmq.tar.gz

创建rabbitmq容器

docker run -it -d --name mq -p 15672:15672 -p 5672:5672 rabbitmq
3.1 五种队列模式
3.1.1 简单模式

在这里插入图片描述

一个生产者(发送方)对应一个消费者(接收方),生产者和消费者必须是一对一的关系。

3.1.2 Work模式

SpringBoot项目:RabbitMQ&MongoDB整合后台消息模块_第1张图片

一个生产者对应多个消费者,但只有一个消费者能获得消息,具有排他性。

3.1.3 发布/订阅模式

SpringBoot项目:RabbitMQ&MongoDB整合后台消息模块_第2张图片

一个生产者首先将消息发送到fanout交换器,交换器绑定多个队列,然后与之对应的所有消费真呢个收到消息,不具有排他性。

3.1.4 路由模式

SpringBoot项目:RabbitMQ&MongoDB整合后台消息模块_第3张图片

生产者将消息发送到direct交换器,交换器按照关键字key把消息路由到某个队列,即符合规则才会转发到该队列。

3.1.5 主题模式

SpringBoot项目:RabbitMQ&MongoDB整合后台消息模块_第4张图片

生产者将消息发送到topic交换器,交换器按照复杂的规则将消息路由到某个队列

3.2 消息持久化

消息的可靠性是RabbitMQ的特色,可靠性由消息持久化实现,可以防止在异常情况下丢失数据,交换器和队列都能持久化。

3.3 消息过期时间

默认情况下,消息是无限期存储在RabbitMQ上的,但我们可以设置过期时间,到期后无论是否已经被接受都会被RabbitMQ删除。

3.4 ACK应答

消费者接收消息之后,必须返回一个Ack应答,那么RabbitMQ才会认为这个消息接收成功,如果想要删除这条消息,消费者发送Ack应答的时候,附带一个deliverTag标志位就可以了。

3.5 同步接收和异步接收(针对小程序)

异步接收消息消耗的系统资源较少,但是微信小程序和后端项目之间并不是长连接,后端项目异步方式接收到队列中的消息也无法推送移动端的小程序。所以后端的Java项目采用同步的方式接收队列的消息,在移动端,我们创建定时器,向后端Java项目发出轮询请求,后端项目接收到轮询秦桧后,用同步方式接收队列的消息,然后把消息存储在MongoDB上面。

4. 消息模块数据模型设计

4.1 创建POJO映射类

MongoDB中没有数据表的概念,而是采用集合Collection存储数据,每一个数据就是一个Document,文档结构其实就是我们常用的JSON。

4.1.1 Message集合
字段 类型 备注
_id UUID 自动生成的主键值
uuid UUID UUID值,并设置有唯一索引,防止消息被重复消费
senderId Integer 发送者ID,就是用户ID,如果是系统发出,则ID为0
senderPhoto String 发送者头像URL
senderName String 发送者名称
msg String 消息内容
sendTime Date 发送时间

针对重复消费:小程序每个5分钟进行轮询,如果积压得消息太多,Java系统没有接受完消息,这时候新的轮询到来,就会产生两个消费者共同接收同一个消息的情况,数据库就添加了同样的记录,如果每条MQ消息都有唯一的UUID值,第一个消费者把消息保存到数据库后,第二个消费者就无法继续保存了。

创建MessageEntity类映射message集合

@Data
@Document(collation = "message")
//标明由mongo来维护该表,collection里面的名字对应着mongodb里面的文档
public class MessageEntity implements Serializable {
    //Serializable启用其序列化功能的接口
    //serializable接口的作用:存储对象在存储介质中,以便在下次使用的时候,可以很快捷的重建一个副本;便于数据传输,尤其是在远程调用的时候。
    @Id
    private String _id;

    @Indexed(unique = true)//唯一索引
    private String uuid;

    @Indexed//加索引后以该字段为条件检索将大大提高速度
    private Integer sendId;

    //可设置默认值为系统头像
    private String senderPhoto="XXX";

    private String senderName;

    private String msg;

    @Indexed
    private Date sendTime;
}
4.1.2 Message_ref集合

虽然message集合记录是消息,里面有接收者ID,但群发消息是接收者ID空,这时候需要用上message_ref集合来记录接收人和已读状态。

字段 类型 备注
_id UUID 主键
messageId UUID message记录的_id
receiverId String 接收人ID
readFlag Boolean 是否已读
lastFlag Boolean 是否为新接收的消息

创建MessageRefEntity类映射message_ref集合

public class MessageRefEntity implements Serializable {
    @Id
    private String _id;

    @Indexed
    private String messageId;

    @Indexed
    private Integer receiverId;

    @Indexed
    private Boolean readFlag;

    @Indexed
    private Boolean lastFlag;
}
4.1.3 MongoDB的联合查询

MongoDB从3.X开始支持集合的连接查询,也就相当于MYSQL的表连接。

先插入两条数据

db.message.insert({
	_id:ObjectId("600bea9ab5bafb311f147506"),
	uuid:"bfcb7c47-5886-c528-5127-ce285bc2322a",
	senderId:0,
	senderPhoto:"https://static-1258386385.cos.ap-beijing.myqcloud.com/img/System.jpg",
	senderName:"Emos系统",
	msg:"HelloWord",
	sendTime:ISODate("2021-01-23T17:21:30Z")
});

db.message_ref.insert({
	_id:ObjectId("600beaf0d6310000830036f3"),
	messageId:"600bea9ab5bafb311f147506",
	receiverId:1,
	readFlag:false,
	lastFlag:true
});

再使用语句连接

db.message.aggregate(
    //数据类型转换,将Oject转成String去连接
	{
        //set定义变量
		$set:{
            //临时变量Id
			"id":{$toString:"$_id"}
		}
	},
    //
	{
		$lookup:{
            //连接message_ref,message出Id,跟message_ref的messageId连接
			from:"message_ref",
			localField:"id",
			foreignField:"messageId",
            //从message_ref取出来的数据保存在ref
			as:"ref"
		}
	},
    //寻找receiverId=1的消息内容,match为查询条件
	{ $match:{"ref.receiverId":1} },
    //按照发送时间降序
	{ $sort:{sendTime:-1} },
    //从0开始往后取50条数据
	{ $skip:0 },
	{ $limit:50 }
)

5. 后端持久层

提供的功能包括:

  • 刷新消息模块
  • 获取分页消息
  • 根据ID查询消息
  • 把未读消息更新成已读
  • 删除消息
5.1 创建MessageDao类
@Repository
public class MessageDao {

    @Autowired
    private MongoTemplate mongoTemplate;

    //插入数据
    public String insert(MessageEntity entity){
        //把北京时间转成格林时间再存到MongoDB
        Date sendTime  = entity.getSendTime();
        //偏移8个小时
        sendTime = DateUtil.offset(sendTime, DateField.HOUR,8);
        entity.setSendTime(sendTime);
        entity = mongoTemplate.save(entity);
        return entity.get_id();
    }
    //按照分页查询消息,注意第二个参数为long
    public List<HashMap> searchMessageByPage(int userId, long start, int length){
        JSONObject json = new JSONObject();
        //数据类型转换
        json.set("$toString","$_id");
        //集合连接
        Aggregation aggregation = Aggregation.newAggregation(
                //设置临时变量id,值为json中拿,
                Aggregation.addFields().addField("id").withValue(json).build(),
                //集合联合查询
                Aggregation.lookup("message_ref","id","messageId","ref"),
                //match设置查询条
                Aggregation.match(Criteria.where("ref.receiverId").is(userId)),
                //排序
                Aggregation.sort(Sort.by(Sort.Direction.DESC,"sendTime")),
                Aggregation.skip(start),
                Aggregation.limit(length)
        );
        //跟message连接,返回为HashMap
        AggregationResults<HashMap> results = mongoTemplate.aggregate(aggregation,"message",HashMap.class);
        //获取数据
        List<HashMap> list =  results.getMappedResults();
        //处理数据
        list.forEach(one->{
            //从ref中提取数据
            List<MessageRefEntity> refList = (List<MessageRefEntity>) one.get("ref");
            MessageRefEntity entity = refList.get(0);
            boolean readFlag = entity.getReadFlag();
            String refId = entity.get_id();
            one.put("readFlag",readFlag);
            one.put("refId",refId);
            one.remove("ref");
            //删除主体ID,到时候删除的时候我们是删除ref主体删除掉
            one.remove("_id");
            //时间转换,转回北京时间
            Date sendTime = (Date)one.get("sendTime");
            sendTime = DateUtil.offset(sendTime,DateField.HOUR,-8);
            //取当前日期比较,sendTime相等,则代表消息是今天,显示发送时间不需要显示日期
            //之前发送的消息才需要显示日期
            String today = DateUtil.today();
            if(today.equals(DateUtil.date(sendTime).toDateStr())){
                one.put("sendTime",DateUtil.format(sendTime,"HH:mm"));
            }else{
                one.put("sendTime",DateUtil.format(sendTime,"yyyy/MM/dd"));
            }
        });
        return list;
    }
    //根据ID查找数据
    public HashMap searchMessageById(String id){
        HashMap map = mongoTemplate.findById(id,HashMap.class,"message");
        Date sendTime = (Date)map.get("sendTime");
        sendTime = DateUtil.offset(sendTime,DateField.HOUR,-8);
        map.replace("sendTime",DateUtil.format(sendTime,"yyyy/MM/dd HH:mm"));
        return map;
    }
}
5.2 创建MessageRefDao类
@Repository
public class MessageRefDao {

    @Autowired
    private MongoTemplate mongoTemplate;

    //插入
    public String insert(MessageRefEntity entity){
        entity = mongoTemplate.save(entity);
        return entity.get_id();
    }
    //查询未读消息的数量(汇总统计都是long类型)
    public long searchUnreadCount(int userId){
        Query query = new Query();
        //设置条件
        query.addCriteria(Criteria.where("readFlag").is(false).and("receiverId").is(userId));
        //查询条件跟映射类
        long count = mongoTemplate.count(query,MessageRefEntity.class);
        return count;
    }
    //修改更新条数
    public long searchLastCount(int userId){
        Query query = new Query();
        query.addCriteria(Criteria.where("lastFlag").is(true).and("receiverId").is(userId));
        Update update = new Update();
        update.set("lastFlag",false);
        UpdateResult result = mongoTemplate.updateMulti(query,update,"message_ref");
        long count = result.getModifiedCount();
        return count;
    }
    //修改消息状态,未读改为已读
    public long updateUnreadMessage(String id){
        //只修改一条,调用updateFirst
        Query query = new Query();
        query.addCriteria(Criteria.where("_id").is(id));
        Update update = new Update();
        update.set("readFlag",true);
        UpdateResult result = mongoTemplate.updateFirst(query,update,"message_ref");
        long count = result.getModifiedCount();
        return count;
    }
    //根据主键值删除
    public long deleteMessageRefById(String id){
        Query query = new Query();
        query.addCriteria(Criteria.where("_id").is(id));
        DeleteResult result = mongoTemplate.remove(query,"message_ref");
        long count = result.getDeletedCount();
        return count;
    }
    //根据UserId删除所有消息
    public long deleteUserMessageRef(int userId){
        Query query = new Query();
        query.addCriteria(Criteria.where("receiverId").is(userId));
        DeleteResult result = mongoTemplate.remove(query,"message_ref");
        long count = result.getDeletedCount();
        return count;
    }
}

6. 后端业务层

6.1 Service接口
public interface MessageService {
    public String insertMessage(MessageEntity entity);
    public List<HashMap> searchMessageByPage(int userId, long start, int length);
    public HashMap searchMessageById(String id);
    public String insertMessageRef(MessageRefEntity entity);
    public long searchUnreadCount(int userId);
    public long searchLastCount(int userId);
    public long updateUnreadMessage(String id);
    public long deleteMessageRefById(String id);
    public long deleteUserMessageRef(int userId);
}
6.2 Service接口实现类
@Service
public class MessageServiceImpl implements MessageService {

    @Autowired
    private MessageDao messageDao;

    @Autowired
    private MessageRefDao messageRefDao;

    @Override
    public String insertMessage(MessageEntity entity) {
        String id = messageDao.insert(entity);
        return id;
    }

    @Override
    public List<HashMap> searchMessageByPage(int userId, long start, int length) {
        List<HashMap> list = messageDao.searchMessageByPage(userId,start,length);
        return list;
    }

    @Override
    public HashMap searchMessageById(String id) {
        HashMap map = searchMessageById(id);
        return map;
    }

    @Override
    public String insertMessageRef(MessageRefEntity entity) {
        String id = messageRefDao.insert(entity);
        return id;
    }

    @Override
    public long searchUnreadCount(int userId) {
        long count = messageRefDao.searchLastCount(userId);
        return count;
    }

    @Override
    public long searchLastCount(int userId) {
        long count = messageRefDao.searchLastCount(userId);
        return count;
    }

    @Override
    public long updateUnreadMessage(String id) {
        long count = messageRefDao.updateUnreadMessage(id);
        return count;
    }

    @Override
    public long deleteMessageRefById(String id) {
        long count = messageRefDao.deleteMessageRefById(id);
        return count;
    }

    @Override
    public long deleteUserMessageRef(int userId) {
        long count = messageRefDao.deleteUserMessageRef(userId);
        return count;
    }
}

7. 后端Web层

7.1.1 SearchMessageByPageForm
@Data
@ApiModel
public class SearchMessageByPageForm {

    @NotNull
    @Min(1)
    private Integer page;

    @NotNull
    @Range(min=1,max=40)
    private Integer length;

}
7.1.2 SearchMessageByIdForm
@Data
@ApiModel
public class SearchMessageByIdForm {
    @NotNull
    private String id;
}
7.1.3 DeleteMessageRefByIdForm
@Data
@ApiModel
public class DeleteMessageRefByIdForm {
    @NotNull
    private String id;
}
7.1.4 UpdateUnreadMessageForm
@Data
@ApiModel
public class UpdateUnreadMessageForm {
    @NotNull
    private String id;
}
7.1.5 MessageController
@RestController
@RequestMapping("/message")
@Api("消息模块网络接口")
public class MessageController {
    @Autowired
    private JwtUtil jwtUtil;

    @Autowired
    private MessageService messageService;

    @PostMapping("/searchMessageByPage")
    @ApiOperation("获取分页消息列表")
    public R searchMessageByPage(@Valid @RequestBody SearchMessageByPageForm form,@RequestHeader("token") String token){
        int userId = jwtUtil.getUserId(token);
        int page = form.getPage();
        int length = form.getLength();
        long start = (page-1)*length;
        List<HashMap> list = messageService.searchMessageByPage(userId,start,length);
        return R.ok().put("result",list);
    }

    @PostMapping("/searchMessageById")
    @ApiOperation("根据ID查询消息")
    public R searchMessageById(@Valid @RequestBody SearchMessageByIdForm form){
        HashMap map = messageService.searchMessageById(form.getId());
        return R.ok().put("result",map);
    }

    @PostMapping("/updateUnreadMessage")
    @ApiOperation("未读消息更新成已读消息")
    public R updateUnreadMessage(@Valid @RequestBody UpdateUnreadMessageForm form){
        long count = messageService.updateUnreadMessage(form.getId());
        return R.ok().put("result",count==1?true:false);
    }

    @PostMapping("/deleteMessageRefById")
    @ApiOperation("删除消息")
    public R deleteMessageRefById(@Valid @RequestBody DeleteMessageRefByIdForm form){
        long count = messageService.deleteMessageRefById(form.getId());
        return R.ok().put("result",count==1?true:false);
    }

}

8. 利用RabbitMQ实现消息投递削峰填谷

8.1 异步和同步如何选择

如果收发消息的时候我们的Java代码选择了同步执行,当用户登录的时候消息没有接收完成,那么Java代码就不会继续往下执行,会造成等待事件过长。如果选择异步执行,收发消息是挂载在后台的,让一个线程去执行。

RabbitMQ提供了同步和异步两种收发消息模式,如果选择了异步,则需要创建一个消费者对象挂载在后台去运行,消费者对象不会退出,没有消息则处于等待状态,而且消费者对象只接收某个用户的消息,例如有一万人登录系统,则会创建一万个消费对象,这对操作系统虚拟机要求很高。同步收发消息,消费者对象销毁这才是可取的。所以我们可以采用异步线程同步收发消息

8.2 导入依赖
        <dependency>
            <groupId>com.rabbitmqgroupId>
            <artifactId>amqp-clientartifactId>
            <version>5.9.0version>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-amqpartifactId>
        dependency>
8.3 创建配置类
@Configuration
public class RabbitMQConfig {

    @Bean
    public ConnectionFactory getFactory(){
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.10.105");
        factory.setPort(5672);
        return factory;
    }
8.4 创建消息任务类

创建多线程任务来收发RabbitMQ消息

@Component
@Slf4j
public class MessageTask {
    @Autowired
    private ConnectionFactory factory;

    @Autowired
    private MessageService messageService;

    //在RabbitMQ每个消息都有自己的名字,即topic
    public void send(String topic, MessageEntity entity){
        //保存在Message集合
        String id = messageService.insertMessage(entity);
        //向RMQ发送消息
        try(Connection connection = factory.newConnection();
            //创建通道
            Channel channel = connection.createChannel();
        ){
            //创建通道
            //通道连接到Topic队列,true代表持久化存储,false-1取消排他
            //false-2代表收发完消息后队列不会自动删除
            channel.queueDeclare(topic,true,false,false,null);
            //绑定额外的数据
            //这里绑定id用于ref关联
            HashMap map = new HashMap();
            map.put("messageId",id);
            //把map放到请求头中,AMQP协议用户连接队列
            AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().headers(map).build();
            //发送消息
            channel.basicPublish("",topic,properties,entity.getMsg().getBytes(StandardCharsets.UTF_8));
            //留下日志提示
            log.debug("消息发送成功");
        }catch (Exception e){
            log.error("执行异常",e);
            throw new EmosException("向MQ发送消息失败");
        }
    }
    //异步发送消息
    @Async
    public void sendAsync(String topic, MessageEntity entity){
        send(topic,entity);
    }
    //接收消息
    //接收消息数量
    public int receive(String topic){
        //参数代表从哪个topic队列接收数据
        int i =0;
        try(Connection connection = factory.newConnection();
            Channel channel = connection.createChannel();
        ){
            channel.queueDeclare(topic,true,false,false,null);
            //从消息队列接收消息,保存到MongoDBREF集合
            while(true){
                //接收消息,false代表不自动发送ACK应答
                GetResponse response = channel.basicGet(topic,false);
                if(response!=null){
                    AMQP.BasicProperties properties = response.getProps();
                    //从properties获得请求数据
                    Map<String,Object> map = properties.getHeaders();
                    //获取MessageId
                    String messageId = map.get("messageId").toString();
                    //获取消息正文,返回的是Byte数组
                    byte[] body = response.getBody();
                    String message = new String(body);
                    //日志记录
                    log.debug("从RabbitMQ接收消息"+message);
                    MessageRefEntity entity = new MessageRefEntity();
                    entity.setMessageId(messageId);
                    entity.setReceiverId(Integer.parseInt(topic));
                    //设置两个标志位置
                    entity.setReadFlag(false);
                    entity.setLastFlag(true);
                    //写入MongoDB
                    messageService.insertMessageRef(entity);
                    long deliveryTag = response.getEnvelope().getDeliveryTag();
                    //返回ACK应答
                    channel.basicAck(deliveryTag,false);
                    i++;
                }else{
                    break;
                }
            }
        }catch (Exception e){
            log.error("执行异常",e);
            throw new EmosException("接收消息失败");
        }
        //从消息队列接收消息,保存到MongoDBREF集合
        //返回ACK应答,表明成功接收消息
        return i;
    }

    //异步接收
    @Async
    public int receiveAsync(String topic){
        return receive(topic);
    }

    //删除消息队列
    public void deleteQueue(String topic){
        try(Connection connection = factory.newConnection();
            Channel channel = connection.createChannel();
        ){
            channel.queueDelete(topic);
            log.debug("消息队列删除成功");
        }catch (Exception e){
            log.error("执行异常",e);
            throw new EmosException("删除队列失败");
        }
    }

    //异步删除
    @Async
    public void deleteQueueAsync(String topic){
        deleteQueue(topic);
    }
}

9. 执行系统消息异步收发

9.1 注册流程中的消息发送
    @Override
    public int registerUser(String registerCode, String code, String nickname, String photo) {
        if(registerCode.equals("000000")){
            boolean bool = userDao.haveRootUser();
            if(!bool){
                String openId = getOpenId(code);
                HashMap param = new HashMap();
                param.put("openId",openId);
                param.put("nickname",nickname);
                param.put("photo",photo);
                param.put("role","[0]");
                param.put("status",1);
                param.put("createTime", new Date());
                param.put("root",true);
                userDao.insert(param);
                int id = userDao.searchIdByOpenId(openId);

                //发送消息开始
                MessageEntity entity = new MessageEntity();
                entity.setSendId(0);
                entity.setSenderName("系统消息");
                entity.setUuid(IdUtil.simpleUUID());
                entity.setSendTime(new Date());
                messageTask.sendAsync(id+"",entity);
                //发送消息结束

                return id;
            }else{
                throw  new EmosException("无法绑定超级管理员账号");
            }
        }else{

        }
        return 0;
    }
9.2 登录接收消息
    @Override
    public Integer login(String code) {
        //根据临时授权拿到openId
        String openId = getOpenId(code);
        Integer id = userDao.searchIdByOpenId(openId);
        //接收消息,topic ID是userId
        messageTask.receiveAsync(id+"");

        if(id==null){
            throw new EmosException("账号不存在");
        }
        return id;
    }
9.3 轮询接收系统消息
    @GetMapping("/refreshMessage")
    @ApiOperation("刷新用户数据")
    public R refreshMessage(@RequestHeader("token") String token){
        int userId = jwtUtil.getUserId(token);
        messageTask.receiveAsync(userId+"");
        long lastRows = messageService.searchLastCount(userId);
        long unreadRows = messageService.searchUnreadCount(userId);
        return R.ok().put("lastRows",lastRows).put("unreadRows",unreadRows);
    }

你可能感兴趣的:(消息模块,rabbitmq,mongodb,java)