如何存取海量数据-数据库分表实现方案

如何存取海量数据——数据库分表实现方案

前言

笔者维护的许多系统,大多在线上稳定运行多年,拥有大量的用户,便拥有海量的数据,那么如何在数量多的情况确保数据库的存取性能呢?这便是本文要介绍的一个数据库分表方案了。

本文基于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"?>




    
    

    
    
        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})
    

    
    

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层确定数据是在哪个库上的逻辑,而这逻辑和确定数据在哪张表上是一样的,依旧是依赖一个业务主键进行哈希计算、取模确定的。

然而分表也有一个缺点,就是你以后的写入、查询,都需要附带一个业务主键,由它来确定库表在的具体位置,不过数据基本都有主键的,而那个主键通常就可以用来业务主键,逻辑上并没有增加多少障碍。

如何存取海量数据——数据库分表实现方案

发表评论
留言与评论(共有 0 条评论) “”
   
验证码:

相关文章

推荐文章