高并发&大数据量&毫秒级响应系统设计方案

一、面临的情况:

数据量:基础数据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阻塞

相关推荐
运气好好的2 小时前
如何处理死锁异常_ORA-00060捕获与重试机制设计
jvm·数据库·python
Promise微笑2 小时前
开关柜局放国产替代浪潮下:开关柜局放监测技术与实践深度解析
网络·数据库·人工智能
皮皮大人2 小时前
agent设计系统-大模型意图识别
前端·人工智能
三维搬砖者2 小时前
挑战AI辅助从零构建3D模型编辑器:01基于Vue3 + Three.js的现代化架构设计
前端·vue.js·github
GinoWi2 小时前
Python 集合
前端·python
时光足迹2 小时前
Tiptap之标注组件
前端·javascript·react.js
Filwaod2 小时前
Java面试现场:从Redis缓存到分布式事务,水货程序员李四的‘表演‘
java·jvm·spring boot·redis·mysql·面试·多线程
2401_867623982 小时前
解决Navicat多图纸模型工作区协同报错怎么办_外键关联与语法解析
jvm·数据库·python
时光足迹3 小时前
Tiptap 之自定义脚注组件
前端·javascript·react.js