原创

Mybatis中生成全局主键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,可谓一劳永逸

原创

评论