1. 程式人生 > >補習系列(16)-springboot mongodb 數據庫應用技巧

補習系列(16)-springboot mongodb 數據庫應用技巧

iter tps 排序 代碼 ltm bubuko 報錯 exception 做了

目錄

  • 一、關於 MongoDB
  • 二、Spring-Data-Mongo
  • 三、整合 MongoDB CRUD
    • A. 引入框架
    • B. 數據庫配置
    • C. 數據模型
    • D. 數據操作
  • 三、高級操作
  • 四、高級技巧
    • 1. 連接池配置
    • 2. 去掉_class屬性
    • 3. 自定義序列化
    • 4. 讀寫分離
  • 小結

一、關於 MongoDB

技術分享圖片

MongoDB 目前非常流行,在最近的DB-Engine排名中居第5位,僅次於傳統的關系型數據庫如 Oracle、Mysql。

技術分享圖片

然而在非關系型數據庫領域,MongoDB已經持續成為佼佼者一段時間了,這與 MongoDB的一些優勢存在一定關系:

  • 無模式(Schema),便於快速開發;
  • 面向文檔化的數據,基於BSON格式(類JSON),靈活性強
  • 高性能,得益於其內存計算能力;
  • 副本集、自動分片特性,提供了高可用及水平擴展能力

MongoDB 的主要對象包括數據庫(database)、集合(collection)、文檔對象(document),與關系型數據庫的對應關系如下:

MySql MongoDB
schema database
table collection
record document
column field

與關系型數據庫一樣,MongoDB也支持索引(不支持外鍵),然而其沒有定義固定的列(Column),字段可以是任何類型的值,比如數值、數組或嵌套文檔等。
在最近發布的4.0版本中,MongoDB開始支持事務。可見,在未來這些數據庫之間的差異只會越來越少。

二、Spring-Data-Mongo

Spring-Data-Mongo 是Spring框架對於MongoDB 數據讀寫的ORM 封裝,
與 大家熟悉的 JPA一樣,其在MongoDB-Java-Driver基礎之上做了一些封裝,令應用開發更加簡便。

如下是SpringData 整體框架的一個概要:

技術分享圖片

從上圖中可以看出,SpringData 是基於分層設計的。從下之上,分別是:

  • 數據庫層;
  • 驅動層(JDBC/Driver);
  • ORM層(Repository);

三、整合 MongoDB CRUD

接下來的篇幅,主要針對如何在項目中使用框架進行MongoDB數據庫的讀寫,部分代碼可供參考。

A. 引入框架

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-mongodb</artifactId>
            <version>${spring-boot.version}</version>
        </dependency>

其中 spring-boot-starter-mongodb 是一個膠水組件,聲明對它的依賴會令項目自動引入spring-data-mongomongodb-java-driver等基礎組件。

B. 數據庫配置

我們在 application.properties 中聲明一段配置:

spring.data.mongodb.host=127.0.0.1
spring.data.mongodb.port=27017
spring.data.mongodb.username=appuser
spring.data.mongodb.password=appuser@2016
spring.data.mongodb.database=appdb

不難理解,這裏是數據庫主機、端口、用戶密碼、數據庫的設置。

C. 數據模型

接下來,要定義數據集合(collection) 的一個結構,以 Book實體為例:

@Document(collection = "book")
@CompoundIndexes({ @CompoundIndex(name = "idx_category_voteCount", def = "{'category': 1, 'voteCount': 1}"),
        @CompoundIndex(name = "idx_category_createTime", def = "{'category': 1, 'createTime': 1}") })
public class Book {

    @Id
    private String id;

    @Indexed
    private String author;

    private String category;

    @Indexed
    private String title;

    private int voteCount;
    private int price;

    @Indexed
    private Date publishDate;

