本文繼續上一篇定時任務中提到的郵件服務,簡單講解Spring Boot中如何使用MongoDB進行應用開發。
上文中提到的這個簡易郵件系統大致設計思路如下:
1、發送郵件支持同步和異步發送兩種
2、郵件使用MongDB進行持久化保存
3、異步發送,直接將郵件批量保存在MongoDB中,然后通過后臺定時任務發送
4、同步發送,先調用Spring的發送郵件功能,接著將郵件批量保存至MongDB
5、不論同步還是異步,郵件發送失敗,定時任務可配置為進行N次重試
一、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、添加依賴


<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-mongodb</artifactId></dependency>
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<MailDO> entList);/*** 創建對象** @param ent*/void insert(MailDO ent);/*** 根據ID查詢對象** @param mailId* @return*/MailDO findByMailId(Long mailId);/*** 查詢一段時間范圍內待發送的郵件** @param startTime 開始時間* @param endTime 結束時間* @return*/List<MailDO> findToSendList(Date startTime, Date endTime);/*** 更新** @param ent*/void update(MailDO ent);/*** 刪除** @param mailId*/void delete(Long mailId);}
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 {@Autowiredprivate MongoTemplate mongoTemplate;public void batchInsert(List<MailDO> entList) {//分組批量多次插入 每次2000條List<List<MailDO>> groupList = CollectionHelper.spliceArrays(entList, AppConst.BATCH_RECORD_COUNT);for (List<MailDO> 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<MailDO> 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<MailDO> 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);} }
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;} }
請大家注意實體上的注解,@Document(collection = "mailinfo")將會在文檔數據庫中創建一個mailinfo的表,@Id表示指定該字段為主鍵, @Field("mail_no")表示實體字段mailNo存在MongoDB中的字段名稱為mail_no。
根據MongoDB官方文檔介紹,如果在插入數據時沒有指定主鍵,MongoDB會自動給插入行自動加上一個主鍵_id,MongoDB客戶端把這個id類型稱為ObjectId,看上去就是一個UUID。我們可以通過注解自己設置主鍵類型,但是根據實踐,_id名稱是無法改變的。@Id和 @Field("mail_id")表面看上去是我想創建一個mail_id為主鍵的表,但是實際主鍵只有_id而沒有mail_id。當然,主鍵的類型不一定非要是UUID,可以是你自己根據業務生成的唯一流水號等等。
同時,還需要注意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 {@Autowiredprivate MailService mailService;@Autowiredprivate 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<MailVO> failedMails = Lists.newArrayList();//沒有處理成功的郵件//構造郵件對象List<MailVO> allMails = generateMails(request);failedMails = processSendMail(allMails);response.setFailMailList(failedMails);response.setSuccess(String.format("發送郵件提交成功,發送失敗的記錄為:%d", failedMails.size()));return response;}/*** 構造待發送郵件 特殊字段賦值** @param request 請求* @return 發送失敗的郵件**/private List<MailVO> generateMails(BatchSendEmailRequest request) {List<MailVO> 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<MailVO> processSendMail(List<MailVO> allMails) {List<MailVO> failedMails = Lists.newArrayList();//沒有處理成功的郵件 List<MailVO> asyncMails = Lists.newArrayList();//待異步處理的郵件for (MailVO mail : allMails) {if (mail.isSync() == false) { //異步處理continue;}//同步調用BizResult<String> 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<String> safeSendMail(MailVO ent) {BizResult<String> 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<MailVO> entList) {boolean isOK = storeMailList(entList);if (isOK == true) {return isOK;}for (int i = 1; i <= AppConst.MAX_RETRY_COUNT; 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<MailVO> entList) {boolean isOK = false;try {List<MailDO> dbEntList = Lists.newArrayList();entList.forEach(x -> {MailDO dbEnt = FastMapperUtil.cloneObject(x, MailDO.class);dbEntList.add(dbEnt);});mailDao.batchInsert(dbEntList);isOK = true;} catch (Exception e) {e.printStackTrace();}return isOK;}}
到這里,MongoDB的主要存儲和查詢就搞定了。
二、郵件
在上面的郵件接口API實現中,我們定義了郵件發送服務MailService,在Spring Boot中發送郵件也非常簡單。
1、郵件配置


## 郵件配置 spring.mail.host=smtp.xxx.com //郵箱服務器地址 spring.mail.username=abc@xxx.com //用戶名 spring.mail.password=123456 //密碼 spring.mail.default-encoding=UTF-8 mail.sender.addr=abc@company.com //發送者郵箱
2、簡單郵件
通過Spring的JavaMailSender對象,可以輕松實現郵件發送。
發送簡單郵件代碼:


/*** 發送簡單文本郵件** @param ent 郵件信息**/public BizResult<String> sendSimpleMail(MailVO ent) {BizResult<String> bizResult = new BizResult<>(true, AppConst.SUCCESS);try {if (ent == null) {bizResult.setFail("郵件信息為空");return bizResult;}if (StringUtils.isEmpty(ent.getToAddress())) {bizResult.setFail("簡單郵件,接收人郵箱為空");return bizResult;}//默認發件人設置if (StringUtils.isEmpty(ent.getFromAddress())) {ent.setFromAddress(senderAddr);}SimpleMailMessage message = new SimpleMailMessage();message.setFrom(ent.getFromAddress());message.setTo(ent.getToAddress());message.setCc(ent.getCcAddress());message.setBcc(ent.getBccAddress());message.setSubject(ent.getSubject());message.setText(ent.getMailBody());message.setSentDate(new Date());mailSender.send(message);bizResult.setSuccess("簡單郵件已經發送");} catch (Exception e) {e.printStackTrace();PowerLogger.error(String.format("發送簡單郵件時發生異常:%s", e));bizResult.setFail(String.format("發送簡單郵件時發生異常:%s", e));} finally {PowerLogger.info(String.format("簡單郵件,發送結果:%s", SerializeUtil.Serialize(bizResult)));}return bizResult;}
3、HTML郵件
同理,我們經常要發送帶格式的HTML郵件,發送代碼可以參考如下:


/*** 發送HTML郵件** @param ent 郵件信息**/public BizResult<String> sendHtmlMail(MailVO ent) {BizResult<String> 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 messageMimeMessageHelper 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;}
郵件附件的處理,本文僅僅是簡單示例,實際情況是通常都免不了要上傳分布式文件系統,如FastDFS等,有空我會繼續寫一下Spring Boot和分布式文件系統的應用實踐。
還記得上一篇文章里的定時任務發送郵件嗎?貼一下MailServiceImpl下的補償發送實現:


/*** 自動查詢并發送郵件** @param startTime 開始時間* @param endTime 結束時間* @return**/public void autoSend(Date startTime, Date endTime) {StopWatch watch = DateTimeUtil.StartNew();List<MailDO> mailDOList = mailDao.findToSendList(startTime, endTime);for (MailDO dbEnt : mailDOList) {MailVO ent = FastMapperUtil.cloneObject(dbEnt, MailVO.class);BizResult<String> bizSendResult = null;if (MailType.TEXT.toString().equalsIgnoreCase(ent.getMailType())) {bizSendResult = sendSimpleMail(ent);} else if (MailType.HTML.toString().equalsIgnoreCase(ent.getMailType())) {bizSendResult = sendHtmlMail(ent);}if (bizSendResult == null) {bizSendResult = new BizResult<>(false, AppConst.SUCCESS, "不支持的郵件類型");}if (bizSendResult.getIsOK() == true) {dbEnt.setSendStatus(SendStatusType.SendSuccess.toString());} else {dbEnt.setSendStatus(SendStatusType.SendFail.toString());}dbEnt.setRetryCount(dbEnt.getRetryCount() + 1);//重試次數+1 dbEnt.setRemark(SerializeUtil.Serialize(bizSendResult));dbEnt.setModifyTime(new Date());dbEnt.setModifyUser("QuartMailTask");mailDao.update(dbEnt);}watch.stop();PowerLogger.info(String.format("本次共處理記錄數:%s,總耗時:%s", mailDOList.size(), watch.getTotalTimeMillis()));}
這里貼出來的示例代碼是線性的一個一個發送郵件,我們完全可以改造成多線程的并行處理方式來提升郵件發送處理能力。
三、MongoDB注意事項
1、常見參數設置問題
MongoDB的默認最大連接數是100,不同的客戶端有不同的實現,對于讀多寫多的應用,最大連接數可能成為瓶頸。
不過設置最大連接數也要注意內存開銷,合理配置連接池maxPoolSize。
其中,生產環境為了保證高可用,通常會配置副本集連接字符串格式mongodb://username:password@host1:port1,host2:port2[,...,hostN:portN]/database?options
options 是連接配置中的可選項,replicaSet 是其中的一個子項。
最終的配置連接串可能形如:mongodb://username:password@host1:port1,host2:port2[,...,hostN:portN]/database?replicaSet=yourreplset&maxPoolSize=512
批量插入可以減少數據向服務器提交次數,提高性能,但是批量提交的BSON不能超過48M,不注意這個細節很容易造成數據丟失。
關于常用連接參數,可以參考這里。
2、MongoDB事務性
早期版本的MongoDB已經支持行級的事務,支持簡單的行級操作原子性,單行的操作要么全部成功,要么全部失敗。
MongoDB的WiredTiger引擎本身支持事務,官方在最新版本中,號稱完全支持ACID和事務。
3、MongoDB如何提升查詢速度
可以選取合適字段創建索引,和RDBMS一樣,MongoDB的索引也有很多種,如:單字段索引、復合索引、多Key索引、哈希索引等。
在常見的查詢字段上合理添加索引,或者定期歸檔數據,減少查詢數據量,這些手段都可以有效提高查詢速度。
還有一種非常常見的手段就是Sharding,也就是數據庫分片技術。當數據量比較大的時候,我們需要把數據分片運行在不同的機器中,以降低CPU、內存和IO的壓力。MongoDB分片技術類似MySQL的水平切分和垂直切分,主要由兩種方式做Sharding:垂直擴展和橫向切分。垂直擴展的方式就是進行集群擴展,添加更多的CPU,內存,磁盤空間等。橫向切分則是通過數據分片的方式,通過集群統一提供服務。
4、MongoDB的高可用方案
高可用是絕大多數數據庫管理系統的核心目標之一。真正的高可用系統,很少有單實例的應用形態存在。
MongoDB支持主從方式、雙機雙工方式(互備互援)和集群工作方式(多服務器互備方式),減少單點出故障的可能。
如果要想生產數據在發生故障后依然可用,就需要確保為生產數據庫多部署一臺服務器。MongoDB副本集提供了數據的保護、高可用和災難恢復的機制。在MongoDB 中,有兩種數據冗余方式,一種是 Master-Slave 模式(主從復制),一種是 Replica Sets 模式(副本集)。主從復制和副本集使用了相同的復制機制,但是副本集額外增加了自動化災備機制:如果主節點宕機,其中一個從節點會自動提升為從節點。除此之外,副本集還提供了其他改進,比如更易于恢復和更復雜地部署拓撲網絡。集群中沒有特定的主庫,主庫是選舉產生,如果主庫 down 了,會再選舉出一臺主庫。
?
參考:
<<MongoDB權威指南>>
https://docs.mongodb.com/
http://www.runoob.com/mongodb/mongodb-tutorial.html
https://www.cnblogs.com/binyue/p/5901328.html
https://yq.aliyun.com/articles/33726
https://yq.aliyun.com/articles/66623
http://www.cnblogs.com/l1pe1/p/7871790.html