Mybatis中如何生成全局主键ID

本文介绍在SpringBoot中集成SnowFlake算法,用于分布式系统中生成全局唯一主键ID。通过配置IdWorker并注入bean,可在插入操作时轻松获取全局ID。

Mybatis中如何生成全局主键ID

上篇我讲了在mybatis中,新增数据时如何返回自增主键,依靠的是数据库可设置主键自动递增的机制,但是这种方法生成的主键扩展性比较差,如在一个分布式的系统中,会造成主键重复的问题。今天这篇文章讲下在分布式系统中如何生成全局唯一主键ID。

常见的解决方案大家可以参考下这篇文章,作者基于漫画的方式讲解的很清晰;

漫画:什么是SnowFlake算法?

本文主要讲下在spring boot中如何集成SnowFlake算法,生成全局主键

SnowFlake算法github的网址: https://github.com/twitter/snowflake 但貌似已经不维护了 ~~

好了在上一篇文章代码的基础上,开始撸代码~~

源码地址

1.第一步导入IdWorker.java

该类是SnowFlake算法的核心,这是我在网上找的一个java版本

/**
 * From: https://github.com/twitter/snowflake
 * An object that generates IDs.
 * This is broken into a separate class in case
 * we ever want to support multiple worker threads
 * per process
 */
public class IdWorker {
    // 时间起始标记点,作为基准,一般取系统的最近时间(一旦确定不能变动)2017/1/1 0:0:0
    private final static long twepoch = 1483200000000L;
    // 机器标识位数
    private final static long workerIdBits = 5L;
    // 数据中心标识位数
    private final static long datacenterIdBits = 5L;
    // 机器ID最大值
    private final static long maxWorkerId = -1L ^ (-1L << workerIdBits);
    // 数据中心ID最大值
    private final static long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
    // 毫秒内自增位
    private final static long sequenceBits = 12L;
    // 机器ID偏左移12位
    private final static long workerIdShift = sequenceBits;
    // 数据中心ID左移17位
    private final static long datacenterIdShift = sequenceBits + workerIdBits;
    // 时间毫秒左移22位
    private final static long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

    private final static long sequenceMask = -1L ^ (-1L << sequenceBits);
    /* 上次生产id时间戳 */
    private static long lastTimestamp = -1L;
    // 0,并发控制
    private long sequence = 0L;

    private final long workerId;
    // 数据标识id部分
    private final long datacenterId;

    /**
     * @param workerId
     * 工作机器ID
     * @param datacenterId
     * 序列号
     */
    public IdWorker(long workerId, long datacenterId) {
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
        }
        if (datacenterId > maxDatacenterId || datacenterId < 0) {
            throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
        }
        this.workerId = workerId;
        this.datacenterId = datacenterId;
    }
    /**
     * 获取下一个ID
     * @return
     */
    public synchronized long nextId() {
        long timestamp = timeGen();
        if (timestamp < lastTimestamp) {
            throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
        }

        if (lastTimestamp == timestamp) {
            // 当前毫秒内,则+1
            sequence = (sequence + 1) & sequenceMask;
            if (sequence == 0) {
                // 当前毫秒内计数满了,则等待下一秒
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0L;
        }
        lastTimestamp = timestamp;
        // ID偏移组合生成最终的ID,并返回ID
        long nextId = ((timestamp - twepoch) << timestampLeftShift)
                | (datacenterId << datacenterIdShift)
                | (workerId << workerIdShift) | sequence;

        return nextId;
    }

    private long tilNextMillis(final long lastTimestamp) {
        long timestamp = this.timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = this.timeGen();
        }
        return timestamp;
    }

    private long timeGen() {
        return System.currentTimeMillis();
    }
}

2.对算法参数进行配置

导入idworker类后要想正常使用,肯定是要先配置一下:

首先在application.yml中新建如下2个配置参数,分别对应机器ID和序列号,多实例部署的话,不同实例该参数需配置不同值

util:
  #工作机器ID
  workerId: 5
  #序列号
  datacenterId: 10

然后创建配置类CustomerConfig,将IdWorker注入为bean

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConfigurationProperties(prefix = "util")
@Data
public class CustomerConfig {
    private Long workerId;
    private Long datacenterId;
    @Bean
    public IdWorker createIdWorker() {
        IdWorker worker = new IdWorker(workerId, datacenterId);
        return worker;
    }

}

3. 如何使用

配置好IdWorker后,使用非常简单,只要每次在调用insert操作时,我们显示的调用idworker的nextId()方法获取一个全局ID,然后将生成的ID赋值给即将插入的对象即可,具体代码如下:

