一、前言
上年接手了一个新项目,这个项目经历多N个团队离职和交接,累计了大量正常人类无法理解的逻辑。考虑到对接数据模块问题突出,决定先把ORM组件梳理下:sharding-jdbc和mybatis-plus。mybatis-plus稍微改下下也是可以支持分表的,但是业务上分库分片的地方太多,所以继续使用sharding-jdbc作为分库分表组件。这篇文章把其中的一些通用的技巧简单总结下。
二、代码环境
基础pom配置如下:
<!-- ShardingSphere -->
<!-- Sharding-JDBC -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc-core</artifactId>
<version>5.3.0</version>
</dependency>
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.33</version>
</dependency>
<!-- commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.10</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.6.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.7.10</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
<version>8.0.21</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
三、几个小技巧
1、mybatis-plus
-
自动填充updateTime和createTime
为什么要需要这俩字段且自动填充呢?三个原因,一是记录,新增和修改的地方逻辑很多,经常有遗漏,自动填充方便统一记录,确保时间准确 二是mysql要接入hive,用updateTime做增量统计。三是方便做数据归档。
拦截器添加剑时间
public class OperationInterceptor implements MetaObjectHandler {
private static final String CREATE_TIME = "createTime";
private static final String UPDATE_TIME = "updateTime";
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, CREATE_TIME, LocalDateTime::now, LocalDateTime.class);
this.strictInsertFill(metaObject, UPDATE_TIME, LocalDateTime::now, LocalDateTime.class);
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, UPDATE_TIME, LocalDateTime::now, LocalDateTime.class);
}
}
添加注解
@TableName("t_course")
@TableName("t_course")
@Data
@ToString
public class Course {
@TableId(value = "cid",type = IdType.ASSIGN_ID)
private Long cid;
private Long userId;
private int status;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
//注意是INSERT_UPDATE,插入和更新都会填充字段
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
}
配置注入
@Configuration
@MapperScan(basePackages = "com.*.**.mapper")
public class MybatisPlusConfig{
@Bean
public OperationInterceptor dataOperationInterceptor() {
return new OperationInterceptor();
}
}
- 移除xml
为什么要移除xml配置呢?实践中发现,配置了数据字段后,还需要经常在xml中在配置一遍;把sql写到xml中,容易遗漏逗号括号等,尤其是一些带复杂判断和拼接的sql。 望着茫茫几千行的sql拼接语句加几百个xml文件,开发中出现了很多配置错误,我经常跟同事吐槽,这考验的不是技术,而是眼力,但是老夫眼力真的不好。 因此这次优化直接把xml配置都干掉,直接使用lamda表达式实现增删改查,配合mybatis-x-generator插件新增表,效果是在团队内部消灭了低级的sql配置错误。
官网配置教程: baomidou.com/guides/wrap…
--- query
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.ne(User::getName, "老王");
--- update
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.notLike(User::getName, "王");
- 唯一键逻辑处理
在网路不稳定,重试等场景,幂等是常见的业务要求。旧项目使用redis锁做幂等判断,但是无法做到100%的可靠,因为redis自己不稳定,而且锁的有效期不好判断,我们有的数据是跨年插入的,因此必须有数据库唯一键约束兜底。
这里有两个操作,新增或者更新,逻辑删除,删除后新增。唯一键存在的情况下,新增会报数据约束异常,逻辑删除的唯一一个键无法插入。唯一键必须和删除标志联合唯一,删除标志使用当前行id值作为删除标志。示例如下:
@TableName("t_course")
@Data
@ToString
public class Course {
@TableId(value = "id",type = IdType.ASSIGN_ID)
private Long id;
private Long userId;
private Long corderNo;
private String cname;
private String brief;
private double price;
private int status;
@TableLogic(delval = "id")
private Integer isDelete;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
}
自动生成的删除语句如下:
UPDATE t_course_1 SET update_time=?, is_delete=id WHERE id=? AND is_delete=0 ::: [2024-11-03T11:35:18.845, 1852911715678773249]
2、sharding-jdbc
-
多字段复杂分片
场景:按日期分表,有creatTime没有账单日期则写入当月的表,如果带有账单日期settle_time则根据账单日期写入对应的日期表。
Sharing.yml
rules:
- !SHARDING
tables:
t_course:
actualDataNodes: db$->{0..1}.t_course
keyGenerateStrategy:
column: cid
keyGeneratorName: alg-snowflake
tableStrategy:
complex:
shardingColumns: settle_time,create_time
shardingAlgorithmName: t_course_complex_custom_algorithm
databaseStrategy:
standard:
shardingColumn: cid
shardingAlgorithmName: standard-range-db
shardingAlgorithms:
t_course_complex_custom_algorithm:
type: STANDARD_COMPLEX_TB
props:
strategy: complex
standard-range-db:
type: STANDARD_TEST_DB
keyGenerators:
alg-snowflake:
type: SNOWFLAKE
public class TableComplexKeysShardingAlgorithm implements ComplexKeysShardingAlgorithm<Comparable<?>> {
/**
* 订单id列名
*/
private static final String COLUMN_SETTLE_TIME = "settle_time";
/**
* 客户id列名
*/
private static final String COLUMN_CREATE_TIME = "create_time";
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames, ComplexKeysShardingValue<Comparable<?>> shardingValue) {
if (!shardingValue.getColumnNameAndRangeValuesMap().isEmpty()) {
throw new RuntimeException("不支持除了=和in的操作");
}
String logicTableName = shardingValue.getLogicTableName();
// 获取订单id
Collection<Comparable<?>> settleTime = shardingValue.getColumnNameAndShardingValuesMap().getOrDefault(COLUMN_SETTLE_TIME, new ArrayList<>(1));
// 获取客户id
Collection<Comparable<?>> createTimes = shardingValue.getColumnNameAndShardingValuesMap().getOrDefault(COLUMN_CREATE_TIME, new ArrayList<>(1));
List<String> result = new ArrayList<>();
if (CollectionUtils.isEmpty(settleTime)) {
result.add("bill");
} else {
result.add("bill" + settleTime.toArray()[0]);
}
return result;
}
@Override
public String getType() {
return "STANDARD_COMPLEX_TB";
}
@Override
public Properties getProps() {
return null;
}
@Override
public void init(Properties properties) {
}
}
- hint使用
shardingsphere.apache.org/document/5.…
强制走某个库
@Select("/* SHARDINGSPHERE_HINT: DATA_SOURCE_NAME=db1 */select * from t_course where cid=#{cid}")
List<Course> findCourse(@Param("cid") Long cid);
在强一致性场景,读写分离策略下,读主库:
@Select("/* SHARDINGSPHERE_HINT: WRITE_ROUTE_ONLY=false */select * from t_course where cid=#{cid}")
List<Course> findCourse(@Param("cid") Long cid);
- spi的使用
5.3以前版本的spi示例在官方文档描述还是比较清楚的,5.3版本难点在于找不到spi的接口文档,没办法只能去源代码中找。具体方法是github.com/apache/shar…,找到5.3的tag切换过去,模仿自带的加解密算法实现。
- 自定义加密算法
一般大公司有自己的加密算法,或者对接政府用国密,这时候需要自定义加密算法。示例如下:
rules:
- !ENCRYPT
tables:
t_user:
columns:
password:
cipherColumn: password_encrypt
encryptorName: encryptor-customize
encryptors:
encryptor_aes:
type: AES
props:
aes-key-value: 123456abc
digest-algorithm-name: SHA-1
encryptor-customize:
type: sha256
props:
aes-key-value: pre
META-INF下的文件名为org.apache.shardingsphere.encrypt.spi.EncryptAlgorithm
代码如下:
@Slf4j
public final class Sha256Encryptor implements StandardEncryptAlgorithm<Object, String> {
private Properties props;
@Override
public Properties getProps() {
return this.props;
}
@Override
public void init(Properties props) {
this.props = props;
}
public String encrypt(Object plaintext, EncryptContext encryptContext) {
if (null == plaintext) {
return null;
}
log.info("Sha256Encryptor encrypt,value:{}", plaintext);
return DigestUtils.sha256Hex(String.valueOf(plaintext));
}
public Object decrypt(String cipherValue, EncryptContext encryptContext) {
log.info("Sha256Encryptor decrypt,value:{}", cipherValue);
return cipherValue;
}
public String getType() {
return "sha256";
}
}
- 自定义主键生成算法
跟加密算法一样,公司也有自定义的,或者雪花算法扛不住qps,需要自定义性能更好的。
3、封装成starter
以上逻辑大部分是业务通用的,为了方便其他服务使用,将其中的功能封装了starter。示例如下:
- 代码迁移
@Configuration
@MapperScan(basePackages = "com.example.mapper")
public class MyAutoConfig {
//其他插件。。。。
@Bean
public OperationInterceptor dataOperationInterceptor() {
return new OperationInterceptor();
}
}
添加spi和autoconfig配置
- 使用方直接通过pon引入这个starter就可以
三、结尾
实际场景还有很多其他改造,如sql日志、数据库配置存储等,改造思想一样,但是通用性差一点,这里就不列出了。
如果这篇文章对您有帮助,记得点赞加关注!您的支持是我继续创作的动力!