SpringBoot 2.2.5 整合Sharding-JDBC 4.1.1 自定義復(fù)合分片算法進行分庫分表,并配置主從分離

說明

  1. 頂頂大名的分庫分表中間件,廢話不多說,官網(wǎng)地址:https://shardingsphere.apache.org/
  2. 本文中數(shù)據(jù)庫用的是mysql5.7,并且實現(xiàn)了一主一從。
  3. 場景是訂單表的分表,并且要支持只根據(jù)user_id進行查詢的場景,所以要將用戶的標(biāo)識信息放到主鍵order_id中,這樣才能既能只根據(jù)主鍵order_id進行查詢,又能只根據(jù)user_id進行查詢。
  4. 順便支持一下主從分離,這個比較簡單,加一下配置即可
  5. 完整代碼地址在結(jié)尾!!

官方簡介

  1. 定位為輕量級Java框架,在Java的JDBC層提供的額外服務(wù)。 它使用客戶端直連數(shù)據(jù)庫,以jar包形式提供服務(wù),無需額外部署和依賴,可理解為增強版的JDBC驅(qū)動,完全兼容JDBC和各種ORM框架。
  2. 適用于任何基于JDBC的ORM框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template或直接使用JDBC。
  3. 支持任何第三方的數(shù)據(jù)庫連接池,如:DBCP, C3P0, BoneCP, Druid, HikariCP等。
  4. 支持任意實現(xiàn)JDBC規(guī)范的數(shù)據(jù)庫。目前支持MySQL,Oracle,SQLServer,PostgreSQL以及任何遵循SQL92標(biāo)準(zhǔn)的數(shù)據(jù)庫。
image.png

第一步,在pom.xml加入依賴,如下

<!-- MySQL驅(qū)動 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
<!-- mybatisPlus 核心庫 -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.3.1</version>
</dependency>
<!-- sharding-jdbc -->
<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
    <version>4.1.1</version>
</dependency>
<!-- hutool工具 -->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.7.5</version>
</dependency>

注:

  1. 本文的ORM框架使用的是MyBatis-Plus。
  2. hutool工具用于生成自定義id。

第二步,在application.yml配置shardingsphere,mybatis-plus相關(guān)配置

spring:
  application:
    name: shardingjdbc-demo-server
  shardingsphere:
    datasource:
      # 數(shù)據(jù)源
      names: master,salve
      master:
        driver-class-name: com.mysql.cj.jdbc.Driver
        password: root
        type: com.zaxxer.hikari.HikariDataSource
        jdbc-url: jdbc:mysql://xxx:3306/db1
        username: root
      salve:
        driver-class-name: com.mysql.cj.jdbc.Driver
        password: root
        type: com.zaxxer.hikari.HikariDataSource
        jdbc-url: jdbc:mysql://xxx:3306/db2
        username: root
    sharding:
      # 主從分離
      master-slave-rules:
        master:
          master-data-source-name: master
          slave-data-source-names: salve
      # 表分片
      tables:
        my_order:
          # 主表分片規(guī)則表名
          actual-data-nodes: master.my_order_$->{0..3}
          # 主鍵策略
#          key-generator:
#            column: id
#            type: MyShardingKeyGenerator
          table-strategy:
            # 行表達式分片
#            inline:
#              algorithm-expression: order_$->{id.longValue() % 4}
#              sharding-column: id
            # 標(biāo)準(zhǔn)分片
#            standard:
#              sharding-column: id
              # 指定自定義分片算法類的全路徑
#              precise-algorithm-class-name: com.jinhx.shardingjdbc.config.MyPreciseShardingAlgorithm
            # 復(fù)合分片
            complex:
              # 分片鍵
              sharding-columns: order_id,user_id
              # 指定自定義分片算法類的全路徑
              algorithm-class-name: com.jinhx.shardingjdbc.config.MyComplexKeysShardingAlgorithm
#          defaultTableStrategy:
    # 打開sql控制臺輸出日志
    props:
      sql:
        show: true

