分布式 ID 需要滿足的條件:
- 全局唯一:這是最基本的要求,必須保證 ID 是全局唯一的铣鹏。
- 高性能:低延時(shí)敷扫,不能因?yàn)橐粋€(gè)小小的 ID 生成,影響整個(gè)業(yè)務(wù)響應(yīng)速度诚卸。
- 高可用:無限接近于100%的可用性葵第。
- 好接入:遵循拿來主義原則,在系統(tǒng)設(shè)計(jì)和實(shí)現(xiàn)上要盡可能簡單合溺。
- 趨勢遞增:這個(gè)要看具體業(yè)務(wù)場景卒密,最好要趨勢遞增,一般不嚴(yán)格要求棠赛。
讓我來先捋一捋常見的分布式 ID 的解決方案有哪些哮奇?
1、數(shù)據(jù)庫自增 ID
這是最常見的方式睛约,利用數(shù)據(jù)庫的 auto_increment 自增 ID鼎俘,當(dāng)我們需要一個(gè)ID的時(shí)候,向表中插入一條記錄返回主鍵 ID辩涝。簡單贸伐,代碼也方便,但是數(shù)據(jù)庫本身就存在瓶頸怔揩,DB 單點(diǎn)無法扛住高并發(fā)場景捉邢。
針對數(shù)據(jù)庫單點(diǎn)性能問題,可以做高可用優(yōu)化商膊,設(shè)計(jì)成主從模式集群伏伐,而且要多主,設(shè)置起始數(shù)和增長步長翘狱。
-- MySQL_1 配置:
set @@auto_increment_offset = 1; -- 起始值
set @@auto_increment_increment = 2; -- 步長
-- 自增ID分別為:1秘案、3、5潦匈、7阱高、9 ......
-- MySQL_2 配置:
set @@auto_increment_offset = 2; -- 起始值
set @@auto_increment_increment = 2; -- 步長
-- 自增ID分別為:2、4茬缩、6赤惊、8、10 ....
但是隨著業(yè)務(wù)不斷增長凰锡,當(dāng)性能再次達(dá)到瓶頸的時(shí)候未舟,想要再擴(kuò)容就太麻煩了圈暗,新增實(shí)例可能還要停機(jī)操作,不利于后續(xù)擴(kuò)容裕膀。
2员串、UUID
UUID 是 Universally Unique Identifier 的縮寫,它是在一定的范圍內(nèi)(從特定的名字空間到全球)唯一的機(jī)器生成的標(biāo)識(shí)符昼扛,UUID 是16字節(jié)128位長的數(shù)字寸齐,通常以36字節(jié)的字符串表示,比如:4D2803E0-8F29-17G3-9B1C-250FE82C4309抄谐。
生成ID性能非常好渺鹦,基本不會(huì)有性能問題,代碼也簡單但是長度過長蛹含,不可讀毅厚,也無法保證趨勢遞增。
3浦箱、雪花算法
雪花算法(Snowflake)是 twitter 公司內(nèi)部分布式項(xiàng)目采用的 ID 生成算法吸耿,開源后廣受國內(nèi)大廠的好評,在該算法影響下各大公司相繼開發(fā)出各具特色的分布式生成器憎茂。
組成結(jié)構(gòu):正數(shù)位(占1 bit)+ 時(shí)間戳(占41 bit)+ 機(jī)器 ID(占10 bit)+ 自增值(占12 bit)珍语,總共64 bit 組成的一個(gè) long 類型。
- 第一個(gè) bit 位(1 bit):Java 中 long 的最高位是符號(hào)位代表正負(fù)竖幔,正數(shù)是0板乙,負(fù)數(shù)是1,一般生成 ID 都為正數(shù)拳氢,所以默認(rèn)為0
- 時(shí)間戳部分(41 bit):毫秒級(jí)的時(shí)間募逞,不建議存當(dāng)前時(shí)間戳,而是用(當(dāng)前時(shí)間戳 - 固定開始時(shí)間戳)的差值馋评,可以使產(chǎn)生的ID從更小的值開始放接;41位的時(shí)間戳可以使用69年,(1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69年
- 工作機(jī)器id(10bit):也被叫做 workId留特,這個(gè)可以靈活配置纠脾,機(jī)房或者機(jī)器號(hào)組合都可以,通常被分為 機(jī)器 ID(占5 bit)+ 數(shù)據(jù)中心(占5 bit)
- 序列號(hào)部分(12bit):自增值支持同一毫秒內(nèi)同一個(gè)節(jié)點(diǎn)可以生成4096個(gè) ID
雪花算法不依賴于數(shù)據(jù)庫蜕青,靈活方便苟蹈,且性能優(yōu)于數(shù)據(jù)庫,ID 按照時(shí)間在單機(jī)上是遞增的右核,但是由于涉及到分布式環(huán)境慧脱,每臺(tái)機(jī)器上的時(shí)鐘不可能完全同步,也許有時(shí)候也會(huì)出現(xiàn)不是全局遞增的情況贺喝。
雪花算法好像挺不錯(cuò)的樣子菱鸥,靚仔決定采用這個(gè)方案試下宗兼。
于是一套操作猛如虎,寫個(gè) demo 給領(lǐng)導(dǎo)看下氮采。
只能繼續(xù)思考方案了
4殷绍、百度(Uid-Generator)
uid-generator 是基于 Snowflake 算法實(shí)現(xiàn)的,與原始的 snowflake 算法不同在于鹊漠,它支持自定義時(shí)間戳篡帕、工作機(jī)器 ID 和 序列號(hào) 等各部分的位數(shù),而且 uid-generator 中采用用戶自定義 workId 的生成策略贸呢,在應(yīng)用啟動(dòng)時(shí)由數(shù)據(jù)庫分配。
具體不多介紹了拢军,官方地址:https://github.com/baidu/uid-generator
也就是說它依賴于數(shù)據(jù)庫楞陷,并且由于是基于 Snowflake 算法,所以也不可讀茉唉。
5固蛾、美團(tuán)(Leaf)
美團(tuán)的 Leaf 非常全面,即支持號(hào)段模式度陆,也支持 snowflake 模式艾凯。
也不多介紹了,官方地址:https://github.com/Meituan-Dianping/Leaf
號(hào)段模式是基于數(shù)據(jù)庫的懂傀,而 snowflake 模式是依賴于 Zookeeper 的
6趾诗、滴滴(TinyID)
TinyID 是基于數(shù)據(jù)庫號(hào)段算法實(shí)現(xiàn),還提供了 http 和 sdk 兩種方式接入蹬蚁。
文檔很全恃泪,官方地址:https://github.com/didi/tinyid
7、Redis 模式
其原理就是利用 redis 的 incr 命令實(shí)現(xiàn) ID 的原子性自增犀斋,眾所周知贝乎,redis 的性能是非常好的,而且本身就是單線程的叽粹,沒有線程安全問題览效。但是使用 redis 做分布式 id 解決方案,需要考慮持久化問題虫几,不然重啟 redis 過后可能會(huì)導(dǎo)致 id 重復(fù)的問題锤灿,建議采用 RDB + AOF 的持久化方式。
分析到這里持钉,我覺得 Redis 的方式非常適用于目前的場景衡招,公司系統(tǒng)原本就用到了 redis,而且也正是采用的 RDB + AOF 的持久化方式每强,這就非常好接入了始腾,只需少量編碼就能實(shí)現(xiàn)一個(gè)發(fā)號(hào)器功能州刽。
話不多說,直接開始干吧浪箭。
本案例基于 Spring Boot 2.5.3 版本
首先在 pom 中引入 redis 依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- lettuce客戶端連接需要這個(gè)依賴 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
application.yml 中配置 redis 連接
spring:
redis:
port: 6379
host: 127.0.0.1
timeout: 5000
lettuce:
pool:
# 連接池大連接數(shù)(使用負(fù)值表示沒有限制)
max-active: 8
# 連接池中的大空閑連接
max-idle: 8
# 連接池中的小空閑連接
min-idle: 0
# 連接池大阻塞等待時(shí)間(使用負(fù)值表示沒有限制)
max-wait: 1000
# 關(guān)閉超時(shí)時(shí)間
shutdown-timeout: 100
將 RedisTemplate 注入 Spring 容器中
@Configuration
public class RedisConfig{
@Bean
@ConditionalOnMissingBean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
// 使用Jackson2JsonRedisSerializer來序列化/反序列化redis的value值
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// value
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
// 使用StringRedisSerializer來序列化/反序列化redis的key值
RedisSerializer<?> redisSerializer = new StringRedisSerializer();
// key
redisTemplate.setKeySerializer(redisSerializer);
redisTemplate.setHashKeySerializer(redisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
使用 redis 依賴中的 RedisAtomicLong 類來實(shí)現(xiàn) redis 自增序列穗椅,從類名就可以看出它是原子性的。
看一下 RedisAtomicLong 的部分源碼
// RedisAtomicLong 的部分源碼
public class RedisAtomicLong extends Number implements Serializable, BoundKeyOperations<String> {
private static final long serialVersionUID = 1L;
//redis 中的 key奶栖,用 volatile 修飾匹表,獲得原子性
private volatile String key;
//當(dāng)前的 key-value 對象,根據(jù)傳入的 key 獲取 value 值
private ValueOperations<String, Long> operations;
//傳入當(dāng)前 redisTemplate 對象宣鄙,為 RedisTemplate 對象的頂級(jí)接口
private RedisOperations<String, Long> generalOps;
public RedisAtomicLong(String redisCounter, RedisConnectionFactory factory) {
this(redisCounter, (RedisConnectionFactory)factory, (Long)null);
}
private RedisAtomicLong(String redisCounter, RedisConnectionFactory factory, Long initialValue) {
Assert.hasText(redisCounter, "a valid counter name is required");
Assert.notNull(factory, "a valid factory is required");
//初始化一個(gè) RedisTemplate 對象
RedisTemplate<String, Long> redisTemplate = new RedisTemplate();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericToStringSerializer(Long.class));
redisTemplate.setExposeConnection(true);
//設(shè)置當(dāng)前的 redis 連接工廠
redisTemplate.setConnectionFactory(factory);
redisTemplate.afterPropertiesSet();
//設(shè)置傳入的 key
this.key = redisCounter;
//設(shè)置當(dāng)前的 redisTemplate
this.generalOps = redisTemplate;
//獲取當(dāng)前的 key-value 集合
this.operations = this.generalOps.opsForValue();
//設(shè)置默認(rèn)值袍镀,如果傳入為 null,則 key 獲取 operations 中的 value冻晤,如果 value 為空苇羡,設(shè)置默認(rèn)值為0
if (initialValue == null) {
if (this.operations.get(redisCounter) == null) {
this.set(0L);
}
//不為空則設(shè)置為傳入的值
} else {
this.set(initialValue);
}
}
//將傳入 key 的 value + 1并返回
public long incrementAndGet() {
return this.operations.increment(this.key, 1L);
}
}
看完源碼,我們繼續(xù)自己的編碼
使用 RedisAtomicLong 封裝一個(gè)基礎(chǔ)的 redis 自增序列工具類
// 只封裝了部分方法鼻弧,還可以擴(kuò)展
@Service
public class RedisService {
@Autowired
RedisTemplate<String, Object> redisTemplate;
/**
* 獲取鏈接工廠
*/
public RedisConnectionFactory getConnectionFactory() {
return redisTemplate.getConnectionFactory();
}
/**
* 自增數(shù)
* @param key
* @return
*/
public long increment(String key) {
RedisAtomicLong redisAtomicLong = new RedisAtomicLong(key, getConnectionFactory());
return redisAtomicLong.incrementAndGet();
}
/**
* 自增數(shù)(帶過期時(shí)間)
* @param key
* @param time
* @param timeUnit
* @return
*/
public long increment(String key, long time, TimeUnit timeUnit) {
RedisAtomicLong redisAtomicLong = new RedisAtomicLong(key, getConnectionFactory());
redisAtomicLong.expire(time, timeUnit);
return redisAtomicLong.incrementAndGet();
}
/**
* 自增數(shù)(帶過期時(shí)間)
* @param key
* @param expireAt
* @return
*/
public long increment(String key, Instant expireAt) {
RedisAtomicLong redisAtomicLong = new RedisAtomicLong(key, getConnectionFactory());
redisAtomicLong.expireAt(expireAt);
return redisAtomicLong.incrementAndGet();
}
/**
* 自增數(shù)(帶過期時(shí)間和步長)
* @param key
* @param increment
* @param time
* @param timeUnit
* @return
*/
public long increment(String key, int increment, long time, TimeUnit timeUnit) {
RedisAtomicLong redisAtomicLong = new RedisAtomicLong(key, getConnectionFactory());
redisAtomicLong.expire(time, timeUnit);
return redisAtomicLong.incrementAndGet();
}
}
根據(jù)業(yè)務(wù)需求編寫發(fā)號(hào)器方法
@Service
public class IdGeneratorService {
@Autowired
RedisService redisService;
/**
* 生成id(每日重置自增序列)
* 格式:日期 + 6位自增數(shù)
* 如:20210804000001
* @param key
* @param length
* @return
*/
public String generateId(String key, Integer length) {
long num = redisService.increment(key, getEndTime());
String id = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")) + String.format("%0" + length + "d", num);
return id;
}
/**
* 獲取當(dāng)天的結(jié)束時(shí)間
*/
public Instant getEndTime() {
LocalDateTime endTime = LocalDateTime.of(LocalDate.now(), LocalTime.MAX);
return endTime.toInstant(ZoneOffset.ofHours(8));
}
}
由于業(yè)務(wù)需求设江,需要每天都重置自增序列,所以這里以每天結(jié)束時(shí)間為過期時(shí)間攘轩,這樣第二天又會(huì)從1開始叉存。
測試一下
@SpringBootTest
class IdGeneratorServiceTest {
@Test
void generateIdTest() {
String code = idGeneratorService.generateId("orderId", 6);
System.out.println(code);
}
}
// 輸出:20210804000001
6位自增序列每天可以生成將近100w個(gè)編碼,對于大多數(shù)公司度帮,已經(jīng)足夠了歼捏。
經(jīng)過本地環(huán)境測試,開啟10個(gè)線程笨篷,1秒內(nèi)每個(gè)線程10000個(gè)請求甫菠,沒有絲毫壓力。
如果覺得有些場景下連續(xù)的編號(hào)會(huì)泄漏公司的數(shù)據(jù)冕屯,比如訂單量寂诱,那么可以設(shè)置隨機(jī)增長步長,這樣就看不出具體訂單量了安聘。但是會(huì)影響生成的編碼數(shù)量痰洒,可以根據(jù)實(shí)際情況調(diào)整自增序列的位數(shù)。
總結(jié)
沒有最好的浴韭,只有最合適的丘喻。在實(shí)際工作中往往都是這樣,需要根據(jù)實(shí)際業(yè)務(wù)需求來選擇最合適的方案念颈。
END
往期推薦
SpringBoot+Redis 實(shí)現(xiàn)消息訂閱發(fā)布
更多精彩推薦嗡靡,請關(guān)注公眾號(hào)【靚仔聊編程】