一、MongoDB
MongoDB现在已经是应用比较广泛的文档型NoSQL产品,有不少公司都拿MongoDB来开发日志系统。随着MongoDB的不断迭代更新,据说最新版已经支持ACID和事务了。不过鉴于历史上MongoDB应用的一些问题,以及考虑到数据持久化和运维的要求,核心业务系统存储的技术选型要非常慎重。
1、什么是MongoDB
MongoDB是由C++语言编写的一个基于分布式文件存储的开源数据库系统。MongoDB将数据存储为一个文档,数据结构由键值(key=>value)对组成。MongoDB 文档类似于 JSON 对象(也就是BSON,10gen开发的一个数据格式),字段值可以包含其他文档,数组及文档数组。主要优点可以概括如下:
(1)、SchemaLess,结构灵活,表结构更改非常自由,不用每次修改的时候都付出代价(想想RDBMS修改表结构要注意什么),适合业务快速迭代表结构非常不确定的场景,而且json和大多数的语言有天然的契合,还支持数组,嵌套文档等数据类型
(2)、自带高可用,自动主从切换(副本集)
(3)、自带水平分片(分片),内置了路由,配置管理,应用只要连接路由,对应用来说是透明的
2、添加依赖
org.springframework.boot
spring-boot-starter-data-mongodb
3、添加配置
配置MongoDB连接串:
spring.data.mongodb.uri=mongodb://name:pass@ip:port/database?maxPoolSize=256
如果使用多台MongoDB数据库服务器,参考配置如下:
spring.data.mongodb.uri=mongodb://user:pwd@ip1:port1,ip2:port2/database?maxPoolSize=512
环境搭建好了,下面就是着手编码了。
通常我们会有各种语言的MongoDB客户端,直接引入调用API。在Spring Boot中,直接使用MongoTemplate即可。
4、定义DAO接口
package com.power.demo.mongodb;
import com.power.demo.domain.MailDO;
import java.util.Date;
import java.util.List;
public interface MailDao {
/**
* 批量创建对象
*
* @param entList
*/
void batchInsert(List entList);
/**
* 创建对象
*
* @param ent
*/
void insert(MailDO ent);
/**
* 根据ID查询对象
*
* @param mailId
* @return
*/
MailDO findByMailId(Long mailId);
/**
* 查询一段时间范围内待发送的邮件
*
* @param startTime 开始时间
* @param endTime 结束时间
* @return
*/
List findToSendList(Date startTime, Date endTime);
/**
* 更新
*
* @param ent
*/
void update(MailDO ent);
/**
* 删除
*
* @param mailId
*/
void delete(Long mailId);
}
MailDao
5、实现DAO
package com.power.demo.mongodb;
import com.power.demo.common.AppConst;
import com.power.demo.common.SendStatusType;
import com.power.demo.domain.MailDO;
import com.power.demo.util.CollectionHelper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.List;
@Component
public class MailDaoImpl implements MailDao {
@Autowired
private MongoTemplate mongoTemplate;
public void batchInsert(List entList) {
//分组批量多次插入 每次2000条
List
> groupList = CollectionHelper.spliceArrays(entList, AppConst.BATCH_RECORD_COUNT);
for (List list : groupList) {
mongoTemplate.insert(list, MailDO.class);
}
}
public void insert(MailDO ent) {
mongoTemplate.save(ent);
}
public MailDO findByMailId(Long mailId) {
Query query = new Query(Criteria.where("mailId").is(mailId));
MailDO ent = mongoTemplate.findOne(query, MailDO.class);
return ent;
}
/**
* 查询一段时间范围内待发送的邮件
*
* @param startTime 开始时间
* @param endTime 结束时间
* @return
*/
public List findToSendList(Date startTime, Date endTime) {
Query query = new Query(Criteria.where("create_time").gte(startTime).lt(endTime)
.and("has_delete").is(Boolean.FALSE)
.and("send_status").ne(SendStatusType.SendSuccess.toString())
.and("retry_count").lt(AppConst.MAX_RETRY_COUNT)) //重试次数小于3的记录
.limit(AppConst.RECORD_COUNT); //每次取20条
List entList = mongoTemplate.find(query, MailDO.class);
return entList;
}
public void update(MailDO ent) {
Query query = new Query(Criteria.where("_id").is(ent.getMailId()));
Update update = new Update()
.set("send_status", ent.getSendStatus())
.set("retry_count", ent.getRetryCount())
.set("remark", ent.getRemark())
.set("modify_time", ent.getModifyTime())
.set("modify_user", ent.getModifyUser());
//更新查询返回结果集的第一条
mongoTemplate.updateFirst(query, update, MailDO.class);
}
public void delete(Long mailId) {
Query query = new Query(Criteria.where("_id").is(mailId));
mongoTemplate.remove(query, MailDO.class);
}
}
MailDaoImpl
6、数据访问对象实体
实体MailDO这里只列举了我在实际开发应用中经常用到的字段,这个实体抽象如下:
package com.power.demo.domain;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;
import java.io.Serializable;
import java.util.Date;
@Data
@Document(collection = "mailinfo")
public class MailDO implements Serializable {
private static final long serialVersionUID = 1L;
//唯一主键
@Id
@Field("mail_id")
private String mailId;
@Field("mail_no")
private Long mailNo;
//邮件类型 如:Text表示纯文本、HTML等
@Field("mail_type")
private String mailType;
//邮件发送人
@Field("from_address")
private String fromAddress;
//邮件接收人
@Field("to_address")
private String toAddress;
//CC邮件接收人
@Field("cc_address")
private String ccAddress;
//BC邮件接收人
@Field("bcc_address")
private String bccAddress;
//邮件标题
@Field("subject")
private String subject;
//邮件内容
@Field("mail_body")
private String mailBody;
//发送优先级 如:Normal表示普通
@Field("send_priority")
private String sendPriority;
//处理状态 如:SendWait表示等待发送
@Field("send_status")
private String sendStatus;
//是否有附件
@Field("has_attachment")
private boolean hasAttatchment;
//附件保存的绝对地址,如fastdfs返回的url
@Field("attatchment_urls")
private String[] attatchmentUrls;
//客户端应用编号或名称 如:CRM、订单、财务、运营等
@Field("client_appid")
private String clientAppId;
//是否删除
@Field("has_delete")
private boolean hasDelete;
//发送次数
@Field("retry_count")
private int retryCount;
//创建时间
@Field("create_time")
private Date createTime;
//创建人
@Field("create_user")
private String createUser;
//更新时间
@Field("modify_time")
private Date modifyTime;
//更新人
@Field("modify_user")
private String modifyUser;
//备注
@Field("remark")
private String remark;
//扩展信息
@Field("extend_info")
private String extendInfo;
public String getMailId() {
return mailId;
}
public void setMailId(String mailId) {
this.mailId = mailId;
}
public Long getMailNo() {
return mailNo;
}
public void setMailNo(Long mailNo) {
this.mailNo = mailNo;
}
public String getMailType() {
return mailType;
}
public void setMailType(String mailType) {
this.mailType = mailType;
}
public String getFromAddress() {
return fromAddress;
}
public void setFromAddress(String fromAddress) {
this.fromAddress = fromAddress;
}
public String getToAddress() {
return toAddress;
}
public void setToAddress(String toAddress) {
this.toAddress = toAddress;
}
public String getCcAddress() {
return ccAddress;
}
public void setCcAddress(String ccAddress) {
this.ccAddress = ccAddress;
}
public String getBccAddress() {
return bccAddress;
}
public void setBccAddress(String bccAddress) {
this.bccAddress = bccAddress;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public String getMailBody() {
return mailBody;
}
public void setMailBody(String mailBody) {
this.mailBody = mailBody;
}
public String getSendPriority() {
return sendPriority;
}
public void setSendPriority(String sendPriority) {
this.sendPriority = sendPriority;
}
public String getSendStatus() {
return sendStatus;
}
public void setSendStatus(String sendStatus) {
this.sendStatus = sendStatus;
}
public boolean isHasAttatchment() {
return hasAttatchment;
}
public void setHasAttatchment(boolean hasAttatchment) {
this.hasAttatchment = hasAttatchment;
}
public String[] getAttatchmentUrls() {
return attatchmentUrls;
}
public void setAttatchmentUrls(String[] attatchmentUrls) {
this.attatchmentUrls = attatchmentUrls;
}
public String getClientAppId() {
return clientAppId;
}
public void setClientAppId(String clientAppId) {
this.clientAppId = clientAppId;
}
public boolean isHasDelete() {
return hasDelete;
}
public void setHasDelete(boolean hasDelete) {
this.hasDelete = hasDelete;
}
public int getRetryCount() {
return retryCount;
}
public void setRetryCount(int retryCount) {
this.retryCount = retryCount;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
public String getCreateUser() {
return createUser;
}
public void setCreateUser(String createUser) {
this.createUser = createUser;
}
public Date getModifyTime() {
return modifyTime;
}
public void setModifyTime(Date modifyTime) {
this.modifyTime = modifyTime;
}
public String getModifyUser() {
return modifyUser;
}
public void setModifyUser(String modifyUser) {
this.modifyUser = modifyUser;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
public String getExtendInfo() {
return extendInfo;
}
public void setExtendInfo(String extendInfo) {
this.extendInfo = extendInfo;
}
}
MailDO
同时,还需要注意attatchment_urls这个字段,看上去数组也可以直接存进MongoDB中,毕竟SchemaLess曾经是MongoDB宣传过的比RDBMS最明显的优势之一。
7、邮件接口
package com.power.demo.apiservice.impl;
import com.google.common.collect.Lists;
import com.power.demo.apientity.request.BatchSendEmailRequest;
import com.power.demo.apientity.response.BatchSendEmailResponse;
import com.power.demo.apiservice.contract.MailApiService;
import com.power.demo.common.*;
import com.power.demo.domain.MailDO;
import com.power.demo.entity.vo.MailVO;
import com.power.demo.mongodb.MailDao;
import com.power.demo.service.contract.MailService;
import com.power.demo.util.ConfigUtil;
import com.power.demo.util.FastMapperUtil;
import com.power.demo.util.SerialNumberUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
@Component
public class MailApiServiceImpl implements MailApiService {
@Autowired
private MailService mailService;
@Autowired
private MailDao mailDao;
/**
* 发送邮件
*
* @param request 请求
* @return 发送失败的邮件
**/
public BatchSendEmailResponse batchSendEmail(BatchSendEmailRequest request) {
BatchSendEmailResponse response = new BatchSendEmailResponse();
response.setSuccess("");
if (request == null) {
response.setFail("请求为空");
} else if (request.getMailList() == null || request.getMailList().size() == 0) {
response.setFail("待发送邮件为空");
}
if (response.getIsOK() == false) {
return response;
}
List failedMails = Lists.newArrayList();//没有处理成功的邮件
//构造邮件对象
List allMails = generateMails(request);
failedMails = processSendMail(allMails);
response.setFailMailList(failedMails);
response.setSuccess(String.format("发送邮件提交成功,发送失败的记录为:%d", failedMails.size()));
return response;
}
/**
* 构造待发送邮件 特殊字段赋值
*
* @param request 请求
* @return 发送失败的邮件
**/
private List generateMails(BatchSendEmailRequest request) {
List allMails = Lists.newArrayList();
for (MailVO mail : request.getMailList()) {
if (mail == null) {
continue;
}
//默认字段赋值
mail.setCreateTime(new Date());
mail.setModifyTime(new Date());
mail.setRetryCount(0);
mail.setHasDelete(false);
mail.setMailNo(SerialNumberUtil.create());
if (StringUtils.isEmpty(mail.getMailType())) {
mail.setMailType(MailType.TEXT.toString());
} else if (Arrays.stream(MailType.values()).filter(x -> x.toString().equalsIgnoreCase(mail.getMailType())).count() == 0) {
mail.setMailType(MailType.TEXT.toString());
}
if (StringUtils.isEmpty(mail.getSendStatus())) {
mail.setSendStatus(SendStatusType.SendWait.toString());
} else if (Arrays.stream(SendStatusType.values()).filter(x -> x.toString().equalsIgnoreCase(mail.getSendStatus())).count() == 0) {
mail.setSendStatus(SendStatusType.SendWait.toString());
}
if (StringUtils.isEmpty(mail.getSendPriority())) {
mail.setSendPriority(SendPriorityType.Normal.toString());
} else if (Arrays.stream(SendPriorityType.values()).filter(x -> x.toString().equalsIgnoreCase(mail.getSendPriority())).count() == 0) {
mail.setSendPriority(SendPriorityType.Normal.toString());
}
if (StringUtils.isEmpty(mail.getMailId())) {
mail.setMailId(String.valueOf(SerialNumberUtil.create()));
}
if (StringUtils.isEmpty(mail.getFromAddress())) {
String fromAddr = ConfigUtil.getConfigVal(AppField.MAIL_SENDER_ADDR);
mail.setFromAddress(fromAddr);
}
allMails.add(mail);
}
return allMails;
}
/**
* 处理邮件
*
* @param allMails 所有邮件
* @return 发送失败的邮件
**/
private List processSendMail(List allMails) {
List failedMails = Lists.newArrayList();//没有处理成功的邮件
List asyncMails = Lists.newArrayList();//待异步处理的邮件
for (MailVO mail : allMails) {
if (mail.isSync() == false) { //异步处理
continue;
}
//同步调用
BizResult bizResult = safeSendMail(mail);//发送邮件成功
if (bizResult.getIsOK() == true) {
mail.setSendStatus(SendStatusType.SendSuccess.toString());
mail.setRemark("同步发送邮件成功");
} else {
mail.setSendStatus(SendStatusType.SendFail.toString());
mail.setRemark(String.format("同步发送邮件失败:%s", bizResult.getMessage()));
failedMails.add(mail);
}
}
//批量保存邮件至MongoDB
safeStoreMailList(allMails);
return failedMails;
}
/**
* 发送邮件
*
* @param ent 邮件信息
* @return
**/
private BizResult safeSendMail(MailVO ent) {
BizResult bizSendResult = null;
if (MailType.TEXT.toString().equalsIgnoreCase(ent.getMailType())) {
bizSendResult = mailService.sendSimpleMail(ent);
} else if (MailType.HTML.toString().equalsIgnoreCase(ent.getMailType())) {
bizSendResult = mailService.sendHtmlMail(ent);
}
if (bizSendResult == null) {
bizSendResult = new BizResult(false, AppConst.SUCCESS, "不支持的邮件类型");
}
return bizSendResult;
}
/**
* 批量保存邮件
*
* @param entList 邮件信息列表
* @return
**/
private boolean safeStoreMailList(List entList) {
boolean isOK = storeMailList(entList);
if (isOK == true) {
return isOK;
}
for (int i = 1; i
try {
Thread.sleep(100 * i);
} catch (Exception te) {
te.printStackTrace();
}
isOK = storeMailList(entList);
if (isOK == true) {
break;
}
}
return isOK;
}
/**
* 存储邮件
*
* @param entList 邮件信息列表
* @return
**/
private boolean storeMailList(List entList) {
boolean isOK = false;
try {
List dbEntList = Lists.newArrayList();
entList.forEach(
MailDO dbEnt = FastMapperUtil.cloneObject(x, MailDO.class);
dbEntList.add(dbEnt);
}
);
mailDao.batchInsert(dbEntList);
isOK = true;
} catch (Exception e) {
e.printStackTrace();
}
return isOK;
}
}
MailApiServiceImpl
到这里,MongoDB的主要存储和查询就搞定了。
二、邮件
在上面的邮件接口API实现中,我们定义了邮件发送服务MailService,在Spring Boot中发送邮件也非常简单。
1、邮件配置
2、简单邮件
通过Spring的JavaMailSender对象,可以轻松实现邮件发送。
发送简单邮件代码:
3、HTML邮件
同理,我们经常要发送带格式的HTML邮件,发送代码可以参考如下:
/**
* 发送HTML邮件
*
* @param ent 邮件信息
**/
public BizResult sendHtmlMail(MailVO ent) {
BizResult bizResult = new BizResult(true, AppConst.SUCCESS);
try {
if (ent == null) {
bizResult.setFail("邮件信息为空");
return bizResult;
}
if (StringUtils.isEmpty(ent.getToAddress())) {
bizResult.setFail("HTML邮件,接收人邮箱为空");
return bizResult;
}
//默认发件人设置
if (StringUtils.isEmpty(ent.getFromAddress())) {
ent.setFromAddress(senderAddr);
}
MimeMessage message = mailSender.createMimeMessage();
//true表示需要创建一个multipart message
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setFrom(ent.getFromAddress());
helper.setTo(ent.getToAddress());
helper.setCc(ent.getCcAddress());
helper.setBcc(ent.getBccAddress());
helper.setSubject(ent.getSubject());
helper.setText(ent.getMailBody(), true);//true表示是html邮件
helper.setSentDate(new Date());
//判断有无附件 循环添加附件
if (ent.isHasAttatchment() && ent.getAttatchmentUrls() != null) {
for (String filePath : ent.getAttatchmentUrls()) {
FileSystemResource file = new FileSystemResource(new File(filePath));
String fileName = filePath.substring(filePath.lastIndexOf(File.separator));
helper.addAttachment(fileName, file);
}
}
mailSender.send(message);
bizResult.setSuccess("HTML邮件已经发送");
} catch (Exception e) {
e.printStackTrace();
PowerLogger.error(String.format("发送HTML邮件时发生异常:%s", e));
bizResult.setFail(String.format("发送HTML邮件时发生异常:%s", e));
} finally {
PowerLogger.info(String.format("HTML邮件,发送结果:%s", SerializeUtil.Serialize(bizResult)));
}
return bizResult;
}
sendHtmlMail
邮件附件的处理,本文仅仅是简单示例,实际情况是通常都免不了要上传分布式文件系统,如FastDFS等。
三、MongoDB注意事项
1、常见参数设置问题
MongoDB的默认最大连接数是100,不同的客户端有不同的实现,对于读多写多的应用,最大连接数可能成为瓶颈。
不过设置最大连接数也要注意内存开销,合理配置连接池maxPoolSize。
2、MongoDB事务性
早期版本的MongoDB已经支持行级的事务,支持简单的行级操作原子性,单行的操作要么全部成功,要么全部失败。
MongoDB的WiredTiger引擎本身支持事务,官方在最新版本中,号称完全支持ACID和事务。
3、MongoDB如何提升查询速度
可以选取合适字段创建索引,和RDBMS一样,MongoDB的索引也有很多种,如:单字段索引、复合索引、多Key索引、哈希索引等。
在常见的查询字段上合理添加索引,或者定期归档数据,减少查询数据量,这些手段都可以有效提高查询速度。
还有一种非常常见的手段就是Sharding,也就是数据库分片技术。当数据量比较大的时候,我们需要把数据分片运行在不同的机器中,以降低CPU、内存和IO的压力。MongoDB分片技术类似MySQL的水平切分和垂直切分,主要由两种方式做Sharding:垂直扩展和横向切分。垂直扩展的方式就是进行集群扩展,添加更多的CPU,内存,磁盘空间等。横向切分则是通过数据分片的方式,通过集群统一提供服务。
4、MongoDB的高可用方案
真正的高可用系统,很少有单实例的应用形态存在。
MongoDB支持主从方式、双机双工方式(互备互援)和集群工作方式(多服务器互备方式),减少单点出故障的可能。
领取专属 10元无门槛券
私享最新 技术干货