# mybatis-plus相關(guān)配置
mybatis-plus:
  # xml掃描,多個目錄用逗號或者分號分隔(告訴 Mapper 所對應(yīng)的 XML 文件位置)
  mapper-locations: classpath:com/jinhx/shardingjdbc/mapper/xml/*.xml
  # 別名包掃描路徑,通過該屬性可以給包中的類注冊別名
  type-aliases-package: com.jinhx.shardingjdbc.entity
  configuration:
    # 不開啟二級緩存
    cache-enabled: false
    # 是否開啟自動駝峰命名規(guī)則映射:從數(shù)據(jù)庫列名到Java屬性駝峰命名的類似映射
    map-underscore-to-camel-case: true
    # 如果查詢結(jié)果中包含空值的列,則 MyBatis 在映射的時候,不會映射這個字段
    call-setters-on-nulls: true
    # 這個配置會將執(zhí)行的sql打印出來,在開發(fā)或測試的時候可以用
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

server:
  port: 8093

第三步,在主庫創(chuàng)建數(shù)據(jù)庫db1,創(chuàng)建訂單表,如下

說明

  1. 本文不進行分庫,只分4個表,分別為my_order_0,my_order_1,my_order_2,my_order_3

sql

CREATE DATABASE db1;

use db1;

create table my_order_0
(
    order_id bigint not null comment '訂單id主鍵'
        primary key,
    user_id  bigint not null comment '用戶id',
    money    bigint not null comment '金額'
)
    comment '用戶訂單表';

 其他表結(jié)構(gòu)一致,此處省略

第四步,創(chuàng)建表操作相應(yīng)類

  1. 使用mybatis-plus的代碼生成器對數(shù)據(jù)庫的表生成相應(yīng)的類,不懂的請參考另外一篇文章-SpringBoot 2.2.5 整合MyBatis-Plus 3.3.1 教程,配置多數(shù)據(jù)源并支持事務(wù),附帶代碼生成器使用教程
  2. 手動創(chuàng)建,包括Order,IOrderService,OrderServiceImpl等,如下

Order

package com.jinhx.shardingjdbc.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.jinhx.shardingjdbc.util.SnowFlakeUtil;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

import java.io.Serializable;
import java.util.Objects;

/**
 * Order
 *
 * @author jinhx
 * @since 2021-07-27
 */
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("my_order")
public class Order implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 分表的數(shù)量,一定要2的n次方
     */
    public static final int TABLE_COUNT = 4;

    /**
     * 訂單id主鍵
     */
    @TableId(type = IdType.INPUT)
    private Long orderId;

    /**
     * 用戶id
     */
    private Long userId;

    /**
     * 金額
     */
    private Long money;

    public void buildOrderId(){
        if (Objects.isNull(this.userId)){
            throw new RuntimeException("userId為空,無法生成orderId");
        }
        this.orderId = SnowFlakeUtil.getSnowflakeId(SnowFlakeUtil.getDataCenterId(this.userId) & (TABLE_COUNT - 1));
    }

    public void buildUserId(Integer dataCenterId){
        if (Objects.isNull(dataCenterId)){
            throw new RuntimeException("dataCenterId為空,無法生成userId");
        }
        this.userId = SnowFlakeUtil.getSnowflakeId(dataCenterId & (TABLE_COUNT - 1));
    }

}

IOrderService

package com.jinhx.shardingjdbc.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.jinhx.shardingjdbc.entity.Order;

import java.util.List;

/**
 * IOrderService
 *
 * @author jinhx
 * @since 2021-07-27
 */
public interface IOrderService extends IService<Order> {

    /**
     * 根據(jù)orderIds查詢
     *
     * @param orderIds orderIds
     * @return List<Order>
     */
    List<Order> selectByOrderIds(List<Long> orderIds);

    /**
     * 根據(jù)userIds查詢
     *
     * @param userIds userIds
     * @return List<Order>
     */
    List<Order> selectByUserIds(List<Long> userIds);

    /**
     * 批量插入
     *
     * @param orders orders
     */
    void insertOrders(List<Order> orders);

}

OrderServiceImpl

package com.jinhx.shardingjdbc.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jinhx.shardingjdbc.entity.Order;
import com.jinhx.shardingjdbc.mapper.OrderMapper;
import com.jinhx.shardingjdbc.service.IOrderService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

