分布式项目使用Redis实现数据库对象自增主键ID
hello。大家好,我是灰小猿,一个超会写bug的程序猿!
在分布式项目中,数据表的主键ID一般可能存在于UUID或自增ID这两种形式,UUID好理解而且实现起来也最容易,但是缺点就是数据表中的主键ID是32位的字符串,在大数据查询等情况下性能会相对比较差,所以在需求允许的情况下,我们通常会优先考虑使用自增ID来代替UUID使用。
在分布式项目中如果你的数据表的主键ID是自增ID,那么常见的生成对象主键ID的方式有:
-
雪花算法
-
优点:实现简单
-
缺点:生成ID较长、生成ID不连续,会造成ID浪费
-
-
采用框架自带的ID生成器,如MybatisPlus的@AutoID
-
优点:依赖框架,实现简单
-
缺点:无法在插入对象前获取到对象的主键ID
-
-
采用Redis缓存生成主键ID等
-
优点:生成的ID连续,数据在表中的可读性好
-
缺点:借助Redis缓存,实现相比前两种较复杂
-
通过雪花算法生成对象主键ID的方式我们在之前的文章中已经介绍过了,这篇文章我们主要介绍如何通过Redis来实现生成对象自增ID的方法。
1、缓存实现原理
通过Redis实现对象自增ID的方式,主要是通过Redis的INCR
和 INCRBY
命令来对键值进行递增操作,从而实现计数器的功能,主要原因是Redis 是单线程模型,所有命令都是原子操作,不会发生竞态条件,
在使用时要留意以下特点与注意事项
-
原子性:
INCR
、INCRBY
和INCRBYFLOAT
命令都是原子性的,这意味着如果多个客户端同时执行这些命令,Redis 会保证每个命令的执行不会发生竞态条件。 -
数据类型要求:这些命令要求操作的值是整数(
INCR
和INCRBY
)或浮点数(INCRBYFLOAT
)。如果键对应的值不是数值类型,Redis 会返回错误。 -
性能:Redis 是单线程的,所有命令都是原子操作,因此在高并发环境下执行这些命令时,性能表现非常好。
-
键不存在的情况:如果执行
INCR
或INCRBY
时,键不存在,Redis 会自动创建这个键并初始化其值为 0,然后进行递增。
2、Redis工具类实现ID自动生成
了解到通过Redis实现的原理之后,就是如何设计缓存以保证每张表的数据之间不会相互影响,
以user表为例,对应的model为UserModel,那么我们就可以将model名称作为key,当前已有的最大的ID作为value来进行存储,因为每一张表对应的model名称都是不一样的,所以这里一定不会出现key重复的情况,
下面是一些在Redis中生成和获取对象自增ID的工具方法,方便新建对象时直接调用
@Component("redisUtils")
@RequiredArgsConstructor
@Slf4j
public class RedisUtils implements InitializingBean {
private final StringRedisTemplate stringRedisTemplate;
private static RedisUtils redisUtils;
@Override
public void afterPropertiesSet() {
redisUtils = this;
}
/**
* 获取自增ID
*/
public static <T> Integer getIncr(Class<T> tClass) {
try {
String key = tClass.getName();
Long increment = redisUtils.stringRedisTemplate.opsForValue().increment(key, 1);
return Math.toIntExact(increment);
} catch (Exception e) {
e.printStackTrace();
throw new RedisSystemException(e.getMessage(), e);
}
}
/**
* 获取自增ID,指定递增长度,用于批量创建对象时,只请求一次Redis
* 如批量创建五个对象,delta等于5
*/
public static <T> Integer getIncr(Class<T> tClass, int delta) {
try {
String key = tClass.getName();
Long increment = redisUtils.stringRedisTemplate.opsForValue().increment(key, delta);
return Math.toIntExact(increment);
} catch (Exception e) {
e.printStackTrace();
throw new RedisSystemException(e.getMessage(), e);
}
}
/**
* 缓存预热,用于项目启动时初始化Redis中各个表的最大ID
*/
public static <T> long incrPreheat(Class<T> tClass, Long maxValue) {
if (maxValue == null) {
maxValue = 0L;
}
if (maxValue < 0) {
//异常抛出,预热的初始值不能小于0
}
Integer incr = getIncr(tClass);
if (maxValue <= incr) {
return incr;
}
return getIncr(tClass, (int) (maxValue - incr));
}
}
解释:
InitializingBean是Spring提供的拓展性接口,InitializingBean接口为bean提供了属性初始化后的处理方法,它只有一个afterPropertiesSet方法,凡是继承该接口的类,在bean的属性初始化后都会执行该方法。
3、缓存预热
缓存预热是指在项目启动时,将每一张表当前最大的主键ID预先加载到Redis中,这样在后面使用的时候,就可以直接从Redis中获取下一次ID即可,不需要再去访问数据库查询最大ID,缓存预热一般会建立在Redis专门的初始化类中,以便在启动项目时可以运行该类,具体如下:
/**
* 缓存预热
*
* @author hxy
*/
@Component
@DependsOn("redisUtils")
@RequiredArgsConstructor
public class RedisInit {
private final UserMapper userMapper;
/**
* 获取每一张表中当前的最大ID,然后预热到redis中
*/
@PostConstruct
public void init() {
RedisUtils.incrPreheat(UserModel.class, userMapper.maxId());
}
}
解释:
@DependsOn注解可以定义在类和方法上,意思是我这个组件要依赖于另一个组件,也就是说被依赖的组件会比该组件先注册到IOC容器中。
解释:@PostConstruct注解
在Java中,@PostConstruct注解,通常用于标记一个方法,它表示该方法在类实例化之后(通过构造函数创建对象之后)立即执行。
加上@PostConstruct注解的方法会在对象的所有依赖项都已经注入完成之后执行。通过使用@PostConstruct注解,我们可以确保在对象完全创建和初始化之后才执行这些操作。这个注解通常用在依赖注入(Dependency Injection)的框架中,例如Spring。
@PostConstruct 注解可以用在任何类的方法上,但它最常用于标记在 Spring Framework 中的 Bean 类中的初始化方法。
4、model层封装调用
在实例化一个model对象的时候,要将model的主键ID进行赋值,这个时候就要从Redis中获取到当前对象应该对应的主键ID,我们可以在model类中构建一个newInstance方法或有参构造,来专门的生成需要赋值主键ID的对象,在方法中调用redis工具类的getIncr方法,获取到最新最小未使用的主键ID并赋值给新建的model即可。
@Getter
@Setter
public class UserModel {
private Integer id;
private String name;
private Integer age;
private String tel;
public UserModel() {
}
/**
* 提供一个能够生成自增ID的有参构造,需要生成自增ID时调用
*/
public UserModel(boolean autoId) {
if (autoId) {
this.id = RedisUtils.getIncr(UserModel.class);
}
}
}
如果你是需要批量创建对象并且给对象赋值主键ID的情况,可以按照如下方式使用,这样可以只调用一次Redis,避免重复调用Redis影响性能。
@Service
@RequiredArgsConstructor
public class UserServiceImpl {
private final UserMapper userMapper;
public void batchAddUser(){
int addNumber = 10;
//提前生成自定跨度的ID,比如之前最大ID是6,获取后得到的incr是16
Integer incr = RedisUtils.getIncr(UserModel.class, addNumber);
List<UserModel> userModels = new ArrayList<>();
for (int i = 0; i < addNumber; i++) {
UserModel userModel = new UserModel();
//每一个主键ID依次递减使用
userModel.setId(incr--);
userModels.add(userModel);
}
userMapper.insertBatch(userModels);
}
}
至此,通过Redis来获取对象自增ID的方法已经完成,如果在使用过程中有其他场景需求,可以对redisUtils中的方法进行拓展即可。
我是灰小猿,我们下期见!