笔者维护的许多系统,大多在线上稳定运行多年,拥有大量的用户,便拥有海量的数据,那么如何在数量多的情况确保数据库的存取性能呢?这便是本文要介绍的一个数据库分表方案了。
本文基于Mybaits实现,前置知识可以参考前文:
分表的最大难点,在于确定你存取的数据在哪张表上。
解决方案在于将一个不可或缺的字段(主键)作为分表依据,我们可以通过对其进行哈希计算,再取模表的数量,即可确定数据在第几张表上了,最后再对表名进行拼接,即可确定表名。
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 条评论) “” |