21

JPA使用雪花算法自动生成唯一主键ID

 3 years ago
source link: http://www.lzhpo.com/article/162
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

UUID作为主键

UUID自动生成主键,但是尽量还是不要使用UUID作为主键,在数据量大的时候,UUID做主键稍显慢一点。

好处就是本地生成,不要基于数据库来了;不好之处就是,UUID 太长了、占用空间大,作为主键性能太差了;更重要的是,UUID 不具有有序性,会导致 B+ 树索引在写的时候有过多的随机写操作(连续的 ID 可以产生部分顺序写),还有,由于在写的时候不能产生有顺序的 append 操作,而需要进行 insert 操作,将会读取整个 B+ 树节点到内存,在插入这条记录后会将整个节点写回磁盘,这种操作在记录占用空间比较大的情况下,性能下降明显。

总而言之,就是:

  1. innodb中的主键索引也是聚集索引,如果插入的数据是顺序的,那么b+树的叶子基本都是满的,缓存也可以很好的发挥作用。如果插入的数据是完全无序的,那么叶子节点会频繁分裂,缓存也基本无效了。这会减少tps。
  2. uuid占用的空间较大。
@Id@GeneratedValue(generator = "uuidGenerator")@GenericGenerator(name = "uuidGenerator", strategy = "uuid")@Column(nullable = false, length = 32)private String id;

如何改进?

其实MySQL官方是推荐使用自增主键作为ID性能是最好的,但是在对于分布式环境中、数据量大的情况下、分库分表的时候,使用自增主键就不太好了。

推荐还是使用雪花算法。

那么JPA中我们知道有直接使用注解UUID的,我们可以写一个使用雪花算法的。

核心SnowflakeIdWorker

生成雪花算法的核心SnowflakeIdWorker

