文章目录
9 案例
9.1 需求描述
电商平台商品列表展示,每个列表项中除了包含商品基本信息、商品描述信息之外,还包括了商品所属的店铺信息,如下:
本案例实现功能如下:
- 添加商品
- 商品分页查询
- 商品统计
9.2 数据库设计
数据库设计如下,其中商品与店铺信息之间进行了垂直分库,分为了PRODUCT_DB(商品库)和STORE_DB(店铺库);商品信息还进行了垂直分表,分为了商品基本信息(product_info)和商品描述信息
(product_descript),地理区域信息(region)作为公共表
,冗余在两库中:
- 考虑到商品信息的数据增长性,对PRODUCT_DB(
商品库
)进行了水平分库
,分片键使用店铺id
,分片策略为店铺ID%2 + 1,因此商品描述信息对所属店铺ID进行了冗余; - 对
商品基本信息
(product_info)和商品描述信息
(product_descript)进行水平分表
,分片键使用商品id
,分片策略为商品ID%2 + 1,并将为这两个表设置为绑定表,避免笛卡尔积join; - 为避免主键冲突,
D生成策略采用雪花算法
来生成全局唯一ID,最终数据库设计为下图:
要求使用读写分离来提升性能,可用性。
9.3 环境说明
操作系统:Win10 数据库:MySQL-5.7.25
JDK:64位 jdk1.8.0_201
应用框架:spring-boot-2.1.3.RELEASE,Mybatis3.5.0
Sharding-JDBC:sharding-jdbc-spring-boot-starter-4.0.0-RC1
9.4 环境准备
9.4.1 mysql主从同步(windows)
参考读写分离章节,对以下库进行主从同步配置:
# 设置需要同步的数据库
binlog‐do‐db=store_db
binlog‐do‐db=product_db_1
binlog‐do‐db=product_db_2
9.4.2 初始化数据库
创建store_db数据库,并执行以下脚本创建表:
create database store_db charset utf8;
DROP TABLE IF EXISTS `region`;
CREATE TABLE `region` (
`id` bigint(20) NOT NULL COMMENT 'id',
`region_code` varchar(50) DEFAULT NULL COMMENT '地理区域编码',
`region_name` varchar(100) DEFAULT NULL COMMENT '地理区域名称',
`level` tinyint(1) DEFAULT NULL COMMENT '地理区域级别(省、市、县)',
`parent_region_code` varchar(50) DEFAULT NULL COMMENT '上级地理区域编码',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
INSERT INTO `region` VALUES (1, '110000', '北京', 0, NULL);
INSERT INTO `region` VALUES (2, '410000', '河南省', 0, NULL);
INSERT INTO `region` VALUES (3, '110100', '北京市', 1, '110000');
INSERT INTO `region` VALUES (4, '410100', '郑州市', 1, '410000');
DROP TABLE IF EXISTS `store_info`;
CREATE TABLE `store_info` (
`id` bigint(20) NOT NULL COMMENT 'id',
`store_name` varchar(100) DEFAULT NULL COMMENT '店铺名称',
`reputation` int(11) DEFAULT NULL COMMENT '信誉等级',
`region_code` varchar(50) DEFAULT NULL COMMENT '店铺所在地',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
INSERT INTO `store_info` VALUES (1, 'XX零食店', 4, '110100');
INSERT INTO `store_info` VALUES (2, 'XX饮品店', 3, '410100');
创建product_db_1、product_db_2数据库,并分别对两库执行以下脚本创建表:
create database product_db_1 charset utf8;
create database product_db_2 charset utf8;
DROP TABLE IF EXISTS `product_descript_1`;
CREATE TABLE `product_descript_1` (
`id` bigint(20) NOT NULL COMMENT 'id',
`product_info_id` bigint(20) DEFAULT NULL COMMENT '所属商品id',
`descript` longtext COMMENT '商品描述',
`store_info_id` bigint(20) DEFAULT NULL COMMENT '所属店铺id',
PRIMARY KEY (`id`) USING BTREE,
KEY `FK_Reference_2` (`product_info_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
DROP TABLE IF EXISTS `product_descript_2`;
CREATE TABLE `product_descript_2` (
`id` bigint(20) NOT NULL COMMENT 'id',
`product_info_id` bigint(20) DEFAULT NULL COMMENT '所属商品id',
`descript` longtext COMMENT '商品描述',
`store_info_id` bigint(20) DEFAULT NULL COMMENT '所属店铺id',
PRIMARY KEY (`id`) USING BTREE,
KEY `FK_Reference_2` (`product_info_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
DROP TABLE IF EXISTS `product_info_1`;
CREATE TABLE `product_info_1` (
`product_info_id` bigint(20) NOT NULL COMMENT 'id',
`store_info_id` bigint(20) DEFAULT NULL COMMENT '所属店铺id',
`product_name` varchar(100) DEFAULT NULL COMMENT '商品名称',
`spec` varchar(50) DEFAULT NULL COMMENT '规格',
`region_code` varchar(50) DEFAULT NULL COMMENT '产地',
`price` decimal(10,0) DEFAULT NULL COMMENT '商品价格',
`image_url` varchar(100) DEFAULT NULL COMMENT '商品图片',
PRIMARY KEY (`product_info_id`) USING BTREE,
KEY `FK_Reference_1` (`store_info_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
DROP TABLE IF EXISTS `product_info_2`;
CREATE TABLE `product_info_2` (
`product_info_id` bigint(20) NOT NULL COMMENT 'id',
`store_info_id` bigint(20) DEFAULT NULL COMMENT '所属店铺id',
`product_name` varchar(100) DEFAULT NULL COMMENT '商品名称',
`spec` varchar(50) DEFAULT NULL COMMENT '规格',
`region_code` varchar(50) DEFAULT NULL COMMENT '产地',
`price` decimal(10,0) DEFAULT NULL COMMENT '商品价格',
`image_url` varchar(100) DEFAULT NULL COMMENT '商品图片',
PRIMARY KEY (`product_info_id`) USING BTREE,
KEY `FK_Reference_1` (`store_info_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
DROP TABLE IF EXISTS `region`;
CREATE TABLE `region` (
`id` bigint(20) NOT NULL COMMENT 'id',
`region_code` varchar(50) DEFAULT NULL COMMENT '地理区域编码',
`region_name` varchar(100) DEFAULT NULL COMMENT '地理区域名称',
`level` tinyint(1) DEFAULT NULL COMMENT '地理区域级别(省、市、县)',
`parent_region_code` varchar(50) DEFAULT NULL COMMENT '上级地理区域编码',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
INSERT INTO `region` VALUES (1, '110000', '北京', 0, NULL);
INSERT INTO `region` VALUES (2, '410000', '河南省', 0, NULL);
INSERT INTO `region` VALUES (3, '110100', '北京市', 1, '110000');
INSERT INTO `region` VALUES (4, '410100', '郑州市', 1, '410000');
9.5 实现步骤
9.5.1 搭建maven工程
(1)搭建工程maven工程shopping,导入资料中基础代码shopping,以dbsharding为总体父工程,并做好spring boot相关配置。
(2)引入maven依赖
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding‐jdbc‐spring‐boot‐starter</artifactId>
<version>4.0.0‐RC1</version>
</dependency>
9.5.2 分片配置
既然是分库分表,那么就需要定义多个真实数据源,每一个数据库链接信息就是一个数据源定义,如:
spring.shardingsphere.datasource.m0.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.m0.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.m0.url = jdbc:mysql://localhost:3306/store_db?useUnicode=true&useSSL=false
spring.shardingsphere.datasource.m0.username = root
spring.shardingsphere.datasource.m0.password = 123456
m0,就是这个真实数据源的名称,然后需要告诉Sharding-JDBC,咱们有哪些真实数据源,如:
spring.shardingsphere.datasource.names = m0,m1,m2,s0,s1,s2
如果需要配置读写分离,还需要告诉Sharding-JDBC,这么多真实数据源,那几个是一套读写分离?也就是定义主从逻辑数据源:
spring.shardingsphere.sharding.master-slave-rules.ds0.master-data-source-name=m0
spring.shardingsphere.sharding.master-slave-rules.ds0.slave-data-source-names=s0
spring.shardingsphere.sharding.master-slave-rules.ds1.master-data-source-name=m1
spring.shardingsphere.sharding.master-slave-rules.ds1.slave-data-source-names=s1
spring.shardingsphere.sharding.master-slave-rules.ds2.master-data-source-name=m2
spring.shardingsphere.sharding.master-slave-rules.ds2.slave-data-source-names=s2
# 真实数据源定义 m为主库 s为从库
server.port=56082
spring.application.name = shopping
spring.profiles.active = local
server.servlet.context-path = /shopping
spring.http.encoding.enabled = true
spring.http.encoding.charset = UTF-8
spring.http.encoding.force = true
spring.main.allow-bean-definition-overriding = true
mybatis.configuration.map-underscore-to-camel-case = true
#sharding-jdbc分片规则
#配置数据源 m0,m1,m2,s0,s1,s2
spring.shardingsphere.datasource.names = m0,m1,m2,s0,s1,s2
spring.shardingsphere.datasource.m0.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.m0.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.m0.url = jdbc:mysql://localhost:3306/store_db?useUnicode=true&useSSL=false
spring.shardingsphere.datasource.m0.username = root
spring.shardingsphere.datasource.m0.password = 123456
spring.shardingsphere.datasource.m1.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.m1.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.m1.url = jdbc:mysql://localhost:3306/product_db_1?useUnicode=true&useSSL=false
spring.shardingsphere.datasource.m1.username = root
spring.shardingsphere.datasource.m1.password = 123456
spring.shardingsphere.datasource.m2.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.m2.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.m2.url = jdbc:mysql://localhost:3306/product_db_2?useUnicode=true&useSSL=false
spring.shardingsphere.datasource.m2.username = root
spring.shardingsphere.datasource.m2.password = 123456
spring.shardingsphere.datasource.s0.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.s0.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.s0.url = jdbc:mysql://localhost:3307/store_db?useUnicode=true&useSSL=false
spring.shardingsphere.datasource.s0.username = root
spring.shardingsphere.datasource.s0.password = 123456
spring.shardingsphere.datasource.s1.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.s1.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.s1.url = jdbc:mysql://localhost:3307/product_db_1?useUnicode=true&useSSL=false
spring.shardingsphere.datasource.s1.username = root
spring.shardingsphere.datasource.s1.password = 123456
spring.shardingsphere.datasource.s2.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.s2.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.s2.url = jdbc:mysql://localhost:3307/product_db_2?useUnicode=true&useSSL=false
spring.shardingsphere.datasource.s2.username = root
spring.shardingsphere.datasource.s2.password = 123456
#主从关系
spring.shardingsphere.sharding.master-slave-rules.ds0.master-data-source-name=m0
spring.shardingsphere.sharding.master-slave-rules.ds0.slave-data-source-names=s0
spring.shardingsphere.sharding.master-slave-rules.ds1.master-data-source-name=m1
spring.shardingsphere.sharding.master-slave-rules.ds1.slave-data-source-names=s1
spring.shardingsphere.sharding.master-slave-rules.ds2.master-data-source-name=m2
spring.shardingsphere.sharding.master-slave-rules.ds2.slave-data-source-names=s2
#分库策略(水平)根据store_info_id店铺进行水平分库
spring.shardingsphere.sharding.default-database-strategy.inline.sharding-column = store_info_id
spring.shardingsphere.sharding.default-database-strategy.inline.algorithm-expression = ds$->{store_info_id % 2 + 1}
#分表策略
# store_info分表策略
spring.shardingsphere.sharding.tables.store_info.actual-data-nodes = ds$->{0}.store_info
spring.shardingsphere.sharding.tables.store_info.table-strategy.inline.sharding-column = id
spring.shardingsphere.sharding.tables.store_info.table-strategy.inline.algorithm-expression = store_info
# product_info分表策略
#数据结点包括,ds1.product_info_1,ds1.product_info_2,ds2.product_info_1,ds2.product_info_2
spring.shardingsphere.sharding.tables.product_info.actual-data-nodes = ds$->{1..2}.product_info_$->{1..2}
spring.shardingsphere.sharding.tables.product_info.table-strategy.inline.sharding-column = product_info_id
spring.shardingsphere.sharding.tables.product_info.table-strategy.inline.algorithm-expression = product_info_$->{product_info_id%2+1}
spring.shardingsphere.sharding.tables.product_info.key-generator.column=product_info_id
spring.shardingsphere.sharding.tables.product_info.key-generator.type=SNOWFLAKE
#product_descript分表策略
spring.shardingsphere.sharding.tables.product_descript.actual-data-nodes = ds$->{1..2}.product_descript_$->{1..2}
spring.shardingsphere.sharding.tables.product_descript.table-strategy.inline.sharding-column = product_info_id
spring.shardingsphere.sharding.tables.product_descript.table-strategy.inline.algorithm-expression = product_descript_$->{product_info_id % 2 + 1}
spring.shardingsphere.sharding.tables.product_descript.key-generator.column=id
spring.shardingsphere.sharding.tables.product_descript.key-generator.type=SNOWFLAKE
# 设置product_info,product_descript为绑定表
spring.shardingsphere.sharding.binding-tables[0] = product_info,product_descript
# 设置region为广播表(公共表),每次更新操作会发送至所有数据源
spring.shardingsphere.sharding.broadcast-tables=region
# 打开sql输出日志
spring.shardingsphere.props.sql.show = true
swagger.enable = true
logging.level.root = info
logging.level.org.springframework.web = info
logging.level.com.itheima.dbsharding = debug
9.5.3 添加商品
实体类,参考基础工程:
DAO实现
@Mapper
@Component
public interface ProductDao {
//添加商品基本信息
@Insert("insert into product_info(store_info_id,product_name,spec,region_code,price)
value(#{storeInfoId},#{productName},#{spec},#{regionCode},#{price})")
@Options(useGeneratedKeys = true,keyProperty = "productInfoId",keyColumn = "id")
int insertProductInfo(ProductInfo productInfo);
//添加商品描述信息
@Insert("insert into product_descript(product_info_id,descript,store_info_id) value(#
{productInfoId},#{descript},#{storeInfoId})")
@Options(useGeneratedKeys = true,keyProperty = "id",keyColumn = "id")
int insertProductDescript(ProductDescript productDescript);
}
service实现,针对垂直分库的两个库,分别实现店铺服务、商品服务
package com.itheima.shopping.service.impl;
import com.itheima.shopping.dao.ProductDao;
import com.itheima.shopping.entity.ProductDescript;
import com.itheima.shopping.entity.ProductInfo;
import com.itheima.shopping.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
/**
* @author Administrator
* @version 1.0
**/
@Service
public class ProductServiceImpl implements ProductService {
@Autowired
ProductDao productDao;
//添加商品
@Override
@Transactional
public void createProduct(ProductInfo productInfo) {
ProductDescript productDescript = new ProductDescript();
//设置商品描述 信息
productDescript.setDescript(productInfo.getDescript());
//调用dao向商品信息表
productDao.insertProductInfo(productInfo);
//将商品信息id设置到productDescript
productDescript.setProductInfoId(productInfo.getProductInfoId());
//设置店铺id
productDescript.setStoreInfoId(productInfo.getStoreInfoId());
//向商品描述信息表插入数据
productDao.insertProductDescript(productDescript);
}
}
controller实现:
/**
* 卖家商品展示
*/
@RestController
public class SellerController {
@Autowired
private ProductService productService;
@PostMapping("/products")
public String createProject(@RequestBody ProductInfo productInfo) {
productService.createProduct(productInfo);
return "创建成功!";
}
单元测试:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ShardingJdbcDemoBootstrap.class)
public class ShardingTest {
@Autowired
ProductService productService;
@Test
public void testCreateProduct(){
for(long i=1;i<10;i++){
//store_info_id,product_name,spec,region_code,price,image_url
ProductInfo productInfo = new ProductInfo();
productInfo.setProductName("Java编程思想"+i);
productInfo.setDescript("Java编程思想是一本非常好的Java教程"+i);
productInfo.setRegionCode("110000");
productInfo.setStoreInfoId(1);
productInfo.setPrice(new BigDecimal(i));
productService.createProduct(productInfo);
}
}
...
单元测试:
@Test
public void testCreateProduct() {
for (int i = 1; i < 10; i++) {
ProductInfo productInfo = new ProductInfo();
productInfo.setStoreInfoId(1L);//店铺id
productInfo.setProductName("Java" + i);//商品名称
productInfo.setSpec("big");
productInfo.setPrice(new BigDecimal(60));
productInfo.setRegionCode("110100");
productInfo.setDescript("Java good" + i);//商品描述
productService.createProduct(productInfo);
}
}
- 这里使用了sharding-jdbc所提供的全局主键生成方式之一雪花算法,来生成全局业务唯一主键。
- 通过添加商品接口新增商品进行分库验证,store_info_id为偶数的数据在product_db_1,为奇数的数据在product_db_2。
- 通过添加商品接口新增商品进行分表验证,product_id为偶数的数据在product_info_1、product_descript_1,为奇数的数据在product_info_2、product_descript_2。
9.5.4 查询商品
Dao实现:
在ProductDao中定义商品查询方法:
@Select("select i.*,d.descript,r.region_name placeOfOrigin from product_info i join product_descript d on i.product_info_id = d.product_info_id " +
" join region r on i.region_code = r.region_code order by product_info_id desc limit #{start},#{pageSize}")
List<ProductInfo> selectProductList(@Param("start") int start, @Param("pageSize") int pageSize);
Service实现:
在ProductServiceImpl定义商品查询方法:
@Override
public List<ProductInfo> queryProduct(int page, int pageSize) {
int start = (page - 1) * pageSize;
return productDao.selectProductList(start, pageSize);
}
Controller实现:
@GetMapping(value = "/products/{page}/{pageSize}")
public List<ProductInfo> queryProduct(@PathVariable("page")int page,@PathVariable("pageSize")int
pageSize){
return productService.queryProduct(page,pageSize);
}
单元测试:
//查询商品
@Test
public void testQueryProduct() {
List<ProductInfo> productInfos = productService.queryProduct(2, 2);
System.out.println(productInfos);
}
通过查询商品列表接口,能够查询到所有分片的商品信息,关联的地理区域,店铺信息正确。
总结:
分页查询是业务中最常见的场景,Sharding-jdbc支持常用关系数据库的分页查询,不过Sharding-jdbc的分页功能比较容易让使用者误解,用户通常认为分页归并会占用大量内存。 在分布式的场景中,将 LIMIT 10000000 , 10改写为 LIMIT 0, 10000010 ,才能保证其数据的正确性。用户非常容易产生ShardingSphere会将大量无意义的数据加载至内存中,造成内存溢出风险的错觉。 其实大部分情况都通过流式归并获取数据结果集,因此ShardingSphere会通过结果集的next方法将无需取出的数据全部跳过,并不会将其存入内存。但同时需要注意的是,由于排序的需要,大量的数据仍然需要传输到Sharding-Jdbc的内存空间。 因此,采用LIMIT这种方式分页,并非最佳实践。 由于LIMIT并不能通过索引查询数据,因此如果可以保证ID的连续性,通过ID进行分页是比较好的解决方案,例如:
SELECT * FROM t_order WHERE id > 100000 AND id <= 100010 ORDER BY id;
或通过记录上次查询结果的最后一条记录的ID进行下一页的查询,例如:
SELECT * FROM t_order WHERE id > 10000000 LIMIT 10;
排序功能是由Sharding-jdbc的排序归并来完成,由于在SQL中存在 ORDER BY 语句,因此每个数据结果集自身是有序的,因此只需要将数据结果集当前游标指向的数据值进行排序即可。这相当于对多个有序的数组进行排序,归并排序是最适合此场景的排序算法。
9.5.5 统计商品
本小节实现商品总数统计,商品分组统计
Dao实现,在ProductDao中定义:
//商品总数
@Select("select count(1) from product_info")
int selectCount();
//商品分组统计
@Select("select t.region_code,count(1) as num from product_info t group by t.region_code having num > 1 order by region_code ")
List<Map> selectProductGroupList();
单元测试:
//统计商品总数
@Test
public void testSelectCount() {
int i = productDao.selectCount();
System.out.println(i);
}
//分组统计商品
@Test
public void testSelectProductGroupList() {
List<Map> maps = productDao.selectProductGroupList();
System.out.println(maps);
}
总结:
分组统计也是业务中常见的场景,分组功能的实现由Sharding-jdbc分组归并完成。分组归并的情况最为复杂,它分为流式分组归并和内存分组归并。 流式分组归并要求SQL的排序项与分组项的字段必须保持一致,否则只能通过内存归并才能保证其数据的正确性。
举例说明,假设根据科目分片,表结构中包含考生的姓名(为了简单起见,不考虑重名的情况)和分数。通过SQL获取每位考生的总分,可通过如下SQL:
SELECT name, SUM(score) FROM t_score GROUP BY name ORDER BY name;
在分组项与排序项完全一致的情况下,取得的数据是连续的,分组所需的数据全数存在于各个数据结果集的当前游标所指向的数据值,因此可以采用流式归并。如下图所示。
进行归并时,逻辑与排序归并类似。 下图展现了进行next调用的时候,流式分组归并是如何进行的。
通过图中我们可以看到,当进行第一次next调用时,排在队列首位的t_score_java将会被弹出队列,并且将分组值同为“Jetty”的其他结果集中的数据一同弹出队列。 在获取了所有的姓名为“Jetty”的同学的分数之后,进行累加操作,那么,在第一次next调用结束后,取出的结果集是“Jetty”的分数总和。 与此同时,所有的数据结果集中的游标都将下移至数据值“Jetty”的下一个不同的数据值,并且根据数据结果集当前游标指向的值进行重排序。 因此,包含名字顺着第二位的“John”的相关数据结果集则排在的队列的前列。
9.5.6 完整代码
DAO
package com.itheima.shopping.dao;
import com.itheima.shopping.entity.ProductDescript;
import com.itheima.shopping.entity.ProductInfo;
import org.apache.ibatis.annotations.*;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
/**
* Created by Administrator.
*/
@Mapper
@Component
public interface ProductDao {
//添加商品基本信息
@Insert("insert into product_info(store_info_id,product_name,spec,region_code,price) " +
" values (#{storeInfoId},#{productName},#{spec},#{regionCode},#{price})")
@Options(useGeneratedKeys = true, keyProperty = "productInfoId", keyColumn = "product_info_id")
int insertProductInfo(ProductInfo productInfo);
//添加商品描述信息
@Insert("insert into product_descript(product_info_id,descript,store_info_id) " +
" value(#{productInfoId},#{descript},#{storeInfoId})")
@Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
int insertProductDescript(ProductDescript productDescript);
@Select("select i.*,d.descript,r.region_name placeOfOrigin from product_info i join product_descript d on i.product_info_id = d.product_info_id " +
" join region r on i.region_code = r.region_code order by product_info_id desc limit #{start},#{pageSize}")
List<ProductInfo> selectProductList(@Param("start") int start, @Param("pageSize") int pageSize);
//商品总数
@Select("select count(1) from product_info")
int selectCount();
//商品分组统计
@Select("select t.region_code,count(1) as num from product_info t group by t.region_code having num > 1 order by region_code ")
List<Map> selectProductGroupList();
}
实体:
package com.itheima.shopping.entity;
import lombok.Data;
/**
* 商品描述
*/
@Data
public class ProductDescript {
private Long id;
/**
* 所属商品id
*/
private Long productInfoId;
/**
* 商品描述
*/
private String descript;
/**
* 所属店铺id
*/
private Long storeInfoId;
}
package com.itheima.shopping.entity;
import lombok.Data;
import java.math.BigDecimal;
/**
* 商品基本信息
*/
@Data
public class ProductInfo {
private Long productInfoId;
/**
* 所属店铺id
*/
private Long storeInfoId;
/**
* 商品名称
*/
private String productName;
/**
* 规格
*/
private String spec;
/**
* 产地
*/
private String regionCode;
/**
* 商品价格
*/
private BigDecimal price;
/**
* 商品图片
*/
private String imageUrl;
关联信息/
/**
* 商品描述
*/
private String descript;
/**
* 产地名称
*/
private String placeOfOrigin;
/**
* 店铺名称
*/
private String storeName;
/**
* 店铺信誉等级
*/
private int reputation;
/**
* 店铺所在地名称
*/
private String storeRegionName;
}
package com.itheima.shopping.entity;
import lombok.Data;
/**
* 地理区域信息
*/
@Data
public class region {
private Long id;
/**
* 地理区域编码
*/
private String regionCode;
/**
* 地理区域名称
*/
private String regionName;
/**
* 地理区域级别(省、市、县)
*/
private int level;
/**
* 上级地理区域编码
*/
private String parentRegionCode;
}
package com.itheima.shopping.entity;
import lombok.Data;
/**
* 店铺信息
*/
@Data
public class StoreInfo {
private Long id;
/**
* 店铺名称
*/
private String storeName;
/**
* 信誉等级
*/
private int reputation;
/**
* 店铺所在地
*/
private String regionCode;
/**
* 店铺所在地名称
*/
private String regionName;
}
service
package com.itheima.shopping.service;
import com.itheima.shopping.entity.ProductInfo;
import java.util.List;
/**
* Created by Administrator.
*/
public interface ProductService {
//添加商品
public void createProduct(ProductInfo product);
//查询商品
public List<ProductInfo> queryProduct(int page, int pageSize);
}
package com.itheima.shopping.service.impl;
import com.itheima.shopping.dao.ProductDao;
import com.itheima.shopping.entity.ProductDescript;
import com.itheima.shopping.entity.ProductInfo;
import com.itheima.shopping.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
/**
* @author Administrator
* @version 1.0
**/
@Service
public class ProductServiceImpl implements ProductService {
@Autowired
ProductDao productDao;
//添加商品
@Override
@Transactional
public void createProduct(ProductInfo productInfo) {
ProductDescript productDescript = new ProductDescript();
//设置商品描述 信息
productDescript.setDescript(productInfo.getDescript());
//调用dao向商品信息表
productDao.insertProductInfo(productInfo);
//将商品信息id设置到productDescript
productDescript.setProductInfoId(productInfo.getProductInfoId());
//设置店铺id
productDescript.setStoreInfoId(productInfo.getStoreInfoId());
//向商品描述信息表插入数据
productDao.insertProductDescript(productDescript);
}
@Override
public List<ProductInfo> queryProduct(int page, int pageSize) {
int start = (page - 1) * pageSize;
return productDao.selectProductList(start, pageSize);
}
}
测试类
package com.itheima.shopping;
import com.itheima.shopping.dao.ProductDao;
import com.itheima.shopping.entity.ProductInfo;
import com.itheima.shopping.service.ProductService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
/**
* @author Administrator
* @version 1.0
**/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ShoppingBootstrap.class)
public class ShardingTest {
@Autowired
ProductService productService;
@Autowired
ProductDao productDao;
//添加商品
@Test
public void testCreateProduct() {
for (int i = 1; i < 10; i++) {
ProductInfo productInfo = new ProductInfo();
productInfo.setStoreInfoId(1L);//店铺id
productInfo.setProductName("Java" + i);//商品名称
productInfo.setSpec("big");
productInfo.setPrice(new BigDecimal(60));
productInfo.setRegionCode("110100");
productInfo.setDescript("Java good" + i);//商品描述
productService.createProduct(productInfo);
}
}
//查询商品
@Test
public void testQueryProduct() {
List<ProductInfo> productInfos = productService.queryProduct(2, 2);
System.out.println(productInfos);
}
//统计商品总数
@Test
public void testSelectCount() {
int i = productDao.selectCount();
System.out.println(i);
}
//分组统计商品
@Test
public void testSelectProductGroupList() {
List<Map> maps = productDao.selectProductGroupList();
System.out.println(maps);
}
}
application.properties
server.port=56082
spring.application.name = shopping
spring.profiles.active = local
server.servlet.context-path = /shopping
spring.http.encoding.enabled = true
spring.http.encoding.charset = UTF-8
spring.http.encoding.force = true
spring.main.allow-bean-definition-overriding = true
mybatis.configuration.map-underscore-to-camel-case = true
#sharding-jdbc分片规则
#配置数据源 m0,m1,m2,s0,s1,s2
spring.shardingsphere.datasource.names = m0,m1,m2,s0,s1,s2
spring.shardingsphere.datasource.m0.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.m0.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.m0.url = jdbc:mysql://localhost:3306/store_db?useUnicode=true&useSSL=false
spring.shardingsphere.datasource.m0.username = root
spring.shardingsphere.datasource.m0.password = 123456
spring.shardingsphere.datasource.m1.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.m1.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.m1.url = jdbc:mysql://localhost:3306/product_db_1?useUnicode=true&useSSL=false
spring.shardingsphere.datasource.m1.username = root
spring.shardingsphere.datasource.m1.password = 123456
spring.shardingsphere.datasource.m2.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.m2.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.m2.url = jdbc:mysql://localhost:3306/product_db_2?useUnicode=true&useSSL=false
spring.shardingsphere.datasource.m2.username = root
spring.shardingsphere.datasource.m2.password = 123456
spring.shardingsphere.datasource.s0.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.s0.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.s0.url = jdbc:mysql://localhost:3307/store_db?useUnicode=true&useSSL=false
spring.shardingsphere.datasource.s0.username = root
spring.shardingsphere.datasource.s0.password = 123456
spring.shardingsphere.datasource.s1.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.s1.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.s1.url = jdbc:mysql://localhost:3307/product_db_1?useUnicode=true&useSSL=false
spring.shardingsphere.datasource.s1.username = root
spring.shardingsphere.datasource.s1.password = 123456
spring.shardingsphere.datasource.s2.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.s2.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.s2.url = jdbc:mysql://localhost:3307/product_db_2?useUnicode=true&useSSL=false
spring.shardingsphere.datasource.s2.username = root
spring.shardingsphere.datasource.s2.password = 123456
#主从关系
spring.shardingsphere.sharding.master-slave-rules.ds0.master-data-source-name=m0
spring.shardingsphere.sharding.master-slave-rules.ds0.slave-data-source-names=s0
spring.shardingsphere.sharding.master-slave-rules.ds1.master-data-source-name=m1
spring.shardingsphere.sharding.master-slave-rules.ds1.slave-data-source-names=s1
spring.shardingsphere.sharding.master-slave-rules.ds2.master-data-source-name=m2
spring.shardingsphere.sharding.master-slave-rules.ds2.slave-data-source-names=s2
#分库策略(水平)根据store_info_id店铺进行水平分库
spring.shardingsphere.sharding.default-database-strategy.inline.sharding-column = store_info_id
spring.shardingsphere.sharding.default-database-strategy.inline.algorithm-expression = ds$->{store_info_id % 2 + 1}
#分表策略
# store_info分表策略
spring.shardingsphere.sharding.tables.store_info.actual-data-nodes = ds$->{0}.store_info
spring.shardingsphere.sharding.tables.store_info.table-strategy.inline.sharding-column = id
spring.shardingsphere.sharding.tables.store_info.table-strategy.inline.algorithm-expression = store_info
# product_info分表策略
#数据结点包括,ds1.product_info_1,ds1.product_info_2,ds2.product_info_1,ds2.product_info_2
spring.shardingsphere.sharding.tables.product_info.actual-data-nodes = ds$->{1..2}.product_info_$->{1..2}
spring.shardingsphere.sharding.tables.product_info.table-strategy.inline.sharding-column = product_info_id
spring.shardingsphere.sharding.tables.product_info.table-strategy.inline.algorithm-expression = product_info_$->{product_info_id%2+1}
spring.shardingsphere.sharding.tables.product_info.key-generator.column=product_info_id
spring.shardingsphere.sharding.tables.product_info.key-generator.type=SNOWFLAKE
#product_descript分表策略
spring.shardingsphere.sharding.tables.product_descript.actual-data-nodes = ds$->{1..2}.product_descript_$->{1..2}
spring.shardingsphere.sharding.tables.product_descript.table-strategy.inline.sharding-column = product_info_id
spring.shardingsphere.sharding.tables.product_descript.table-strategy.inline.algorithm-expression = product_descript_$->{product_info_id % 2 + 1}
spring.shardingsphere.sharding.tables.product_descript.key-generator.column=id
spring.shardingsphere.sharding.tables.product_descript.key-generator.type=SNOWFLAKE
# 设置product_info,product_descript为绑定表
spring.shardingsphere.sharding.binding-tables[0] = product_info,product_descript
# 设置region为广播表(公共表),每次更新操作会发送至所有数据源
spring.shardingsphere.sharding.broadcast-tables=region
# 打开sql输出日志
spring.shardingsphere.props.sql.show = true
swagger.enable = true
logging.level.root = info
logging.level.org.springframework.web = info
logging.level.com.itheima.dbsharding = debug
10 总结
为什么分库分表?
-
分库分表就是为了解决由于数据量过大而导致数据库性能降低的问题,将原来独立的数据库拆分成若干数据库组成 ,将数据大表拆分成若干数据表组成,使得单一数据库、单一数据表的数据量变小,从而达到提升数据库性能的目的。
-
分库分表方式:垂直分表、垂直分库、水平分库、水平分表
-
分库分表带来问题:由于数据分散在多个数据库,服务器导致了事务一致性问题、跨节点join问题、跨节点分页、排序、函数,主键需要全局唯一,公共表。
-
Sharding-JDBC基础概念:逻辑表,真实表,数据节点,绑定表,广播表,分片键,分片算法,分片策略,主键生成策略
-
Sharding-JDBC核心功能:数据分片,读写分离
-
Sharding-JDBC执行流程: SQL解析 => 查询优化 =>SQL路由 => SQL改写 => SQL执行 => 结果归并
-
最佳实践:系统在设计之初就应该对业务数据的耦合松紧进行考量,从而进行垂直分库、垂直分表,使数据层架构清晰明了。若非必要,无需进行水平切分,应先从缓存技术着手降低对数据库的访问压力。如果缓存使用过后,数据库访问量还是非常大,可以考虑数据库读、写分离原则。若当前数据库压力依然大,且业务数据持续增长无法估量,最后可考虑水平分库、分表,单表拆分数据控制在1000万以内。