Bootstrap

分布式项目使用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的INCRINCRBY 命令来对键值进行递增操作,从而实现计数器的功能,主要原因是Redis 是单线程模型,所有命令都是原子操作,不会发生竞态条件,

在使用时要留意以下特点与注意事项

  1. 原子性INCRINCRBYINCRBYFLOAT 命令都是原子性的,这意味着如果多个客户端同时执行这些命令,Redis 会保证每个命令的执行不会发生竞态条件。

  2. 数据类型要求:这些命令要求操作的值是整数(INCRINCRBY)或浮点数(INCRBYFLOAT)。如果键对应的值不是数值类型,Redis 会返回错误。

  3. 性能:Redis 是单线程的,所有命令都是原子操作,因此在高并发环境下执行这些命令时,性能表现非常好。

  4. 键不存在的情况:如果执行 INCRINCRBY 时,键不存在,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中的方法进行拓展即可。

我是灰小猿,我们下期见!

;