他们说,一个表的数据不要太多,太多就要分表,再多就要分库。
那就听他们的。然后为了分库分表,引入了一堆中间件,重不重?烦不烦?
为此,本文尝试提供一个小而美的分表实现,其基于 Spring Data JPA 模块,同时为了方便,还用到了 MapStruct 和 QueryDsl,后两项非必需。
说它小而美,是因为它只解决了一个最简单的场景:
我们卖了很多 IoT 设备,这些设备会不定时通过 MQTT 向服务端传送信息,服务端先是保存这些信息,再用定时任务每天分析这些数据。
一个简化后的信息,有一个自增的 id 字段、一个代表该设备用户的 userNo 字段和一个代表消息内容的 message 字段。
业务上对信息的使用很简单:
-
将信息存入表。 -
将信息表中的数据,按单个用户查出并分析,将分析结果存入其它的表。
所以,在没分表之前,这个信息表基本只会用到如下语句:
insert into message values (?);
select * from message where user_no = ?;
但随着用户的变多,每天要接收处理的数据要迅速增加,分表早晚要进行。
分表之后,用到的语句基本不变,但怎么将要执行的语句定位到给定的子表中,需要细细考虑与设计。
因为我们分析的主体是单个用户的信息,所以,我们根据 userNo 字段来决定数据落在哪张表中。
为了演示方便,假设我们只分2张表:message_01 和 message_02。这两张表的结构完全一样。
在插入数据和查询数据时,我们对 userNo 的 hashcode 对 2 取余:
userNo.hashCode() % 2;
-
当余数为 0 时,数据落在 message_01 表中; -
反之,余数为 1 时,数据落在 message_02 表中;
如果你要分更多的表,在上面基础上调整即可。
以上是理论阶段,接下来看看怎么用代码实现
因为用的是 Spring Data JPA 模块来做的,所以,我们需要两个实体类 —— Message01.java 和 Message02.java:
@Data
@Entity
@Table(name = "message_01")
public class Message01 extends Message {
}
@Data
@Entity
@Table(name = "message_02")
public class Message02 extends Message {
}
这两个实体类都是空类,主要是为了符合 Spring 对实体的要求,一张表要对应一个实体。实体真正的信息都在父类 Message.java 中:
@Data
@MappedSuperclass
public class Message {
@Id
@Column(columnDefinition = "bigint(20) COMMENT 'ID,自增'")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "user_no", columnDefinition = "varchar(32) COMMENT '用户编号'", nullable = false)
String userNo;
@Column(columnDefinition = "varchar(100) COMMENT '消息'", nullable = false)
String message;
}
注意这里 @MappedSuperclass 注解的使用,表示子类将继承父类对表的字段的定义。
按 Spring 的规范,有了实体类,接下来是 Repository 接口类,以方便进行 CRUD 操作:
public interface Message01Repository extends JpaRepository<Message01, Long> {
}
public interface Message02Repository extends JpaRepository<Message02, Long> {
}
这两个类没什么可说的。。
下面是重要的实现阶段了。
对于一个分表的实现,最好设计成对使用者来说是透明的,即,使用者在使用时,只用操作 Message 这张”抽象的表”,而不用知道有 Message01 和 Message02 的存在!
先看看针对新增和查询的消息服务 MessageService.java:
public interface MessageService {
/**
* 新增一条消息
*
* @return 自增的id
*/
Long addMessage(Message msg);
/**
* 查询出给定用户的所有消息
*/
List<Message> findAll(String userNo);
}
我们再考虑怎么实现上述接口…
-
无论是新增还是查询,我们都需要用到具体的 Repository 接口,所以 MessageService 的实现类中要有注入 Message01Repository 和 Message02Repository 这两个 bean; -
在新增时,我们要把 Message 入参转换为具体的子类(Message01 或 Message02),再使用相应的 Repository 进行 save() 操作; -
无论是新增还是查询,都要根据入参中的 userNo 信息定位到具体的 Repository 和 Message 子类。
有了以上分析,我们的 MessageService 的实现类大致如下:
@Service
public class MessageServiceImpl implements MessageService {
@Autowired
Message01Repository message01Repository;
@Autowired
Message02Repository message02Repository;
/**
* 用来将 {@link Message} 类转换为具体的子类
*/
@Autowired
MessageMapper messageMapper;
/**
* 子表到对应实体的映射
*/
Map<Integer, Supplier<? extends Message>> entityMap;
/**
* 子表到对应 Repository 的映射
*/
Map<Integer, JpaRepository<? extends Message, Long>> repositoryMap;
@PostConstruct
void init() {
repositoryMap = new HashMap<>();
repositoryMap.put(0, message01Repository);
repositoryMap.put(1, message02Repository);
entityMap = new HashMap<>();
entityMap.put(0, Message01::new);
entityMap.put(1, Message02::new);
}
/**
* 定位用户的数据在哪一张表
*/
int getTablePartitionFor(String userNo) {
return userNo.hashCode() % 2;
}
/**
* 定位用户所在具体实体类
*/
Supplier<? extends Message> getEntityFor(String userNo) {
return entityMap.get(getTablePartitionFor(userNo));
}
/**
* 找出操作用户数据表的 Repository
*/
JpaRepository<? extends Message, Long> getRepositoryFor(String userNo) {
return repositoryMap.get(getTablePartitionFor(userNo));
}
@Override
public Long addMessage(Message msg) {
// 找到操作用户数据的 Repository
JpaRepository repository = getRepositoryFor(msg.getUserNo());
// 找到用户数据对应的实体
Message entity = getEntityFor(msg.getUserNo()).get();
// 将入参数据设置到刚才的实体中
messageMapper.mapToTarget(msg, entity);
// 保存实体
repository.save(entity);
// 返回自增id
return entity.getId();
}
@Override
public List<Message> findAll(String userNo) {
// 找到操作用户数据的 Repository
JpaRepository repository = getRepositoryFor(userNo);
// 找到用户数据对应的实体
Message entity = getEntityFor(userNo).get();
entity.setUserNo(userNo);
// 按 userNo 查询
Example<Message> exam = Example.of(entity);
// 返回查询结果
return repository.findAll(exam);
}
}
为了方便理解,上述代码加入了详细的解释。
另外,MessageMapper 作为把 Message 转换到具体子类的转换类,其代码如下:
@Mapper
public interface MessageMapper {
Message mapToTarget(Message base, @MappingTarget Message target);
}
其具体的实现,由 MapStruct 自动生成。
Spring Data JPA 提供的查询不够强大,我一般会将其结合 QueryDsl 来使用,结合后,上面的 findAll() 方法还能这样实现:
@Service
public class MessageServiceImpl2 implements MessageService {
@PersistenceContext
private EntityManager em;
/**
* 子表到相应 EntityPath 的映射
*/
Map<Integer, EntityPath<?>> entityPathMap;
@PostConstruct
void init() {
entityPathMap = new HashMap<>();
entityPathMap.put(0, QMessage01.message01);
entityPathMap.put(1, QMessage02.message02);
}
int getTablePartitionFor(String userNo) {
return userNo.hashCode() % 2;
}
EntityPath<?> getEntityPathFor(String userNo) {
return entityPathMap.get(getTablePartitionFor(userNo));
}
public Long addMessage(Message msg) {
// 忽略
return null;
}
public List<Message> findAll(String userNo) {
EntityPath<?> path = getEntityPathFor(userNo);
JPAQuery<Message> query = new JPAQuery<Message>(em).from(path);
QMessageBase q = new QMessageBase(path.getRoot().toString());
// 添加更灵活的条件
query.where(q.userNo.eq(userNo));
return query.fetch();
}
}
之所以提及这个,因为当查询条件除了 userNo 外还有其它字段时,用 QueryDsl 处理会更加得心应手。
哦了,你还有别的疑惑吗?
对了,下面是使用效果截图:
– END –
原文始发于微信公众号(背井):基于 Spring Data JPA 实现简单的分表功能
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/246716.html