import cn.hutool.core.date.SystemClock;/** * 雪花算法生成唯一ID * * @author Zhaopo Liu */public class SnowflakeIdWorker {    /**     * 机器id所占的位数     */    private final long workerIdBits = 5L;    /**     * 数据标识id所占的位数     */    private final long datacenterIdBits = 5L;    /**     * 工作机器ID(0~31)     */    private final long workerId;    /**     * 数据中心ID(0~31)     */    private final long datacenterId;    /**     * 毫秒内序列(0~4095)     */    private long sequence = 0L;    /**     * 上次生成ID的时间截     */    private long lastTimestamp = -1L;    /**     * 构造函数     *     * @param workerId     工作ID (0~31)     * @param datacenterId 数据中心ID (0~31)     */    public SnowflakeIdWorker(long workerId, long datacenterId) {        // 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数)        long maxWorkerId = ~(-1L << workerIdBits);        if (workerId > maxWorkerId || workerId < 0) {            throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));        }        // 支持的最大数据标识id,结果是31        long maxDatacenterId = ~(-1L << datacenterIdBits);        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 SnowflakeId     */    public synchronized long nextId() {        long timestamp = timeGen();        // 如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常        if (timestamp < lastTimestamp) {            throw new RuntimeException(                    String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));        }        // 如果是同一时间生成的,则进行毫秒内序列        // 序列在id中占的位数        long sequenceBits = 12L;        if (lastTimestamp == timestamp) {            // 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095)            long sequenceMask = ~(-1L << sequenceBits);            sequence = (sequence + 1) & sequenceMask;            // 毫秒内序列溢出            if (sequence == 0) {                // 阻塞到下一个毫秒,获得新的时间戳                timestamp = tilNextMillis(lastTimestamp);            }        }        // 时间戳改变,毫秒内序列重置        else {            sequence = 0L;        }        // 上次生成ID的时间截        lastTimestamp = timestamp;        long datacenterIdShift = sequenceBits + workerIdBits;        // 时间截向左移22位(5+5+12)        long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;        // 开始时间截 (2015-01-01)        long twepoch = 1420041600000L;        return ((timestamp - twepoch) << timestampLeftShift)                | (datacenterId << datacenterIdShift)                | (workerId << sequenceBits)                | sequence;    }    /**     * 阻塞到下一个毫秒,直到获得新的时间戳     *     * @param lastTimestamp 上次生成ID的时间截     * @return 当前时间戳     */    private long tilNextMillis(long lastTimestamp) {        long timestamp = timeGen();        while (timestamp <= lastTimestamp) {            timestamp = timeGen();        }        return timestamp;    }    /**     * 高并发环境下,返回以毫秒为单位的当前时间     *     * @return 当前时间(毫秒)     */    private long timeGen() {        return SystemClock.now();    }}

封装生成ID工具类IdGen

定义工具IdGen,可选择性使用UUID和雪花算法:

import com.withive.utils.SpringContextHolder;import java.util.UUID;/** * 唯一ID生成工具 * * @author Zhaopo Liu */public class IdGen {    /**     * 封装JDK自带的UUID, 中间无-分割.     */    public static String uuid() {        return UUID.randomUUID().toString().replaceAll("-", "");    }    /**     * 基于snowflake算法生成ID     *     * @return     */    public static long snowflakeId() {        return SpringContextHolder.getApplicationContext().getBean(SnowflakeIdWorker.class).nextId();    }}

配置类SnowflakeProperties

雪花算法配置SnowflakeProperties

import lombok.Data;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.context.annotation.Configuration;/** * ID生成配置 * * @author Zhaopo Liu */@Data@Configuration@ConfigurationProperties(prefix = "snowflake")public class SnowflakeProperties {    /**     * 工作节点ID     */    private String workerId;    /**     * 数据中心ID     */    private String datacenterId;}

初始化SnowFlake配置/Bean

初始化Bean:

import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;/** * ID生成配置 * * @author lzhpo */@Configurationpublic class SnowFlake {    @Autowired    private SnowflakeProperties snowflakeProperties;    /**     * 初始化SnowflakeIdWorker Bean     *     * @return SnowflakeIdWorker     */    @Bean    public SnowflakeIdWorker initTokenWorker() {        return new SnowflakeIdWorker(                Long.parseLong(snowflakeProperties.getWorkerId()),                Long.parseLong(snowflakeProperties.getDatacenterId()));    }}

SpringContextHolder工具

SpringContextHolder

package com.lzhpo.common.utils;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.BeansException;import org.springframework.beans.factory.DisposableBean;import org.springframework.context.ApplicationContext;import org.springframework.context.ApplicationContextAware;import org.springframework.context.annotation.Lazy;import org.springframework.core.env.Environment;import org.springframework.stereotype.Service;import java.util.ArrayList;import java.util.List;/** * SpringContextHolder * * @author Zhaopo Liu */@Service@Lazy(value = false)@Slf4jpublic class SpringContextHolder implements ApplicationContextAware, DisposableBean {    private static ApplicationContext applicationContext = null;    private static final List<CallBack> CALL_BACKS = new ArrayList<>();    private static boolean addCallback = true;    /**     * 针对 某些初始化方法,在SpringContextHolder 未初始化时 提交回调方法。 在SpringContextHolder 初始化后,进行回调使用     *     * @param callBack 回调函数     */    public static synchronized void addCallBacks(CallBack callBack) {        if (addCallback) {            SpringContextHolder.CALL_BACKS.add(callBack);        } else {            log.error("CallBack:{} 已无法添加!立即执行", callBack.getCallBackName());            callBack.executor();        }    }    /**     * 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型.     */    @SuppressWarnings("unchecked")    public static <T> T getBean(String name) {        assertContextInjected();        return (T) applicationContext.getBean(name);    }    /**     * 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型.     */    public static <T> T getBean(Class<T> requiredType) {        assertContextInjected();        return applicationContext.getBean(requiredType);    }    /**     * 获取SpringBoot 配置信息     *     * @param property     属性key     * @param defaultValue 默认值     * @param requiredType 返回类型     * @return /     */    public static <T> T getProperties(String property, T defaultValue, Class<T> requiredType) {        T result = defaultValue;        try {            result = getBean(Environment.class).getProperty(property, requiredType);        } catch (Exception ignored) {        }        return result;    }    /**     * 获取SpringBoot 配置信息     *     * @param property 属性key     * @return /     */    public static String getProperties(String property) {        return getProperties(property, null, String.class);    }    /**     * 获取SpringBoot 配置信息     *     * @param property     属性key     * @param requiredType 返回类型     * @return /     */    public static <T> T getProperties(String property, Class<T> requiredType) {        return getProperties(property, null, requiredType);    }    /**     * 检查ApplicationContext不为空.     */    private static void assertContextInjected() {        if (applicationContext == null) {            throw new IllegalStateException("applicaitonContext属性未注入, 请在applicationContext"                    + ".xml中定义SpringContextHolder或在SpringBoot启动类中注册SpringContextHolder.");        }    }    /**     * 清除SpringContextHolder中的ApplicationContext为Null.     */    private static void clearHolder() {        log.info("清除SpringContextHolder中的ApplicationContext:{}", applicationContext);        applicationContext = null;    }    @Override    public void destroy() {        SpringContextHolder.clearHolder();    }    @Override    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {        if (SpringContextHolder.applicationContext != null) {            log.error("SpringContextHolder中的ApplicationContext被覆盖, 原有ApplicationContext为:"                    + SpringContextHolder.applicationContext);        }        SpringContextHolder.applicationContext = applicationContext;        if (addCallback) {            for (CallBack callBack : SpringContextHolder.CALL_BACKS) {                callBack.executor();            }            CALL_BACKS.clear();        }        SpringContextHolder.addCallback = false;    }}

回调任务接口

/** * 针对某些初始化方法,在SpringContextHolder初始化前时,可提交一个 提交回调任务。 在SpringContextHolder 初始化后,进行回调使用。 * * @author Zhaopo Liu * @see SpringContextHolder {@link SpringContextHolder} */public interface CallBack {    /** 回调执行方法 */    void executor();    /**     * 本回调任务名称     *     * @return /     */    default String getCallBackName() {        return Thread.currentThread().getId() + ":" + this.getClass().getName();    }}

集成JPA配置GenerateSnowflakeId

重写IdentifierGenerator

import org.hibernate.HibernateException;import org.hibernate.MappingException;import org.hibernate.engine.spi.SharedSessionContractImplementor;import org.hibernate.id.Configurable;import org.hibernate.id.IdentifierGenerator;import org.hibernate.service.ServiceRegistry;import org.hibernate.type.Type;import org.springframework.stereotype.Component;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;import java.io.Serializable;import java.util.Properties;/** * JPA主键使用雪花算法自动生成 * * @author Zhaopo Liu */@Service@Component@Transactionalpublic class GenerateSnowflakeId implements IdentifierGenerator, Configurable {    @Override    public void configure(Type type, Properties params, ServiceRegistry serviceRegistry) throws MappingException {    }    @Override    public Serializable generate(SharedSessionContractImplementor session, Object object) throws HibernateException {        return IdGen.snowflakeId();    }}

com.lzhpo.snowflake.GenerateSnowflakeId就是GenerateSnowflakeId

@Id@GenericGenerator(name = "snowflakeId", strategy = "com.lzhpo.snowflake.GenerateSnowflakeId" )@GeneratedValue(generator = "snowflakeId")@ApiModelProperty(value = "ID", hidden = true)@Column(name = "id", columnDefinition="bigint(20) COMMENT '主键'")private Long id;

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK