在服務器系統開發時,為了適應數據大并發的請求,我們往往需要對數據進行異步存儲,特別是在做分布式系統時,這個時候就不能等待插入數據庫返回了取自動id了,而是需要在插入數據庫之前生成一個全局的唯一id,使用全局的唯一id,在游戲服務器中,全局唯一的id可以用于將來合服方便,不會出現鍵沖突。也可以將來在業務增長的情況下,實現分庫分表,比如某一個用戶的物品要放在同一個分片內,而這個分片段可能是根據用戶id的范圍值來確定的,比如用戶id大于1000小于100000的用戶在一個分片內。目前常用的有以下幾種:
1,Java 自帶的UUID.
UUID.randomUUID().toString(),可以通過服務程序本地產生,ID的生成不依賴數據庫的實現。
優勢:
本地生成ID,不需要進行遠程調用。
全局唯一不重復。
水平擴展能力非常好。
劣勢:
ID有128 bits,占用的空間較大,需要存成字符串類型,索引效率極低。
生成的ID中沒有帶Timestamp,無法保證趨勢遞增,數據庫分庫分表時不好依賴
2,基于Redis的incr方法
Redis本身是單線程操作的,而incr更保證了一種原子遞增的操作。而且支持設置遞增步長。
優勢:
部署方便,使用簡單,只需要調用一個redis的api即可。
可以多個服務器共享一個redis服務,減少共享數據的開發時間。
Redis可以群集部署,解決單點故障的問題。
劣勢:
如果系統太龐大的話,n多個服務同時向redis請求,會造成性能瓶頸。
3,來自Flicker的解決方案
這個解決方法是基于數據庫自增id的,它使用一個單獨的數據庫專門用于生成id。詳細的大家可以網上找找,個人覺得使用挺麻煩的,不建議使用。
4,Twitter Snowflake
snowflake是twitter開源的分布式ID生成算法,其核心思想是:產生一個long型的ID,使用其中41bit作為毫秒數,10bit作為機器編號,12bit作為毫秒內序列號。這個算法單機每秒內理論上最多可以生成1000*(2^12)個,也就是大約400W的ID,完全能滿足業務的需求。
根據snowflake算法的思想,我們可以根據自己的業務場景,產生自己的全局唯一ID。因為Java中long類型的長度是64bits,所以我們設計的ID需要控制在64bits。
優點:高性能,低延遲;獨立的應用;按時間有序。
缺點:需要獨立的開發和部署。
比如我們設計的ID包含以下信息:
| 41 bits: Timestamp | 3 bits: 區域 | 10 bits: 機器編號 | 10 bits: 序列號 |
產生唯一ID的Java代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
|
/** * 自定義 ID 生成器 * ID 生成規則: ID長達 64 bits * * | 41 bits: Timestamp (毫秒) | 3 bits: 區域(機房) | 10 bits: 機器編號 | 10 bits: 序列號 | */ public class GameUUID{ // 基準時間 private long twepoch = 1288834974657L; //Thu, 04 Nov 2010 01:42:54 GMT // 區域標志位數 private final static long regionIdBits = 3L; // 機器標識位數 private final static long workerIdBits = 10L; // 序列號識位數 private final static long sequenceBits = 10L; // 區域標志ID最大值 private final static long maxRegionId = -1L ^ (-1L << regionIdBits); // 機器ID最大值 private final static long maxWorkerId = -1L ^ (-1L << workerIdBits); // 序列號ID最大值 private final static long sequenceMask = -1L ^ (-1L << sequenceBits); // 機器ID偏左移10位 private final static long workerIdShift = sequenceBits; // 業務ID偏左移20位 private final static long regionIdShift = sequenceBits + workerIdBits; // 時間毫秒左移23位 private final static long timestampLeftShift = sequenceBits + workerIdBits + regionIdBits; private static long lastTimestamp = -1L; private long sequence = 0L; private final long workerId; private final long regionId; public GameUUID( long workerId, long regionId) { // 如果超出范圍就拋出異常 if (workerId > maxWorkerId || workerId < 0 ) { throw new IllegalArgumentException( "worker Id can't be greater than %d or less than 0" ); } if (regionId > maxRegionId || regionId < 0 ) { throw new IllegalArgumentException( "datacenter Id can't be greater than %d or less than 0" ); } this .workerId = workerId; this .regionId = regionId; } public GameUUID( long workerId) { // 如果超出范圍就拋出異常 if (workerId > maxWorkerId || workerId < 0 ) { throw new IllegalArgumentException( "worker Id can't be greater than %d or less than 0" ); } this .workerId = workerId; this .regionId = 0 ; } public long generate() { return this .nextId( false , 0 ); } /** * 實際產生代碼的 * * @param isPadding * @param busId * @return */ private synchronized long nextId( boolean isPadding, long busId) { long timestamp = timeGen(); long paddingnum = regionId; if (isPadding) { paddingnum = busId; } if (timestamp < lastTimestamp) { try { throw new Exception( "Clock moved backwards. Refusing to generate id for " + (lastTimestamp - timestamp) + " milliseconds" ); } catch (Exception e) { e.printStackTrace(); } } //如果上次生成時間和當前時間相同,在同一毫秒內 if (lastTimestamp == timestamp) { //sequence自增,因為sequence只有10bit,所以和sequenceMask相與一下,去掉高位 sequence = (sequence + 1 ) & sequenceMask; //判斷是否溢出,也就是每毫秒內超過1024,當為1024時,與sequenceMask相與,sequence就等于0 if (sequence == 0 ) { //自旋等待到下一毫秒 timestamp = tailNextMillis(lastTimestamp); } } else { // 如果和上次生成時間不同,重置sequence,就是下一毫秒開始,sequence計數重新從0開始累加, // 為了保證尾數隨機性更大一些,最后一位設置一個隨機數 sequence = new SecureRandom().nextInt( 10 ); } lastTimestamp = timestamp; return ((timestamp - twepoch) << timestampLeftShift) | (paddingnum << regionIdShift) | (workerId << workerIdShift) | sequence; } // 防止產生的時間比之前的時間還要?。ㄓ捎贜TP回撥等問題),保持增量的趨勢. private long tailNextMillis( final long lastTimestamp) { long timestamp = this .timeGen(); while (timestamp <= lastTimestamp) { timestamp = this .timeGen(); } return timestamp; } // 獲取當前的時間戳 protected long timeGen() { return System.currentTimeMillis(); } } |
使用自定義的這種方法需要注意的幾點:
為了保持增長的趨勢,要避免有些服務器的時間早,有些服務器的時間晚,需要控制好所有服務器的時間,而且要避免NTP時間服務器回撥服務器的時間;在跨毫秒時,序列號總是歸0,會使得序列號為0的ID比較多,導致生成的ID取模后不均勻,所以序列號不是每次都歸0,而是歸一個0到9的隨機數。
上面說的這幾種方式我們可以根據自己的需要去選擇。在游戲服務器開發中,根據自己的游戲類型選擇,比如手機游戲,可以使用簡單的redis方式,簡單不容易出錯,由于這種游戲單服并發新建id量并不太大,完全可以滿足需要。而對于大型的世界游戲服務器,它本身就是以分布式為主的,所以可以使用snowflake的方式,上面的snowflake代碼只是一個例子,需要自己根據自己的需求去定制,所以有額外的開發量,而且要注意上述所說的注意事項。
以上所述是小編給大家介紹的基于Java代碼實現游戲服務器生成全局唯一ID的方法匯總,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復大家的。在此也非常感謝大家對服務器之家網站的支持!
原文鏈接:http://www.cnblogs.com/wgslucky/archive/2016/10/27/6006157.html