一、面临的情况:
数据量:基础数据10亿+;
数据来源:第三方系统
数据内容:手机基本信息(imei、imei2、meid、主板编码、销售日期、物料编码、机型等信息)
面向对象:to c&中台系统,为多个业务系统提供基础数据查询校验
二、要求:
支持:QPS:5000+;
响应时间:10ms以内
查询维度:imei、imei2、meid、主板编码
三、设计思路:
a、针对业务
1、建立索引表
sql
CREATE TABLE `device_index_x` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`device_code` varchar(100) DEFAULT NULL COMMENT '索引分表的device_code,值为imei/imei2/meid/emmcid',
`device_uid` varchar(64) NOT NULL DEFAULT '' COMMENT '设备唯一标志,目前为imei',
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_device_index_device_code` (`device_code`)
) ENGINE=InnoDB AUTO_INCREMENT=137581588 DEFAULT CHARSET=utf8 COMMENT='设备索引表'
为了尽可能的缩小存储容量和提高查询速度,索引表里有仅有两个业务字段,devcie_code和device_uid
分表键:device_code,值为imei、imei2、meid、主板编码,方便业务以多维度查询设备信息。
2、建立主表:
sql
CREATE TABLE `device_info_0` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`device_uid` varchar(64) NOT NULL DEFAULT '' COMMENT '备用设备唯一标志,目前是imei',
`imei` varchar(20) NOT NULL DEFAULT '' COMMENT 'IMEI',
`imei2` varchar(20) NOT NULL DEFAULT '' COMMENT 'IMEI2',
`meid` varchar(20) NOT NULL DEFAULT '' COMMENT 'MEID',
`emmcid` varchar(100) NOT NULL DEFAULT '' COMMENT 'EMMCID',
`mac` varchar(128) DEFAULT '' COMMENT '设备MAC地址',
`sn` varchar(64) DEFAULT '' COMMENT '设备sn码',
`model` varchar(20) NOT NULL DEFAULT '' COMMENT '机型',
`item` varchar(10) NOT NULL DEFAULT '' COMMENT '物料编码',
`device_type` tinyint(2) DEFAULT '0' COMMENT '设备类型;0:手机;1:手表;2:pad 3:内部机器 4:内部处理机',
`source` tinyint(2) DEFAULT '0' COMMENT '数据来源;0:大数据;1:MES',
`sale_date` date DEFAULT NULL COMMENT '销售时间',
`out_factory_date` datetime DEFAULT NULL COMMENT '出厂时间',
`old_flag` tinyint(2) DEFAULT '0' COMMENT '二手设备标志;0:新机;1:二手设备',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_device_info_uid` (`device_uid`),
KEY `idx_sale_date` (`sale_date`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='设备信息'
主表信息,存储设备(手表、手机、pad、耳机)基础信息。通过device_uid和索引表关联
b、针对大数据量分库分表
1)、分库分表数量:4库32表
- 阿里《Java开发手册》建议: 单表行数超过500万行或者单表容量超过2GB,推荐分库分表。
- 业界通用经验值: 一般认为单表超过2000万行(2000w)时,MySQL的B+树索引高度过高,会导致查询效率急剧下降,需要考虑分区或分表。
2)、分表分表数量计算思路:针对存量总数10亿,1000000000/128=700w+/每表;再针对每年1000w数据增量计算,达到单表2000w,系统可以支撑10年+;
3)、分库分表中间件:shardingsphere
配置:
XML
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:sharding="http://shardingsphere.io/schema/shardingsphere/sharding"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://shardingsphere.io/schema/shardingsphere/sharding
http://shardingsphere.io/schema/shardingsphere/sharding/sharding.xsd">
<bean id="device" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="jdbcUrl" value="${device.jdbc.url}"/>
<property name="username" value="${device.jdbc.username}"/>
<property name="password" value="${device.jdbc.password}"/>
<property name="maximumPoolSize" value="${maxPoolSize}"/>
<property name="minimumIdle" value="${minPoolSize}"/>
<property name="idleTimeout" value="100"/>
<property name="connectionTimeout" value="5000"/>
</bean>
<bean id="device000" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="jdbcUrl" value="${device000.jdbc.url}"/>
<property name="username" value="${device000.jdbc.username}"/>
<property name="password" value="${device000.jdbc.password}"/>
<property name="maximumPoolSize" value="${maxPoolSize}"/>
<property name="minimumIdle" value="${minPoolSize}"/>
<property name="idleTimeout" value="100"/>
<property name="connectionTimeout" value="5000"/>
</bean>
<bean id="device001" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="jdbcUrl" value="${device001.jdbc.url}"/>
<property name="username" value="${device001.jdbc.username}"/>
<property name="password" value="${device001.jdbc.password}"/>
<property name="maximumPoolSize" value="${maxPoolSize}"/>
<property name="minimumIdle" value="${minPoolSize}"/>
<property name="idleTimeout" value="100"/>
<property name="connectionTimeout" value="5000"/>
</bean>
<bean id="device002" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="jdbcUrl" value="${device002.jdbc.url}"/>
<property name="username" value="${device002.jdbc.username}"/>
<property name="password" value="${device002.jdbc.password}"/>
<property name="maximumPoolSize" value="${maxPoolSize}"/>
<property name="minimumIdle" value="${minPoolSize}"/>
<property name="idleTimeout" value="100"/>
<property name="connectionTimeout" value="5000"/>
</bean>
<bean id="device003" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="jdbcUrl" value="${device003.jdbc.url}"/>
<property name="username" value="${device003.jdbc.username}"/>
<property name="password" value="${device003.jdbc.password}"/>
<property name="maximumPoolSize" value="${maxPoolSize}"/>
<property name="minimumIdle" value="${minPoolSize}"/>
<property name="idleTimeout" value="100"/>
<property name="connectionTimeout" value="5000"/>
</bean>
<!-- inline 表达式写法(Groovy静态语言,同样支持复杂算法)-->
<sharding:inline-strategy id="indexDatabaseShardingStrategy" sharding-column="device_code" algorithm-expression="device00$->{Math.abs(device_code.hashCode()).intdiv(32) % 4}" />
<sharding:inline-strategy id="infoDatabaseShardingStrategy" sharding-column="device_uid" algorithm-expression="device00$->{Math.abs(device_uid.hashCode()).intdiv(32) % 4}" />
<sharding:inline-strategy id="deviceUserDatabaseShardingStrategy" sharding-column="imei" algorithm-expression="device00$->{Math.abs(imei.hashCode()).intdiv(32) % 4}" />
<sharding:inline-strategy id="deviceInfoShardingStrategy" sharding-column="device_uid" algorithm-expression="device_info_$->{String.format('%d', Math.abs(device_uid.hashCode()) % 32)}" />
<sharding:inline-strategy id="deviceIndexShardingStrategy" sharding-column="device_code" algorithm-expression="device_index_$->{String.format('%d', Math.abs(device_code.hashCode()) % 32)}" />
<sharding:inline-strategy id="deviceUserShardingStrategy" sharding-column="imei" algorithm-expression="device_user_$->{String.format('%d', Math.abs(imei.hashCode()) % 32)}" />
<sharding:data-source id="shardingDataSource">
<sharding:sharding-rule data-source-names="device,device000,device001,device002,device003" default-data-source-name="device">
<sharding:table-rules>
<!-- actual-data-nodes为真实表配置,多个真实表可用逗号分开。generate-key-column为sharding的自增策略键,不用的话就删除 -->
<sharding:table-rule logic-table="device_info" actual-data-nodes="device00$->{0..3}.device_info_$->{0..31}"
database-strategy-ref="infoDatabaseShardingStrategy" table-strategy-ref="deviceInfoShardingStrategy"/>
<sharding:table-rule logic-table="device_index" actual-data-nodes="device00$->{0..3}.device_index_$->{0..31}"
database-strategy-ref="indexDatabaseShardingStrategy" table-strategy-ref="deviceIndexShardingStrategy"/>
<sharding:table-rule logic-table="device_user" actual-data-nodes="device00$->{0..3}.device_user_$->{0..31}"
database-strategy-ref="deviceUserDatabaseShardingStrategy" table-strategy-ref="deviceUserShardingStrategy"/>
</sharding:table-rules>
</sharding:sharding-rule>
<!-- 打印sharding执行的sql语句,可查看逻辑表和真实表执行情况;还需配置pom.xml并添加logback.xml -->
<sharding:props>
<prop key="sql.show">false</prop>
</sharding:props>
</sharding:data-source>
</beans>
数据源配置:
XML
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!--sale 库-->
<!-- 读写分离数据源 -->
<bean id="dataSource" class="com.vivo.framework.mybatis.singleton.SingletonDataSource" destroy-method="close" init-method="initialize">
<property name="logicName" value="saleDatabase"/>
</bean>
<!--3、配置SqlSessionFactory对象 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!--注入数据库连接池 -->
<property name="dataSource" ref="dataSource" />
<!--配置mybatis全局配置文件:mybatis-config.xml -->
<property name="configLocation" value="classpath:META-INF/mybatis/mybatis-config.xml" />
<!--扫描entity包,使用别名,多个用;隔开 -->
<!-- <property name="typeAliasesPackage" value="vivo-device.internet.*.dal.entity" /> -->
<!--扫描sql配置文件:mapper需要的xml文件 -->
<property name="mapperLocations" value="classpath*:META-INF/mybatis/mapper/sale/*.xml" />
</bean>
<!--4、配置扫描Dao接口包,动态实现DAO接口,注入到spring容器 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!--注入SqlSessionFactory -->
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
<!-- 给出需要扫描的Dao接口 -->
<property name="basePackage" value="com.vivo.internet.vivodevice.sale.dao" />
</bean>
<!--3、配置SqlSessionFactory对象 -->
<bean id="shardingSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!--注入数据库连接池 -->
<property name="dataSource" ref="shardingDataSource"/>
<!--配置mybatis全局配置文件:mybatis-config.xml -->
<property name="configLocation" value="classpath:META-INF/mybatis/mybatis-config.xml"/>
<!--扫描entity包,使用别名,多个用;隔开 -->
<!-- <property name="typeAliasesPackage" value="com.vivo.internet.internet.*.dal.entity" /> -->
<!--扫描sql配置文件:mapper需要的xml文件 -->
<property name="mapperLocations" value="classpath*:META-INF/mybatis/mapper/device/*/*.xml"/>
</bean>
<!--4、配置扫描Dao接口包,动态实现DAO接口,注入到spring容器 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!--注入SqlSessionFactory -->
<property name="sqlSessionFactoryBeanName" value="shardingSqlSessionFactory"/>
<!-- 给出需要扫描的Dao接口 -->
<property name="basePackage" value="com.vivo.internet.vivodevice.device.machine.dao"/>
</bean>
<!--4、配置card SqlSessionFactory对象 -->
<bean id="cardShardingSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!--注入数据库连接池 -->
<property name="dataSource" ref="cardShardingDataSource"/>
<!--配置mybatis全局配置文件:mybatis-config.xml -->
<property name="configLocation" value="classpath:META-INF/mybatis/mybatis-config.xml"/>
<!--扫描entity包,使用别名,多个用;隔开 -->
<!-- <property name="typeAliasesPackage" value="com.vivo.internet.internet.*.dal.entity" /> -->
<!--扫描sql配置文件:mapper需要的xml文件 -->
<property name="mapperLocations" value="classpath*:META-INF/mybatis/mapper/warranty/*/*.xml"/>
</bean>
<!--4、配置扫描card Dao接口包,动态实现DAO接口,注入到spring容器 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!--注入SqlSessionFactory -->
<property name="sqlSessionFactoryBeanName" value="cardShardingSqlSessionFactory"/>
<!-- 给出需要扫描的Dao接口 -->
<property name="basePackage" value="com.vivo.internet.vivodevice.warranty.card.dao"/>
</bean>
<!-- 读写分离数据源 -->
<!--
<bean id="warrantyDataSource" class="com.vivo.framework.mybatis.singleton.SingletonDataSource" destroy-method="close" init-method="initialize">
<property name="logicName" value="warrantyDatabase"/>
</bean>-->
<!--配置整合mybatis过程 -->
<!--配置服务宝数据源-->
<bean id="warrantyDataSource" class="com.vivo.framework.mybatis.singleton.SingletonDataSource"
destroy-method="close" init-method="initialize">
<property name="logicName" value="warrantyDatabase"/>
</bean>
<!--配置服务宝数据库SqlSessionFactory对象 -->
<bean id="warrantySqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!--注入数据库连接池 -->
<property name="dataSource" ref="warrantyDataSource"/>
<!--配置mybatis全局配置文件:mybatis-config.xml -->
<property name="configLocation" value="classpath:META-INF/mybatis/mybatis-config.xml"/>
<!--扫描entity包,使用别名,多个用;隔开 -->
<!-- <property name="typeAliasesPackage" value="warranty-console.internet.*.dal.entity" /> -->
<!--扫描sql配置文件:mapper需要的xml文件 -->
<property name="mapperLocations" value="classpath*:META-INF/mybatis/mapper/warranty/*/*.xml"/>
</bean>
<!--配置扫描Dao接口包,动态实现DAO接口,注入到spring容器 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!--注入SqlSessionFactory -->
<property name="sqlSessionFactoryBeanName" value="warrantySqlSessionFactory"/>
<!-- 给出需要扫描的Dao接口 -->
<property name="basePackage" value="com.vivo.internet.vivodevice.warranty.card.dao,com.vivo.internet.vivodevice.warranty.sim.dao,com.vivo.internet.vivodevice.warranty.card.dao,com.vivo.internet.vivodevice.warranty.normarl.dao"/>
</bean>
<!--配置只读库数据源-->
<bean id="warrantyReadOnlyDatabase" class="com.vivo.framework.mybatis.singleton.SingletonDataSource" destroy-method="close"
init-method="initialize">
<property name="logicName" value="warrantyReadOnlyDatabase"/>
</bean>
<!--配置warranty SqlSessionFactory对象 -->
<bean id="warrantyReadSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!--注入数据库连接池 -->
<property name="dataSource" ref="warrantyReadOnlyDatabase"/>
<!--配置mybatis全局配置文件:mybatis-config.xml -->
<property name="configLocation" value="classpath:META-INF/mybatis/mybatis-config.xml"/>
<!--扫描sql配置文件:mapper需要的xml文件 -->
<property name="mapperLocations" value="classpath*:META-INF/mybatis/mapper/card/read/*.xml"/>
</bean>
<!--配置扫描Dao接口包,动态实现DAO接口,注入到spring容器 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!--注入SqlSessionFactory -->
<property name="sqlSessionFactoryBeanName" value="warrantyReadSqlSessionFactory"/>
<!-- 给出需要扫描的Dao接口 -->
<property name="basePackage" value="com.vivo.internet.vivodevice.card.dao.read"/>
</bean>
<!--配置deviceUserDataSource数据源-->
<bean id="deviceUserDataSource" class="com.vivo.framework.mybatis.singleton.SingletonDataSource" destroy-method="close"
init-method="initialize">
<property name="logicName" value="deviceDatabase"/>
</bean>
<!--配置warranty SqlSessionFactory对象 -->
<bean id="deviceUserSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!--注入数据库连接池 -->
<property name="dataSource" ref="deviceUserDataSource"/>
<!--配置mybatis全局配置文件:mybatis-config.xml -->
<property name="configLocation" value="classpath:META-INF/mybatis/mybatis-config.xml"/>
<!--扫描entity包,使用别名,多个用;隔开 -->
<!-- <property name="typeAliasesPackage" value="com.vivo.internet.internet.*.dal.entity" /> -->
<!--扫描sql配置文件:mapper需要的xml文件 -->
<property name="mapperLocations" value="classpath*:META-INF/mybatis/mapper/device/deviceuser/*.xml"/>
</bean>
<!--配置扫描Dao接口包,动态实现DAO接口,注入到spring容器 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!--注入SqlSessionFactory -->
<property name="sqlSessionFactoryBeanName" value="deviceUserSqlSessionFactory"/>
<!-- 给出需要扫描的Dao接口 -->
<property name="basePackage" value="com.vivo.internet.vivodevice.device.machine.deviceuser"/>
</bean>
</beans>
maven坐标:
XML
<dependency>
<groupId>io.shardingsphere</groupId>
<artifactId>sharding-core</artifactId>
<version>${sharding-jdbc.version}</version>
</dependency>
<dependency>
<groupId>io.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-namespace</artifactId>
<version>${sharding-jdbc.version}</version>
</dependency>
XML
<sharding-jdbc.version>3.1.0</sharding-jdbc.version>
java落地分库分表算法:
java
import org.apache.commons.lang.StringUtils;
/**
*@author PAGEQIU
*@date 2019/6/5
*@描述:
**/
public class SplitTableUtil {
/**
* 分表算法
* @param deviceUid
* @return
*/
public static Integer splitTable(String deviceUid) {
if(StringUtils.isNotBlank(deviceUid) && deviceUid.hashCode() != 0) {
if (deviceUid.hashCode() == Integer.MIN_VALUE) {
return Integer.MAX_VALUE % 32;
} else {
return Math.abs(deviceUid.hashCode()) % 32;
}
}else{
return 0;
}
}
/**
* 分库算法
* @param deviceUid
* @return
*/
public static Integer splitDataBase(String deviceUid) {
if(StringUtils.isNotBlank(deviceUid) && deviceUid.hashCode() != 0) {
if (deviceUid.hashCode() == Integer.MIN_VALUE) {
return Integer.MAX_VALUE/32%4;
} else {
return Math.abs(deviceUid.hashCode()) / 32 % 4;
}
}else{
return 0;
}
}
public static void main(String[] args) {
System.out.println(splitDataBase("303G3G068B00000"));
System.out.println(splitTable("303G3G068B00000"));
}
}
c、进一步增加响应时间
采用device_code维度的缓存设备信息
缓存方案:
1)、懒加载的方式:查询设备信息时存储至redis,失效时间24小时;
2)、预加载
说明:
a、每日从大数据同步设备信息,此时可以将缓存的值设置为空对象{},失效时间为1个月+一天的随机秒数
b、激活提醒接口(查询)场景,判断是null还是空对象,如果是空对象,直接返回,如果是null则回源数据库,如果存在,则加载对应的电子保卡数据到缓存中,且过期时间为1天,如果不存在,则将空对象放到缓存中;失效时间设置为30分钟
c、激活接口,则删除缓存中的对象,删除失败则回滚
d、只在同步发货数据且是插入发货数据的时候才设置空对象缓存(SyncMobileInfoFromBigDataByHourTask、SyncDeviceInfoTask)
ps:失效时间,设置需要防止同时失效,key的已经过期总量超过25%,会导致redis阻塞