簡單實用!利用Redis輕松實現(xiàn)高并發(fā)全局ID生成器
我相信你會經(jīng)常遇到要生成唯一 ID 的場景,比如標(biāo)識每次請求、生成一個訂單編號、創(chuàng)建用戶需要創(chuàng)建一個用戶 ID。
謝霸戈:這還不簡單,用 UUID 不就行了。
UUID 確實是個好東西,生成的 ID 全球唯一,但是有兩個致命缺陷。
- 不是遞增的。MySQL 中索引的數(shù)據(jù)結(jié)構(gòu)是 B+Tree,這種數(shù)據(jù)結(jié)構(gòu)的特點是索引樹上的節(jié)點的數(shù)據(jù)是有序的,而如果使用 UUID 作為主鍵,那么每次插入數(shù)據(jù)時,因為無法保證每次產(chǎn)生的 UUID 有序,所以就會出現(xiàn)新的 UUID 需要插入到索引樹的中間去,這樣可能會頻繁地導(dǎo)致頁分裂,使性能下降。
- 太占用內(nèi)存。每個 UUID 由 36 個字符組成,在字符串進(jìn)行比較時,需要從前往后比較,字符串越長,性能越差。另外字符串越長,占用的內(nèi)存越大,由于頁的大小是固定的,這樣一個頁上能存放的關(guān)鍵字?jǐn)?shù)量就會越少,這樣最終就會導(dǎo)致索引樹的高度越大,在索引搜索的時候,發(fā)生的磁盤 IO 次數(shù)越多,性能越差。
謝霸戈:那咋辦呢?
別急,今天我就給大家?guī)硪粋€神器級的解決方案——Redis 分布式 ID 生成器!配合 SpringBoot3.0,讓你的 ID 生成變得既簡單又高效。
分布式 ID 要滿足什么要求
在進(jìn)入正文前,先介紹下分布式 ID 應(yīng)該滿足哪些特性。
分布式 ID 生成器需要滿足以下特性。
- 有序性之單調(diào)遞增,想要分而治之、二分法查找就必須實現(xiàn)。另外,MySQL 是你們用的最多的數(shù)據(jù)庫,B+ 樹為了維護(hù) ID 的有序性,就會頻繁的在索引的中間位置插入而挪動后面節(jié)點的位置,甚至導(dǎo)致頻繁的頁分裂,這對于性能的影響是極大的。
- 全局唯一性,ID 不唯一就會出現(xiàn)主鍵沖突。
- 高性能,生成 ID 是高頻操作,如果性能緩慢,系統(tǒng)的整體性能都會受到限制。
- 高可用,也就是在給定的時間間隔內(nèi),一個系統(tǒng)總的可用時間占的比例。
- 存儲空間小,用 MySQL 的 InnoDB B+樹來說,普通索引(非聚集索引)會存儲主鍵值,主鍵越大,每個 Page 頁可以存儲的數(shù)據(jù)就越少,訪問磁盤 I/O 的次數(shù)就會增加。
Redis String 實現(xiàn)分布式 ID
Redis 集群能保證高可用和高性能,為了節(jié)省內(nèi)存,ID 可以使用數(shù)字的形式,并且通過遞增的方式來創(chuàng)建新的 ID。
防止重啟數(shù)據(jù)丟失,你還需要把 Redis AOF 持久化開啟。
MySQL:“開啟 AOF 持久,為了性能設(shè)置成 everysec 策略還是有可能丟失一秒的數(shù)據(jù),所以你還可以使用一個異步機(jī)制將生成的最大 ID 持久化到一個 MySQL。”
好主意,在生成 ID 之后發(fā)送一條消息到 MQ 消息隊列中,把值持久化到 MySQL 中。
我們可以使用 Redis String 數(shù)據(jù)類型來實現(xiàn),key 用于區(qū)分不同業(yè)務(wù)場景的 ID 生成器,value 存儲 ID。
String 數(shù)據(jù)類型提供了 INCR 指令,它能把 key 中存儲的數(shù)字加 1 并返回客戶端。如果 key 不存在,那么 key 的 value 先被初始化成 0,再執(zhí)行加 1 操作并返回給客戶端。
Redis,作為一個高性能的內(nèi)存數(shù)據(jù)庫,天生就適合處理高并發(fā)的場景。它的“單線程”模型更是讓它在處理 ID 生成時如魚得水。
Redis 的操作是原子性的,這就意味著在整個過程中,不會有任何的并發(fā)問題出現(xiàn),從而確保了 ID 的唯一性。
設(shè)計思路
設(shè)計思路如下圖所示。
圖 2-4
- 假設(shè)訂單 ID 生成器的 key 是“counter:order”,當(dāng)應(yīng)用服務(wù)啟動的時候先從數(shù)據(jù)庫中查詢出最大值 M。執(zhí)行 EXISTS counter:order 判斷是否存在 key。
- Redis 中不存在 key “counter:order”,執(zhí)行 SET counter:order M 將 M 值作寫入 Redis。
- Redis 中存在 key “counter:order”,值為 K,那么就比較 M 和 K 的值,執(zhí)行 SET counter:order max(M, N)將最大值寫入 Redis,相等的話就不操作。
- 應(yīng)用服務(wù)啟動完成后,每次需要生成 ID 的時候,應(yīng)用程序就向 Redis 服務(wù)器發(fā)送 INCR counter:order指令。
- 應(yīng)用程序?qū)@取到的 ID 值發(fā)送到 MQ 消息隊列,消費者監(jiān)聽隊列把值更新到 MySQL。
SpringBoot 代碼實現(xiàn)
接下來,我們結(jié)合 SpringBoot3.0 來打造一個強大且易用的 Redis 分布式 ID 生成器。
首先,我們需要在 SpringBoot 項目中引入 redis 的依賴。在pom.xml文件中添上這行代碼:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
搞定依賴后,我們得告訴 SpringBoot 怎么連接到 Redis。打開application.yml文件,填上 Redis 的服務(wù)地址和端口:
spring:
application:
name: redis
redis:
host: 127.0.0.1
port: 6379
password: magebyte
timeout: 6000
萬事俱備,只欠東風(fēng)!接下來,我們編寫一個 ID 生成器工具類。這個工具類負(fù)責(zé)與 Redis 交互,生成唯一的 ID。這里我們使用 Redis 的INCR命令,它能讓 ID 自增,確保每次獲取的 ID 都是唯一的。
@Component
public class OrderIdGenerator implements InitializingBean {
private final StringRedisTemplate redisTemplate;
/**
* 操作數(shù)據(jù)庫 dao
*/
private final IdGeneratorMapper idGeneratorMapper;
private static final String KEY = "counter:order";
/**
* 數(shù)據(jù)庫中的 ID 值
*/
private String dbId;
@Autowired
public OrderIdGenerator(StringRedisTemplate redisTemplate, IdGeneratorMapper idGeneratorMapper) {
this.redisTemplate = redisTemplate;
this.idGeneratorMapper = idGeneratorMapper;
}
public Long generateId(String key) {
return redisTemplate.opsForValue().increment(key, 1);
}
@Override
public void afterPropertiesSet() throws Exception {
// 從數(shù)據(jù)庫查詢最大 ID
this.dbId = idGeneratorMapper.getMaxID(KEY);
Boolean hasKey = redisTemplate.hasKey(KEY);
if (Boolean.TRUE.equals(hasKey)) {
// key 存在,比較 dbId 與 redisValue,取出最大值
String redisValue = redisTemplate.opsForValue().get(KEY);
String targetValue = max(this.dbId, redisValue);
} else {
自定義 ID 規(guī)則
不過呢,光有唯一的 ID 還不夠,我們還得讓它更符合業(yè)務(wù)的實際需求。比如訂單編號吧,我們可能希望它的格式是ORD-20240528-0001,其中ORD是業(yè)務(wù)標(biāo)識,20240528是日期,0001是當(dāng)天的序號。
public String generateCustomId(String key, String prefix, String datePattern) {
long sequence = redisTemplate.opsForValue().increment(key, 1);
return String.format("%s-%s-%04d", prefix, new SimpleDateFormat(datePattern).format(new Date()), sequence);
}
那具體怎么用呢?讓我們在業(yè)務(wù)代碼中一探究竟!想象一下,在一個電商系統(tǒng)中,當(dāng)一個新的訂單如流星般劃過天際,我們迫不及待地想要一個獨一無二的 ID 來標(biāo)記它時——很簡單,只需調(diào)用我們的generateCustomId
方法,傳入訂單相關(guān)的參數(shù)即可。
@Service
public class OrderService {
private final RedisIdGenerator idGenerator;
@Autowired
public OrderService(RedisIdGenerator idGenerator) {
this.idGenerator = idativeIdGenerator;
}
public Order createOrder(OrderRequest request) {
String orderId = idGenerator.generateCustomId("order:id", "ORD", "yyyyMMdd");
Order order = new Order();
order.setId(orderId);
// 其他業(yè)務(wù)邏輯...
return order;
}
}