/**
 * OrderServiceImpl
 *
 * @author jinhx
 * @since 2021-07-27
 */
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements IOrderService {

    /**
     * 根據(jù)orderIds查詢
     *
     * @param orderIds orderIds
     * @return List<Order>
     */
    @Override
    public List<Order> selectByOrderIds(List<Long> orderIds) {
        return baseMapper.selectBatchIds(orderIds);
    }

    /**
     * 根據(jù)userIds查詢
     *
     * @param userIds userIds
     * @return List<Order>
     */
    @Override
    public List<Order> selectByUserIds(List<Long> userIds) {
        return baseMapper.selectList(new LambdaQueryWrapper<Order>()
                .in(CollectionUtils.isNotEmpty(userIds), Order::getUserId, userIds));
    }

    /**
     * 批量插入
     *
     * @param orders orders
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void insertOrders(List<Order> orders) {
        if (CollectionUtils.isNotEmpty(orders)){
            if (orders.stream().mapToInt(item -> baseMapper.insert(item)).sum() != orders.size()){
                log.error("批量插入order表失敗 orders={}" + orders);
                throw new RuntimeException("批量插入order表失敗");
            }
        }
    }

}

第五步,配置MybatisPlus,如下

1. 在啟動類MybatisplusApplication新增@MapperScan注解,里面寫入生成的文件中的mapper存放的路徑,用于掃描mapper文件。

package com.jinhx.shardingjdbc;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@MapperScan("com.jinhx.shardingjdbc.mapper")
@SpringBootApplication
public class ShardingjdbcApplication {

    public static void main(String[] args) {
        SpringApplication.run(ShardingjdbcApplication.class, args);
    }

}

2. 創(chuàng)建MybatisPlus配置類,MybatisPlusConfig,主要是配置一些插件的使用,此步可省略

package com.jinhx.shardingjdbc.config;

import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import com.baomidou.mybatisplus.extension.plugins.pagination.optimize.JsqlParserCountOptimize;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;

/**
 * Mybatis-Plus配置類
 *
 * @author jinhx
 * @since 2021-07-27
 */
@EnableTransactionManagement
@Configuration
public class MybatisPlusConfig {

    /**
     * mybatis-plus SQL執(zhí)行效率插件【生產(chǎn)環(huán)境可以關(guān)閉】,設(shè)置 dev test 環(huán)境開啟
     */
//    @Bean
//    @Profile({"dev", "qa"})
//    public PerformanceInterceptor performanceInterceptor() {
//        PerformanceInterceptor performanceInterceptor = new PerformanceInterceptor();
//        performanceInterceptor.setMaxTime(1000);
//        performanceInterceptor.setFormat(true);
//        return performanceInterceptor;
//    }

    /**
     * 分頁插件
     */
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
        // 設(shè)置請求的頁面大于最大頁后操作, true調(diào)回到首頁,false 繼續(xù)請求  默認(rèn)false
         paginationInterceptor.setOverflow(false);
        // 設(shè)置最大單頁限制數(shù)量,默認(rèn) 500 條,-1 不受限制
         paginationInterceptor.setLimit(500);
        // 開啟 count 的 join 優(yōu)化,只針對部分 left join
        paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(true));
        return paginationInterceptor;
    }
}

第六步,創(chuàng)建自定義復(fù)合分片算法類MyComplexKeysShardingAlgorithm,注意自己替換application.yml里面的全路徑

package com.jinhx.shardingjdbc.config;

import com.jinhx.shardingjdbc.util.SnowFlakeUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.shardingsphere.api.sharding.complex.ComplexKeysShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.complex.ComplexKeysShardingValue;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * 配置Sharding-JDBC復(fù)合分片算法
 * 根據(jù)id和age計算,來確定是路由到那個表中
 * 目前處理 = 和 in 操作,其余的操作,比如 >、< 等范圍操作均不支持。
 *
 * @author jinhx
 * @since 2021-07-27
 */
@Slf4j
public class MyComplexKeysShardingAlgorithm implements ComplexKeysShardingAlgorithm<Long> {

    /**
     * orderId
     */
    private static final String COLUMN_ORDER_ID = "order_id";

    /**
     * userId
     */
    private static final String COLUMN_USER_ID = "user_id";

    /**
     * 重寫復(fù)合分片算法
     */
    @Override
    public Collection<String> doSharding(Collection<String> availableTargetNames, ComplexKeysShardingValue<Long> shardingValue) {
        if (!shardingValue.getColumnNameAndRangeValuesMap().isEmpty()) {
            throw new RuntimeException("條件全部為空,無法路由到具體的表,暫時不支持范圍查詢");
        }

        // 獲取orderId
        Collection<Long> orderIds = shardingValue.getColumnNameAndShardingValuesMap().getOrDefault(COLUMN_ORDER_ID, new ArrayList<>(1));
        // 獲取userId
        Collection<Long> userIds = shardingValue.getColumnNameAndShardingValuesMap().getOrDefault(COLUMN_USER_ID, new ArrayList<>(1));

        if (CollectionUtils.isEmpty(orderIds) && CollectionUtils.isEmpty(userIds)) {
            throw new RuntimeException("orderId,userId字段同時為空,無法路由到具體的表,暫時不支持范圍查詢");
        }

        // 獲取最終要查詢的表后綴序號的集合,入?yún)㈨樞虿荒茴嵉?        List<Integer> tableNos = getTableNoList(orderIds, userIds);

        return tableNos.stream()
                // 對可用的表數(shù)量求余數(shù),獲取到真實的表的后綴
//                .map(idSuffix -> String.valueOf(idSuffix % availableTargetNames.size()))
                // 拼接獲取到真實的表
                .map(tableSuffix -> availableTargetNames.stream().filter(targetName -> targetName.endsWith(String.valueOf(tableSuffix))).findFirst().orElse(null))
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
    }