    private Date updateTime;
    private Date createTime;
...

這裏,我們給Book 實體定義了一些屬性:

屬性名 描述
id 書籍ID
author 作者
category 書籍分類
title 書籍標題
voteCount 投票數量
price 價格
publishDate 發布日期
updateTime 更新時間
createTime 創建時間

除此以外,我們還會用到幾個註解:

註解 描述
@Document 聲明實體為MongoDB文檔
@Id 標記ID屬性
@Indexed 單鍵索引
@CompoundIndexes 復合索引集
@CompoundIndex 復合索引

關於MongoDB索引形態,可以參考官方文檔做一個詳細了解。

D. 數據操作

ORM 框架可以讓你通過操作對象來直接影響數據,這樣一來,可以大大減少上手的難度,你不再需要熟悉大量驅動層的API了。
Spring-Data-Mongo 實現了類JPA的接口,通過預定義好的Repository可實現代碼方法到數據庫操作語句DML的映射。
下面是一些例子:

  • BookRepository
public interface BookRepository extends MongoRepository<Book, String> {

    public List<Book> findByAuthor(String author);

    public List<Book> findByCategory(String category, Pageable pageable);

    public Book findOneByTitle(String title);
}

我們所看到的 findByAttribute 將會直接被轉換成對應的條件查詢,如 findByAuthor 等價於

db.book.find({author:'Lilei'})

接下來,我們可以方便的在業務邏輯層(service層) 對Repository 進行調用,如下:

@Service
public class BookService {

    @Autowired
    private BookRepository bookRepository;

    private static final Logger logger = LoggerFactory.getLogger(BookService.class);

    /**
     * 創建book
     * 
     * @param category
     * @param title
     * @param author
     * @param price
     * @param publishDate
     * @return
     */
    public Book createBook(String category, String title, String author, int price, Date publishDate) {
        if (StringUtils.isEmpty(category) || StringUtils.isEmpty(title) || StringUtils.isEmpty(author)) {
            return null;
        }

        Book book = new Book();
        book.setAuthor(author);
        book.setTitle(title);
        book.setCategory(category);
        book.setPrice(price);
        book.setPublishDate(publishDate);

        book.setVoteCount(0);
        book.setCreateTime(new Date());
        book.setUpdateTime(book.getCreateTime());

        return bookRepository.save(book);
    }

    /**
     * 更新價格
     * 
     * @param id
     * @param price
     * @return
     */
    public boolean updatePrice(String id, int price) {
        if (StringUtils.isEmpty(id)) {
            return false;
        }

        Book book = bookRepository.findOne(id);
        if (book == null) {

            logger.info("the book '{}' is not exist", id);
            return false;
        }

        book.setPrice(price);
        book.setUpdateTime(new Date());
        if (bookRepository.save(book) != null) {
            return true;
        }
        return false;
    }

    /**
     * 根據獲取book
     * 
     * @param title
     * @return
     */
    public Book getBookByTitle(String title) {
        if (StringUtils.isEmpty(title)) {
            return null;
        }
        return bookRepository.findOneByTitle(title);
    }

    /**
     * 獲取投票排行列表
     * 
     * @param category
     * @param max
     * @return
     */
    public List<Book> listTopVoted(String category, int max) {

        if (StringUtils.isEmpty(category) || max <= 0) {
            return Collections.emptyList();
        }

        // 按投票數倒序排序
        Sort sort = new Sort(Direction.DESC, Book.COL_VOTE_COUNT);
        PageRequest request = new PageRequest(0, max, sort);

        return bookRepository.findByCategory(category, request);

    }

    /**
     * 刪除書籍
     * 
     * @param id
     * @return
     */
    public boolean deleteBook(String id) {
        Book book = bookRepository.findOne(id);
        if (book == null) {

            logger.info("the book '{}' is not exist", id);
            return false;
        }

        bookRepository.delete(book);
        return true;
    }

}

關於Repository 映射規則,可以從這裏找到詳細介紹。

三、高級操作

有時候,Repository的方法映射無法較好的滿足一些特定場景,比如高級檢索、局部更新、覆蓋索引查詢等等,
此時可以使用框架提供的 MongoTemplate 工具類來完成這些定制,MongoTemplate 提供了大量的 Criteria API 來封裝 Mongo-Java-Driver的實現。
我們一方面可以選擇直接使用該API,另一方面,則可以更加"優雅"的整合到Repository 接口,如下面的代碼:

  • 聲明 Custom 接口
public interface BookRepositoryCustom {

    public PageResult<Book> search(String category, String title, String author, Date publishDataStart,
            Date publishDataEnd, Pageable pageable);

    public boolean incrVoteCount(String id, int voteIncr);
}
  • 聲明接口繼承關系
public interface BookRepository extends MongoRepository<Book, String>, BookRepositoryCustom{
  • 實現類
public class BookRepositoryImpl implements BookRepositoryCustom {

    @Autowired
    private MongoTemplate mongoTemplate;

    public boolean incrVoteCount(String id, int voteIncr) {
        if (StringUtils.isEmpty(id)) {
            return false;
        }

        Query query = new Query();
        query.addCriteria(Criteria.where("id").is(id));

        Update update = new Update();

        update.inc(Book.COL_VOTE_COUNT, voteIncr);
        update.set(Book.COL_UPDATE_TIME, new Date());

        WriteResult result = mongoTemplate.updateFirst(query, update, Book.class);
        return result != null && result.getN() > 0;
    }

    @Override
    public PageResult<Book> search(String category, String title, String author, Date publishDataStart,
            Date publishDataEnd, Pageable pageable) {
        Query query = new Query();

        if (!StringUtils.isEmpty(category)) {
            query.addCriteria(Criteria.where(Book.COL_CATEGORY).is(category));
        }

        if (!StringUtils.isEmpty(author)) {
            query.addCriteria(Criteria.where(Book.COL_AUTHOR).is(author));
        }

        if (!StringUtils.isEmpty(title)) {
            query.addCriteria(Criteria.where(Book.COL_TITLE).regex(title));
        }

        if (publishDataStart != null || publishDataEnd != null) {
            Criteria publishDateCond = Criteria.where(Book.COL_PUBLISH_DATE);

            if (publishDataStart != null) {
                publishDateCond.gte(publishDataStart);
            }
            if (publishDataEnd != null) {
                publishDateCond.lt(publishDataEnd);
            }
            query.addCriteria(publishDateCond);
        }

        long totalCount = mongoTemplate.count(query, Book.class);
        if (totalCount <= 0) {
            return new PageResult<Book>();
        }

        if (pageable != null) {
            query.with(pageable);
        }

        List<Book> books = mongoTemplate.find(query, Book.class);
        return PageResult.of(totalCount, books);
    }
}

利用 AOP的魔法 ,Spring 框架會自動將我們這段代碼實現織入 到Bean對象中,
這樣一來,我們原先對Repository的依賴引用方式就不需要改變了。

四、高級技巧

SpringBoot中完成Mongodb的自動化配置,是通過MongoAutoConfiguration、MongoDataAutoConfiguration完成的。

其中MongoAutoConfiguration的實現如下:

@Configuration
@ConditionalOnClass(MongoClient.class)
@EnableConfigurationProperties(MongoProperties.class)
@ConditionalOnMissingBean(type = "org.springframework.data.mongodb.MongoDbFactory")
public class MongoAutoConfiguration {

    private final MongoProperties properties;

    private final MongoClientOptions options;

    private final Environment environment;

    private MongoClient mongo;

    public MongoAutoConfiguration(MongoProperties properties,
            ObjectProvider<MongoClientOptions> options, Environment environment) {
        this.properties = properties;
        this.options = options.getIfAvailable();
        this.environment = environment;
    }

    @PreDestroy
    public void close() {
        if (this.mongo != null) {
            this.mongo.close();
        }
    }

