当业务数据量很大而且增长快,在设计数据库时,我们往往不会采用数据库自增ID,这样不便于未来的数据库扩容。我们往往会自己想办法生成ID,业界用得较多的是SnowFlake算法,但该算法有一个缺点,因为ID获取方法上同步锁的存在,在高并发下会存在性能瓶颈。以下是SnowFlake算法获取ID的部分代码:
/**
* 获得下一个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));
}
//如果是同一时间生成的,则进行毫秒内序列
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
//毫秒内序列溢出
if (sequence == 0) {
//阻塞到下一个毫秒,获得新的时间戳
timestamp = tilNextMillis(lastTimestamp);
}
}
//时间戳改变,毫秒内序列重置
else {
sequence = 0L;
}
//上次生成ID的时间截
lastTimestamp = timestamp;
//移位并通过或运算拼到一起组成64位的ID
return ((timestamp - twepoch) << timestampLeftShift) //
| (datacenterId << datacenterIdShift) //
| (workerId << workerIdShift) //
| sequence;
}
以下是我实现的算法的代码:
package com.tuling.util; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicLong; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * 默认的ID生成器,生成的ID格式为:0 + 当前距2020年的年数(8位) + 自增序号(40) + 启动序号(7) + 服务器标识(8) * 启动序号是为了解决服务器重启后ID生成重复的问题,每次重启,序号都会自增 * @author lujintao * @date 2020-04-30 */ public class DefaultIdGenerator implements IdGenerator<Long>{ private static final Logger LOGGER = LoggerFactory.getLogger(DefaultIdGenerator.class); private static final ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1); private static final int DAYS_OF_YEAR = 365; private static final StartIndex START_INDEX = new StartIndex(0); private static final int BASE_YEAR = 2020; private AtomicLong counter = new AtomicLong(); //年份(以2020年为起点,距离2020年的年份)占用的位数 private int yearLength; //记录当前年份与基准年份(2020)年相隔的年数 private int yearDistance; //表示要与年份进行与计算的另一数 private long yearAnother; // 年份需要向左移动的位数 private int yearMove; //启动序号占用的位数 private int startIndexLength; //表示要与启动序号进行与计算的另一数 private long startIndexAnother; // 启动序号需要向左移动的位数 private int startIndexMove; //服务器ID private long serverId; //服务器标识占用的位数 private int serverIdLength; //计数器序列号占用的标识 private int sequenceLength; //表示要与序列号进行与计算的另一数 private long sequenceAnother; // 序列号需要向左移动的位数 private int sequenceMove; static { //初始化并更新启动序号,在原有基础上加1,以此保证重启后生成的ID不会和之前的重复 START_INDEX.initAndUpdate(); } /** * * @param yearLength 年份占用的位数 * @param startIndexLength 启动序号占用的位数 * @param serverIdLength 服务器标识占用的位数 * @param serverId 服务器标识 */ public DefaultIdGenerator(int yearLength, int startIndexLength, int serverIdLength long serverId){ this.yearLength = yearLength; this.yearDistance = getYearDistance(); this.startIndexLength = startIndexLength; this.serverIdLength = serverIdLength; this.sequenceLength = 63 - yearLength - startIndexLength - serverIdLength; this.yearAnother = getAnotherForAnd(yearLength); this.startIndexAnother = getAnotherForAnd(startIndexLength); this.sequenceAnother = getAnotherForAnd(sequenceLength); this.yearMove = 63 - yearLength; this.sequenceMove = startIndexLength + serverIdLength; this.startIndexMove = serverIdLength; this.serverId = serverId; //每到新的一年开始时,就重新计算yeatDistan executorService.scheduleAtFixedRate(() -> { this.yearDistance = getYearDistance(); },DAYS_OF_YEAR - Calendar.getInstance().get(Calendar.DAY_OF_YEAR),DAYS_OF_YEAR, TimeUnit.DAYS); } /** * 获得一个指定长度的ID * 自增序号的获取采用了从counter.getAndIncrement()的值中截取特定长度位的方式获取,是鉴于 * 以下理由:在一个整数不断自增的过程中,其尾部特定长度的数字是跟着周期性变化的,例如我们 * 观察一个整数从1000增加到3000,会发现当数字从1000增加到2000时,其尾部的数字从000变到999 * ,从2000增加到3000时,尾部数字也从000变到999。对于long型整数来说,当整数自增时,其尾部 * 特定长度的二进制位的变化也符合这个规律。无论是普通情况下的自增,还是Long.MAX_VALUE + 1
* ,或者 -1 + 1。只要我们保证序列号能表达的最大的数一定比业务系统一年需要的ID数多,那生成
* 的ID就不可能出现重复 * @return Id */ public Long getId() { long result = 0L; result = (((long)yearDistance & yearAnother) << yearMove) | ((counter.getAndIncrement() & sequenceAnother) << sequenceMove) | ((START_INDEX.getIndex() & startIndexAnother) << startIndexMove) | serverId; return result; } private int getYearDistance(){ return Calendar.getInstance().get(Calendar.YEAR) - BASE_YEAR; } /** * 当一个整数需要截取低位length个位时,为了完成这一操作,需要与之进行与操作的另一个数
* 譬如为了截取a = 01010000 10000000 00010010 00110000 11000000 11000000 * 11000000 01010001的最后7位,可以这么做,让它与b = 00000000 00000000 0000 * 0000 00000000 00000000 00000000 00000000 01111111进行与计算即可即 a & b
* @param length * @return */ private long getAnotherForAnd(int length){ return ((long)-1) >>> (64 - length); } } 该生成器在生成ID时,无需加锁,也不存在并发问题,经我本人实测,在我的电脑(windows7系统, AMD 双核3.0GHZ)上在单线程下生成100万个ID, SnowFlake算法最短耗时1674ms,而此算法最短耗时44ms。 项目源码地址: https://github.com/lujintao2000/Id_Generator
相关推荐
分布式ID生成器是现代互联网系统中的重要组成部分,它在大数据量和高并发的场景下扮演着关键角色。本文将深入探讨“通用、灵活、高性能”的分布式ID生成器的设计原理、实现方式以及它在服务器应用和分布式服务/框架...
总结,百度开源的UidGenerator是一款高效、稳定的分布式ID生成器,通过解决时钟回调问题和采用无锁的RingBuffer设计,保证了ID的唯一性和高性能。将其整合到SpringBoot和MyBatis项目中,可以方便地为系统提供全局...
在实际应用中,rs-xid可能被用作数据库主键生成器、消息队列的消息ID生成器,或者其他任何需要全局唯一标识的场景。通过其提供的API,开发者可以方便地在Java、Python、Go等语言中集成和使用rs-xid。 在`xid-master...
2. **性能优化**:高性能的 ID 生成器通常需要处理大量的并发请求。因此,算法的设计必须高效,避免锁竞争,可能使用无锁数据结构或原子操作来实现。 3. **可扩展性**:随着系统的扩展,ID 生成器应该能够容易地...
分布式ID生成器是现代互联网系统中不可或缺的一部分,尤其是在大数据量、高并发的场景下,确保全局唯一ID的生成显得尤为重要。百度开源的`uidGenerator`就是这样一个工具,它旨在为服务提供高效、稳定的分布式ID生成...
CosId,作为一个通用、灵活且高性能的分布式ID生成器,为解决这一问题提供了强大的解决方案。 首先,我们来深入了解CosId的核心特性。CosId的主要目标是提供全局唯一且具有时间序列性的ID,这对于数据的存储和查询...
这种方法可能会导致一定的重复率,特别是在高并发环境下。 3. **哈希算法**:使用哈希函数将用户名或其他唯一信息转换为固定长度的ID,以确保唯一性。这种方法可以避免ID冲突,但可能需要额外的冲突解决机制。 4. ...
- **ID生成方案**:介绍在分布式环境中保证ID全局唯一性的几种常见方案,如UUID、Snowflake算法等。 - **具体实现**:结合分库分表的场景,探讨如何在实际项目中实现全局唯一ID的生成与分配。 #### 6. RPC底层通讯...
综上所述,`PublicIdService`是一个关键的基础设施组件,它使用Java语言,结合了多种设计原则和技术手段,确保了在高并发、高可用环境下生成的公众唯一ID的质量和效率。理解并掌握这些知识点对于开发和维护这样的...
Redis具有高性能的特性,能够快速存取数据,非常适合用来存储Session数据。例如,在Spring框架中可以通过配置将Session存储到Redis中,从而实现Session的共享和持久化。 #### 七、MyBatis与对象映射 **MyBatis对象...
例如,在分布式系统中,可以利用分布式锁的乐观锁实现,如Redis的`watch`命令,或者通过分布式ID生成器(如Snowflake)来实现版本号的唯一性和全局性。 总之,Java乐观锁是并发控制的一种高效策略,它通过减少锁的...
7. **Netty的高性能表现**:采用异步非阻塞I/O模型,高效的数据缓冲和处理机制,以及灵活的编解码器支持等,使其成为高性能网络应用程序的理想选择。 #### 六、分布式系统 1. **Dubbo的基本原理**:Dubbo是一个高...
10. **性能优化**:Kotlin的编译器能够生成高效的字节码,配合精心设计的数据结构和算法,logPotato能够在不影响性能的情况下提供强大的日志服务。 综上所述,logPotato作为一个Kotlin实现的日志处理框架,结合了...
70. **SpringMVC与Struts的区别**:SpringMVC是一个更轻量级的Web框架,与Spring框架的整合度更高,提供更好的性能。Struts是一个基于MVC模式的重量级框架。 71. **避免SQL注入的方法**:使用预处理语句...