数据库 分表如何查询(如何存取海量数据)
数据库 分表如何查询(如何存取海量数据)我们定义了两个数据库访问层的接口package com.example.itspringbootmybatisshared.domain; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.math.BigDecimal; import java.util.Date; /** * 用户信息 * * @author hongcunlin */ @Data @NoArgsConstructor @AllArgsConstructor @Builder public class UserInfo { /** * 自增ID */ private Long i
前言笔者维护的许多系统,大多在线上稳定运行多年,拥有大量的用户,便拥有海量的数据,那么如何在数量多的情况确保数据库的存取性能呢?这便是本文要介绍的一个数据库分表方案了。
本文基于Mybaits实现,前置知识可以参考前文:
SpringBoot集成MyBatis的相关要点
思路分表的最大难点,在于确定你存取的数据在哪张表上。
解决方案在于将一个不可或缺的字段(主键)作为分表依据,我们可以通过对其进行哈希计算,再取模表的数量,即可确定数据在第几张表上了,最后再对表名进行拼接,即可确定表名。
案例1.domain层
我们定义了一个和数据库表字段对应的javaBean
package com.example.itspringbootmybatisshared.domain;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.util.Date;
/**
* 用户信息
*
* @author hongcunlin
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserInfo {
/**
* 自增ID
*/
private Long id;
/**
* 用户名
*/
private String name;
/**
* 用户余额
*/
private BigDecimal money;
/**
* 创建时间
*/
private Date createTime = new Date();
}
2.mapper层
我们定义了两个数据库访问层的接口
package com.example.itspringbootmybatisshared.mapper;
import com.example.itspringbootmybatisshared.domain.UserInfo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Component;
/**
* @author hongcunlin
*/
@Mapper
@Component
public interface UserInfoMapper {
/**
* 新增用户
*
* @param userInfo 用户信息
* @param tableName 表名
*/
void addUserInfo(@Param("userInfo") UserInfo userInfo @Param("tableName") String tableName);
/**
* 查询用户信息
*
* @param id 用户id
* @param tableName 表名
* @return 用户信息
*/
UserInfo getUserInfoById(@Param("id") Long id @Param("tableName") String tableName);
}
编写对应的SQL
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--mapper-->
<mapper namespace="com.example.itspringbootmybatisshared.mapper.UserInfoMapper">
<!--获取所有用户信息-->
<select id="getAll" resultType="com.example.itspringbootmybatisshared.domain.UserInfo">
select *
from user_info;
</select>
<!-- 新增用户,注意tableName不能预编译 -->
<insert id="addUserInfo">
insert into ${tableName} (id name money create_time)
values (#{userInfo.id jdbcType=BIGINT}
#{userInfo.name jdbcType=VARCHAR}
#{userInfo.money jdbcType=DECIMAL}
#{userInfo.createTime jdbcType=DATE})
</insert>
<!-- 查询用户,注意tableName不能预编译 -->
<select id="getUserInfoById" resultType="com.example.itspringbootmybatisshared.domain.UserInfo">
select *
from ${tableName}
where id = #{id}
</select>
</mapper>
3.Service层
我们定义了用户信息操作的接口与实现,主要是对mapper做了一层表名的封装,统一处理表名的相关逻辑
package com.example.itspringbootmybatisshared.service;
import com.example.itspringbootmybatisshared.domain.UserInfo;
/**
* 用户Service层接口
*
* @author hongcunlin
*/
public interface UserInfoService {
/**
* 新增用户信息
*
* @param userInfo 用户信息
*/
void addUserInfo(UserInfo userInfo);
/**
* 查询用户信息
*
* @param id 用户id
* @return 用户信息
*/
UserInfo getUserInfoById(Long id);
}
package com.example.itspringbootmybatisshared.service.impl;
import com.example.itspringbootmybatisshared.domain.UserInfo;
import com.example.itspringbootmybatisshared.mapper.UserInfoMapper;
import com.example.itspringbootmybatisshared.service.UserInfoService;
import com.example.itspringbootmybatisshared.utils.TableUtils;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
* 用户Service层实现
*
* @author hongcunlin
*/
@Service
public class UserInfoServiceImpl implements UserInfoService {
/**
* 用户mapper层
*/
@Resource
private UserInfoMapper userInfoMapper;
/**
* 新增用户信息
*
* @param userInfo 用户信息
*/
@Override
public void addUserInfo(UserInfo userInfo) {
userInfoMapper.addUserInfo(userInfo TableUtils.getTabNameById(userInfo.getId()));
}
/**
* 查询用户信息
*
* @param id 用户id
* @return 用户信息
*/
@Override
public UserInfo getUserInfoById(Long id) {
return userInfoMapper.getUserInfoById(id TableUtils.getTabNameById(id));
}
}
4.工具层
这是本文的核心,就是如何确定表在哪里。
我们定义了2个常量,分别是表名的基础名称、表数量。
我们定义了1个获取表名的方法,便是通过对用户的id进行hash计算,在%表数量,最后再拼接表名的基础组成部分。
package com.example.itspringbootmybatisshared.utils;
/**
* 分表工具
* 定位表
*
* @author hongcunlin
*/
public class TableUtils {
/**
* 表名基本组成部分
*/
private static final String TAB_NAME_BASE = "user_info_";
/**
* 表数量
*/
private static final Integer TAB_NUM = 8;
/**
* 根据id获取表名
*
* @param id 用户id
* @return 表名
*/
public static String getTabNameById(Long id) {
return TAB_NAME_BASE (id.hashCode() % TAB_NUM 1);
}
}
测试
我们先测试新增数据
package com.example.itspringbootmybatisshared;
import com.example.itspringbootmybatisshared.domain.UserInfo;
import com.example.itspringbootmybatisshared.service.UserInfoService;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import java.math.BigDecimal;
@SpringBootTest
public class UserAddTest {
@Resource
UserInfoService userInfoService;
@Test
void addUserInfoTest() {
UserInfo userInfo = new UserInfo();
userInfo.setId(10000L);
userInfo.setMoney(new BigDecimal("10.00"));
userInfo.setName("中国电信");
userInfoService.addUserInfo(userInfo);
}
}
运行完上面的单元测试,我查询数据库,可以看到该表user_info_1已有该数据了
我们再进行查询数据测试
package com.example.itspringbootmybatisshared;
import com.example.itspringbootmybatisshared.domain.UserInfo;
import com.example.itspringbootmybatisshared.service.UserInfoService;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
@SpringBootTest
public class UserQueryTest {
@Resource
UserInfoService userInfoService;
@Test
void getUserInfoTest() {
UserInfo userInfo = userInfoService.getUserInfoById(10000L);
System.out.println(userInfo);
}
}
运行完上面的单元测试,可以看到顺利打印出刚才新增的数据
说明分表读写是正常的。
最后分表很好的解决了我们数据量大存取的性能问题。
此外,分库的解决方案和分表类似,只不过多了1层确定数据是在哪个库上的逻辑,而这逻辑和确定数据在哪张表上是一样的,依旧是依赖一个业务主键进行哈希计算、取模确定的。
然而分表也有一个缺点,就是你以后的写入、查询,都需要附带一个业务主键,由它来确定库表在的具体位置,不过数据基本都有主键的,而那个主键通常就可以用来业务主键,逻辑上并没有增加多少障碍。