    @Bean
    @ConditionalOnMissingBean
    public MongoClient mongo() throws UnknownHostException {
        this.mongo = this.properties.createMongoClient(this.options, this.environment);
        return this.mongo;
    }

}

從上面的代碼可見,如果應用代碼中未聲明 MongoClient、MongoDbFactory,那麽框架會根據配置文件自動做客戶端的初始化。
通過聲明,可以取消這些自動化配置:

@SpringBootApplication
@EnableAutoConfiguration(exclude = { EmbeddedMongoAutoConfiguration.class, MongoDataAutoConfiguration.class,
        MongoAutoConfiguration.class })
public class DemoBoot {
...

真實線上的項目中,會對MongoDB 客戶端做一些定制,下面的介紹幾個用法

1. 連接池配置

@Configuration
public class MongoConfig {

    @Bean
    public MongoDbFactory mongoFactory(MongoProperties mongo) throws Exception {

        MongoClientOptions.Builder builder = new MongoClientOptions.Builder();
        // 連接池配置
        builder.maxWaitTime(1000 * 60 * 1).socketTimeout(30 * 1000).connectTimeout(10 * 1000).connectionsPerHost(60)
                .minConnectionsPerHost(60).socketKeepAlive(true);

        // 設置鑒權信息
        MongoCredential credential = null;
        if (!StringUtils.isEmpty(mongo.getUsername())) {
            credential = MongoCredential.createCredential(mongo.getUsername(), mongo.getDatabase(),
                    mongo.getPassword());
        }

        MongoClientOptions mongoOptions = builder.build();

        List<ServerAddress> addrs = Arrays.asList(new ServerAddress(mongo.getHost(), mongo.getPort()));
        MongoClient mongoClient = null;
        if (credential != null) {
            mongoClient = new MongoClient(addrs, Arrays.asList(credential), mongoOptions);
        } else {
            mongoClient = new MongoClient(addrs, mongoOptions);
        }
        return new SimpleMongoDbFactory(mongoClient, mongo.getDatabase());
    }

我們所關心的,往往是連接池大小、超時參數閾值、隊列這幾個,如下:

//連接池最小值
private int minConnectionsPerHost;
//連接池最大值
private int maxConnectionsPerHost = 100;
//線程等待連接阻塞系數
private int threadsAllowedToBlockForConnectionMultiplier = 5;
//選擇主機超時
private int serverSelectionTimeout = 1000 * 30;
//最大等待
private int maxWaitTime = 1000 * 60 * 2;
//最大連接閑時
private int maxConnectionIdleTime;
//最大連接存活
private int maxConnectionLifeTime;
//TCP建立連接超時
private int connectTimeout = 1000 * 10;
//TCP讀取超時
private int socketTimeout = 0;
//TCP.keepAlive是否啟用
private boolean socketKeepAlive = true;
//心跳頻率
private int heartbeatFrequency = 10000;
//最小心跳間隔
private int minHeartbeatFrequency = 500;
//心跳TCP建立連接超時
private int heartbeatConnectTimeout = 20000;
//心跳TCP讀取超時
private int heartbeatSocketTimeout = 20000;

2. 去掉_class屬性

通過 SpringDataMongo 定義的實體,會自動寫入一個***_class***屬性,大多數情況下這個不是必須的,可以通過配置去掉:

@Bean
    public MongoTemplate mongoTemplate(MongoDbFactory mongoDbFactory, MongoMappingContext context) {

        DbRefResolver dbRefResolver = new DefaultDbRefResolver(mongoDbFactory);
        MappingMongoConverter converter = new MappingMongoConverter(dbRefResolver, context);
        converter.setTypeMapper(new DefaultMongoTypeMapper(null));

        converter.afterPropertiesSet();

        MongoTemplate mongoTemplate = new MongoTemplate(mongoDbFactory, converter);

        return mongoTemplate;
    }

3. 自定義序列化

一些基礎的字段類型,如 int 、long、string,通過JDK 裝箱類就可以完成,
對於內嵌的對象類型,SpringDataMongo框架會將其轉換為 DBObject對象(java driver 實體)。
一般情況下這已經足夠了,但某些場景下你不得不實現自己的序列化方式,比如通過文檔存儲某些特殊格式的內容。

這需要用到 Converter 接口,如下面的代碼:

   @Bean
    public MongoTemplate mongoTemplate(MongoDbFactory mongoDbFactory, MongoMappingContext context) {

        DbRefResolver dbRefResolver = new DefaultDbRefResolver(mongoDbFactory);
        MappingMongoConverter converter = new MappingMongoConverter(dbRefResolver, context);
        converter.setTypeMapper(new DefaultMongoTypeMapper(null));

        // 自定義轉換
        converter.setCustomConversions(customConversions());
        converter.afterPropertiesSet();

        MongoTemplate mongoTemplate = new MongoTemplate(mongoDbFactory, converter);
        return mongoTemplate;
    }

    private CustomConversions customConversions() {
        List<Converter<?, ?>> converters = new ArrayList<Converter<?, ?>>();
        converters.add(new BasicDBObjectWriteConverter());
        converters.add(new BasicDBObjectReadConverter());
        return new CustomConversions(converters);
    }

    /**
     * 寫入序列化
     */
    @WritingConverter
    public static class BasicDBObjectWriteConverter implements Converter<BasicDBObject, String> {

        public String convert(BasicDBObject source) {
            if (source == null) {
                return null;
            }
            return source.toJson();
        }
    }

    /**
     * 讀取反序列化
     */
    @ReadingConverter
    public static class BasicDBObjectReadConverter implements Converter<String, BasicDBObject> {

        public BasicDBObject convert(String source) {
            if (source == null || source.length() <= 0) {
                return null;
            }
            return BasicDBObject.parse(source);
        }
    }

4. 讀寫分離

MongoDB 本身支持讀寫分離的實現,前提是采用副本集、分片副本集的架構,
通過聲明客戶端的 ReadPreference 級別可以達到優先讀主、優先讀備的控制。

@Configuration
public class MongoConfig {

    @Bean(name="secondary")
    public MongoDbFactory mongoFactory(MongoProperties mongo) throws Exception {

        MongoClientOptions.Builder builder = new MongoClientOptions.Builder();
        // 連接池配置
        builder.maxWaitTime(1000 * 60 * 1).socketTimeout(30 * 1000).connectTimeout(10 * 1000).connectionsPerHost(60)
                .minConnectionsPerHost(60).socketKeepAlive(true);
        // 優先讀備節點
        builder.readPreference(ReadPreference.secondaryPreferred());
        ...

上面的代碼中,將會為MongoClient 設置 secondaryPreferred 的讀級別。
ReadPreference 級別包括以下幾種:

級別 描述
primary 默認值,只從主節點讀,主節點不可用時報錯
primaryPreferred 優先主節點(primary)讀,主節點不可用時到從節點(secondary)讀
secondary 僅從備節點(secondary)讀取數據
secondaryPreferred 優先從備節點讀,從節點不可用時到主節點讀取
nearest 到網絡延遲最低的節點讀取數據,不管是主節點還是從節點

Gitee同步代碼

小結

MongoDB 是當下 NoSQL 數據庫的首選,也有不少服務化架構采用了 MongoDB作為主要數據庫,
其在 4.x版本中即將推出事務功能,在未來該文檔數據庫相對於RDBMS的差距將會大大縮小。
也正由於MongoDB 具備 簡單、易擴展、高性能等特性,其社區活躍度非常高,是非常值得關註和學習的。

歡迎繼續關註"美碼師的補習系列-springboot篇" ,期待更多精彩內容^-^

補習系列(16)-springboot mongodb 數據庫應用技巧