DepartmentController.java

@Api(description = "department")
@RestController
@RequestMapping("dept")
public class DepartmentController {
    @Autowired
    public DepartmentService departmentService;
    @Autowired
    public IdWorker idWorker;
    
    @ApiOperation(value = "新增部门")
    @PostMapping("new")
    public ResultMsg newDepartment(@RequestBody Department department)
    {
        //每次插入数据时,调用nextId()获取一个全局ID
        department.setId(idWorker.nextId());
        int result = departmentService.insertDept(department);
        return ResultMsg.getMsg(result);
    }
}

4.测试

打开浏览器输入:http://localhost:9292/mybatis/swagger-ui.html 进入我们Swagger接口测试界面找到dept->new方法,输入测试参数,然后点击按钮try it out

我们连续插入多条看看效果:

源码地址

5.总结

相比较上篇文章中自增主键的方式,每条涉及插入的SQL语句都要设置useGeneratedKeys和keyProperty,全局获取ID的方式只要一次配置后,后边每次插入操作只需调用下idWorker.nextId()方法,简直不要太简单

建议大家在在实际开发过程中都采用这种方式来生成主键ID,可谓一劳永逸

### MyBatis 主键自动生成配置及使用 #### 配置 `useGeneratedKeys` 实现主键自动生成 当希望在插入记录之后立即获得由数据库生成的主键时,可以在映射文件中的 `<insert>` 标签内设置 `useGeneratedKeys="true"` 和 `keyProperty` 属性。前者告知 MyBatis 使用 JDBC 的 getGeneratedKeys 方法来获取主键;后者指定将返回的主键赋给实体类中哪个字段。 对于 MySQL 数据库而言,在执行完 insert 操作后,MyBatis 可以自动填充新创建的对象实例的 ID 字段[^1]: ```xml <insert id="insertPerson" parameterType="com.example.Person" useGeneratedKeys="true" keyProperty="id"> INSERT INTO person (name, age) VALUES (#{name}, #{age}) </insert> ``` 这段 XML 定义了一个名为 `insertPerson` 的 Insert 映射语句,并指定了它应该接收一个 Person 类型参数。同时启用了 `useGeneratedKeys=true` 来指示 MyBatis 获取数据库产生的主键,并将其存储到传入对象的 `id` 属性里。 #### 利用 Sequence 或者其他方式预先分配主键 如果使用的不是支持自增列特性的数据库(比如 Oracle),则可以采用定义 sequence 序列的方式来自增长主键值。此时可以通过添加 `order="BEFORE"` 参数让 MyBatis 在实际执行插入操作前先取得新的主键值并填入待插数据之中[^2]。 下面是一个例子展示如何结合 selectKey 元素以及 order 属性实现这一点: ```xml <insert id="insertStudent" parameterType="java.util.Map"> <selectKey resultType="int" keyProperty="student.studId" order="BEFORE"> SELECT seq_student.NEXTVAL FROM DUAL </selectKey> INSERT INTO STUDENT(STUDID, NAME, AGE) VALUES(#{student.studId}, #{student.name}, #{student.age}); </insert> ``` 这里展示了针对 Oracle 数据库的一个场景,其中 `seq_student` 是预设好的序列名称用于生产唯一的 studId 值。此代码片段会在真正做插入动作之前就准备好要插入的新纪录所需的唯一标识符。 #### 自定义主键生成器 除了上述两种常见情况外,还可以通过编写自己的 TypeHandler 并注册至 MyBatis 中去处理更复杂的业务逻辑下的主键生成需求。这种方式允许开发者完全掌控主键是如何被计算出来的过程,甚至可以从外部服务调用来决定最终的结果[^3]。 例如,假设有一个基于 UUID 的全局唯一识别码的需求,则可能需要如下所示的方式来构建相应的处理器: ```java public class UuidGenerator implements TypeHandler<String> { @Override public void setParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException { if(parameter == null){ ps.setNull(i,jdbcType.TYPE_CODE); }else{ ps.setString(i,UUID.randomUUID().toString()); } } // ... other methods ... } ``` 随后就可以像这样应用这个 type handler : ```xml <resultMap id="baseResultMap" type="com.example.EntityWithUuid"> <!-- fields mapping --> <id column="uuid_column_name" property="uuidField" javaType="String" jdbcType="VARCHAR"/> </resultMap> <insert id="insertEntity" parameterType="com.example.EntityWithUuid"> INSERT INTO entity_table(uuid_column_name,...) VALUES (<foreach item="item" collection="list" separator=",">#{item.uuidField}</foreach>,...) </insert> ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值