    /**
     * 獲取最終要查詢的表后綴序號的集合
     *
     * @param orderIds orderId字段集合
     * @param userIds userId字段集合
     * @return 最終要查詢的表后綴序號的集合
     */
    private List<Integer> getTableNoList(Collection<Long> orderIds, Collection<Long> userIds) {
        List<Integer> result = new ArrayList<>();
        if (CollectionUtils.isNotEmpty(orderIds)){
            // 獲取表位信息
            result.addAll(orderIds.stream()
                    .filter(item -> Objects.nonNull(item) && item > 0)
                    .map(item -> (int) SnowFlakeUtil.getDataCenterId(item))
                    .collect(Collectors.toList()));
        }

        if (CollectionUtils.isNotEmpty(userIds)) {
            // 獲取表位信息
            result.addAll(userIds.stream().filter(item -> Objects.nonNull(item) && item > 0)
                    .map(item -> (int) SnowFlakeUtil.getDataCenterId(item))
                    .collect(Collectors.toList()));
        }

        if (CollectionUtils.isNotEmpty(result)) {
            log.info("SharingJDBC解析路由表后綴成功 redEnvelopeIds={} uids={} 路由表后綴列表={}", orderIds, userIds, result);
            // 合并去重
            return result.stream().distinct().collect(Collectors.toList());
        }
        log.error("SharingJDBC解析路由表后綴失敗 redEnvelopeIds={} uids={}", orderIds, userIds);
        throw new RuntimeException("orderId,userId解析路由表后綴為空,無法路由到具體的表,暫時不支持范圍查詢");
    }

}

第七步,編寫單元測試類,ShardingjdbcApplicationTests,并進行測試

測試步驟

  1. 先運行insertOrdersTest方法向數(shù)據(jù)庫插入數(shù)據(jù),跑完分別查看四個表,是否都有數(shù)據(jù),且理論上來說應(yīng)該是數(shù)據(jù)均勻
  2. 分別從4個表里面隨機撈幾條數(shù)據(jù)的order_id出來,然后運行selectByOrderIdsTest,看是否都能查出數(shù)據(jù),此步驟是為了驗證只根據(jù)order_id進行路由查詢是否正常
  3. 分別從4個表里面隨機撈幾條數(shù)據(jù)的user_id出來,然后運行selectByUserIdsTest,看是否都能查出數(shù)據(jù),此步驟是為了驗證只根據(jù)user_id進行路由查詢是否正常

ShardingjdbcApplicationTests

package com.jinhx.shardingjdbc;

import com.jinhx.shardingjdbc.entity.Order;
import com.jinhx.shardingjdbc.service.IOrderService;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.util.Lists;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.List;

@Slf4j
// 獲取啟動類,加載配置,確定裝載 Spring 程序的裝載方法,它回去尋找 主配置啟動類(被 @SpringBootApplication 注解的)
@SpringBootTest
class ShardingjdbcApplicationTests {

    @Autowired
    private IOrderService iOrderService;

    @Test
    void selectByOrderIdsTest() {
        List<Long> orderIds = Lists.newArrayList(1443844581547311109L, 1443844581547442181L, 1443844581547573255L, 1443844581547704327L);
        log.info(iOrderService.selectByOrderIds(orderIds).toString());
    }

    @Test
    void selectByUserIdsTest() {
        List<Long> userIds = Lists.newArrayList(1443844581547311108L, 1443844581547311106L, 1443844581547442180L, 1443844581547704326L);
        log.info(iOrderService.selectByUserIds(userIds).toString());
    }

    @Test
    void insertOrdersTest() {
        List<Order> orders = Lists.newArrayList();
        for (int i = 1;i < 100;i++){
            Order order = new Order();
            order.buildUserId(i);
            order.setMoney(i * 1000L);
            order.buildOrderId();
            orders.add(order);
        }
        log.info("orders={}", orders);
        iOrderService.insertOrders(orders);
    }

    @BeforeEach
    void testBefore(){
        log.info("測試開始!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
    }

    @AfterEach
    void testAfter(){
        log.info("測試結(jié)束!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
    }

}

完整代碼地址:https://github.com/Jinhx128/springboot-demo

注:此工程包含多個module,本文所用代碼均在shardingjdbc-demo模塊下

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,443評論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,530評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,407評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,981評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,759評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,204評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,263評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,415評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,955評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,650評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,892評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,675評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,967評論 2 374

推薦閱讀更多精彩內(nèi)容