使用了ZooKeeper的臨時節(jié)點來協(xié)調(diào)各個工作節(jié)點,如果一個工作節(jié)點掛掉了,它的臨時節(jié)點也會被刪除,這樣可以保證每個工作節(jié)點獲得的ID是唯一的。
在分布式系統(tǒng)中,生成全局唯一ID是非常重要的,因為在分布式系統(tǒng)中,多個節(jié)點同時生成ID可能會導(dǎo)致ID沖突。下面介紹幾種常用的分布式ID解決方案。
UUID
UUID(通用唯一標(biāo)識符)是由128位數(shù)字組成的標(biāo)識符,它可以保證在全球范圍內(nèi)的唯一性,因為其生成算法基于時間戳、節(jié)點ID等因素。UUID可以使用Java自帶的UUID類來生成,如下所示:
javaCopy code
import java.util.UUID;
public class UuidGenerator {
public static void main(String[] args) {
UUID uuid = UUID.randomUUID();
System.out.println(uuid.toString());
}
}
UUID的優(yōu)點是簡單易用,無需額外的配置和管理,可以直接使用Java自帶的UUID類生成。但是UUID長度較長(128位),不太適合作為數(shù)據(jù)庫表的主鍵,且不易于排序和索引。
Snowflake
Snowflake是Twitter開源的一種分布式ID生成算法,它可以生成64位的唯一ID,其中包含了時間戳、數(shù)據(jù)中心ID和機器ID等信息。Snowflake算法的Java代碼如下所示:
Snowflake算法的Java代碼:
javaCopy code
public class SnowflakeGenerator {
private final static long START_STMP = 1480166465631L;
private final static long SEQUENCE_BIT = 12;
private final static long MACHINE_BIT = 5;
private final static long DATACENTER_BIT = 5;
private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT);
private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);
private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);
private final static long MACHINE_LEFT = SEQUENCE_BIT;
private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
private final static long TIMESTMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT;
private long datacenterId;
private long machineId;
private long sequence = 0L;
private long lastStmp = -1L;
public SnowflakeGenerator(long datacenterId, long machineId) {
if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) {
throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0");
}
if (machineId > MAX_MACHINE_NUM || machineId < 0) {
throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0");
}
this.datacenterId = datacenterId;
this.machineId = machineId;
}
public synchronized long nextId() {
long currStmp = getNewstmp();
if (currStmp < lastStmp) {
throw new RuntimeException("Clock moved backwards. Refusing to generate id");
}
if (currStmp == lastStmp) {
sequence = (sequence + 1) & MAX_SEQUENCE;
if (sequence == 0L) {
currStmp = getNextMill();
}
} else {
sequence = 0L;
}
lastStmp = currStmp;
return (currStmp - START_STMP) << TIMESTMP_LEFT
| datacenterId << DATACENTER_LEFT
| machineId << MACHINE_LEFT
| sequence;
}
private long getNextMill() {
long mill = getNewstmp();
while (mill <= lastStmp) {
mill = getNewstmp();
}
return mill;
}
private long getNewstmp() {
return System.currentTimeMillis();
}
}
Snowflake算法的優(yōu)點是生成ID的性能高,且ID長度較短(64位),可以作為數(shù)據(jù)庫表的主鍵,且便于排序和索引。但是需要注意,如果集群中的節(jié)點數(shù)超過了機器ID所占的位數(shù),或者集群規(guī)模很大,時間戳位數(shù)不夠用,那么就需要考慮其他的分布式ID生成算法。
Leaf
Leaf是美團(tuán)點評開源的一種分布式ID生成算法,它可以生成全局唯一的64位ID。Leaf算法的Java代碼如下所示:
Leaf算法的Java代碼:
javaCopy code
public class LeafGenerator {
private static final Logger logger = LoggerFactory.getLogger(LeafGenerator.class);
private static final String WORKER_ID_KEY = "leaf.worker.id";
private static final String PORT_KEY = "leaf.port";
private static final int DEFAULT_PORT = 8080;
private static final int DEFAULT_WORKER_ID = 0;
private static final int WORKER_ID_BITS = 10;
private static final int SEQUENCE_BITS = 12;
private static final int MAX_WORKER_ID = (1 << WORKER_ID_BITS) - 1;
private static final int MAX_SEQUENCE = (1 << SEQUENCE_BITS) - 1;
private static final long EPOCH = 1514736000000L;
private final SnowflakeIdWorker idWorker;
public LeafGenerator() {
int workerId = SystemPropertyUtil.getInt(WORKER_ID_KEY, DEFAULT_WORKER_ID);
int port = SystemPropertyUtil.getInt(PORT_KEY, DEFAULT_PORT);
this.idWorker = new SnowflakeIdWorker(workerId, port);
logger.info("Initialized LeafGenerator with workerId={}, port={}", workerId, port);
}
public long nextId() {
return idWorker.nextId();
}
private static class SnowflakeIdWorker {
private final long workerId;
private final long port;
private long sequence = 0L;
private long lastTimestamp = -1L;
SnowflakeIdWorker(long workerId, long port) {
if (workerId < 0 || workerId > MAX_WORKER_ID) {
throw new IllegalArgumentException(String.format("workerId must be between %d and %d", 0, MAX_WORKER_ID));
}
this.workerId = workerId;
this.port = port;
}
synchronized long nextId() {
long timestamp = System.currentTimeMillis();
if (timestamp < lastTimestamp) {
throw new RuntimeException("Clock moved backwards. Refusing to generate id");
}
if (timestamp == lastTimestamp) {
sequence = (sequence + 1) & MAX_SEQUENCE;
if (sequence == 0L) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
return ((timestamp - EPOCH) << (WORKER_ID_BITS + SEQUENCE_BITS))
| (workerId << SEQUENCE_BITS)
| sequence;
}
private long tilNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
}
}
Leaf算法的特點是生成ID的速度比Snowflake算法略慢,但是可以支持更多的Worker節(jié)點。Leaf算法生成的ID由三部分組成,分別是時間戳、Worker ID和序列號,其中時間戳占用42位、Worker ID占用10位、序列號占用12位,總共64位。
以上是常見的分布式ID生成算法,當(dāng)然還有其他的一些方案,如:MongoDB ID、UUID、Twitter Snowflake等。不同的方案適用于不同的業(yè)務(wù)場景,具體實現(xiàn)細(xì)節(jié)和性能表現(xiàn)也有所不同,需要根據(jù)實際情況選擇合適的方案。
除了上述介紹的分布式ID生成算法,還有一些新的分布式ID生成方案不斷涌現(xiàn),例如Flicker的分布式ID生成算法,它使用了類似于Snowflake的思想,但是采用了不同的位數(shù)分配方式,相比Snowflake更加靈活,并且可以根據(jù)需要動態(tài)調(diào)整每個部分占用的位數(shù)。此外,F(xiàn)acebook還推出了ID Generation Service (IGS)方案,該方案將ID的生成和存儲分離,提供了更加靈活和可擴展的方案,但是需要進(jìn)行更加復(fù)雜的架構(gòu)設(shè)計和實現(xiàn)。
針對不同的業(yè)務(wù)需求,可以設(shè)計多套分布式ID生成方案。下面是我個人的一些建議:
- 基于數(shù)據(jù)庫自增ID生成:使用數(shù)據(jù)庫自增ID作為全局唯一ID,可以很好的保證ID的唯一性,并且實現(xiàn)簡單,但是并發(fā)量較高時可能會導(dǎo)致性能瓶頸。因此,在高并發(fā)場景下不建議使用。
- 基于UUID生成:使用UUID作為全局唯一ID,可以很好地保證ID的唯一性,但是ID長度較長(128位),不便于存儲和傳輸,并且存在重復(fù)ID的概率非常小但不為0。因此,建議在分布式系統(tǒng)中使用時要考慮ID的長度和存儲傳輸?shù)某杀尽?/li>
- 基于Redis生成:使用Redis的原子性操作,可以保證ID的唯一性,并且生成ID的速度非???,可以適用于高并發(fā)場景。但是需要注意,如果Redis宕機或者性能不足,可能會影響ID的生成效率和可用性。
- 基于ZooKeeper生成:使用ZooKeeper的序列號生成器,可以保證ID的唯一性,并且實現(xiàn)較為簡單,但是需要引入額外的依賴和資源,并且可能會存在性能瓶頸。
選擇適合自己業(yè)務(wù)場景的分布式ID生成方案,需要綜合考慮ID的唯一性、生成速度、長度、存儲成本、可擴展性、可用性等多個因素。同時需要注意,不同方案的實現(xiàn)細(xì)節(jié)和性能表現(xiàn)也有所不同,需要根據(jù)實際情況進(jìn)行權(quán)衡和選擇。
下面給出每種方案的詳細(xì)代碼demo:
基于數(shù)據(jù)庫自增ID生成
javaCopy code
public class IdGenerator {
private static final String JDBC_URL = "jdbc:mysql://localhost:3306/test";
private static final String JDBC_USER = "root";
private static final String JDBC_PASSWORD = "password";
public long generateId() {
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
Class.forName("com.mysql.jdbc.Driver");
conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);
pstmt = conn.prepareStatement("INSERT INTO id_generator (stub) VALUES (null)", Statement.RETURN_GENERATED_KEYS);
pstmt.executeUpdate();
rs = pstmt.getGeneratedKeys();
if (rs.next()) {
return rs.getLong(1);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (rs != null) {
rs.close();
}
if (pstmt != null) {
pstmt.close();
}
if (conn != null) {
conn.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
return 0L;
}
}
基于UUID生成
javaCopy code
import java.util.UUID;
public class IdGenerator {
public String generateId() {
return UUID.randomUUID().toString().replace("-", "");
}
}
基于Redis生成
javaCopy code
import redis.clients.jedis.Jedis;
public class IdGenerator {
private static final String REDIS_HOST = "localhost";
private static final int REDIS_PORT = 6379;
private static final String REDIS_PASSWORD = "password";
private static final int ID_GENERATOR_EXPIRE_SECONDS = 3600;
private static final String ID_GENERATOR_KEY = "id_generator";
public long generateId() {
Jedis jedis = null;
try {
jedis = new Jedis(REDIS_HOST, REDIS_PORT);
jedis.auth(REDIS_PASSWORD);
long id = jedis.incr(ID_GENERATOR_KEY);
jedis.expire(ID_GENERATOR_KEY, ID_GENERATOR_EXPIRE_SECONDS);
return id;
} catch (Exception e) {
e.printStackTrace();
} finally {
if (jedis != null) {
jedis.close();
}
}
return 0L;
}
}
基于ZooKeeper生成
javaCopy code
import java.util.concurrent.CountDownLatch;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooDefs.Ids;
import org.apache.zookeeper.ZooKeeper;
public class IdGenerator implements Watcher {
private static final String ZK_HOST = "localhost";
private static final int ZK_PORT = 2181;
private static final int SESSION_TIMEOUT = 5000;
private static final String ID_GENERATOR_NODE = "/id_generator";
private static final int ID_GENERATOR_EXPIRE_SECONDS = 3600;
private long workerId = 0;
public IdGenerator() {
try {
ZooKeeper zk = new ZooKeeper(ZK_HOST + ":" + ZK_PORT, SESSION_TIMEOUT, this);
CountDownLatch latch = new CountDownLatch(1);
latch.await();
if (zk.exists(ID_GENERATOR_NODE, false) == null) {
zk.create(ID_GENERATOR_NODE, null, Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
workerId = zk.getChildren(ID_GENERATOR_NODE, false).size();
zk.create(ID_GENERATOR_NODE + "/worker_" + workerId, null, Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
} catch (Exception e) {
e.printStackTrace();
}
}
public long generateId() {
ZooKeeper zk = null;
try {
zk = new ZooKeeper(ZK_HOST + ":" + ZK_PORT, SESSION_TIMEOUT, null);
CountDownLatch latch = new CountDownLatch(1);
latch.await();
zk.create(ID_GENERATOR_NODE + "/id_", null, Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL, (rc, path, ctx, name) -> {}, null);
byte[] data = zk.getData(ID_GENERATOR_NODE + "/worker_" + workerId, false, null);
long id = Long.parseLong(new String(data)) * 10000 + zk.getChildren(ID_GENERATOR_NODE, false).size();
return id;
} catch (Exception e) {
e.printStackTrace();
} finally {
if (zk != null) {
try {
zk.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
return 0L;
}
@Override
public void process(WatchedEvent event) {
if (event.getState() == Event.KeeperState.SyncConnected) {
System.out.println("Connected to ZooKeeper");
CountDownLatch latch = new CountDownLatch(1);
latch.countDown();
}
}
}
注意,這里使用了ZooKeeper的臨時節(jié)點來協(xié)調(diào)各個工作節(jié)點,如果一個工作節(jié)點掛掉了,它的臨時節(jié)點也會被刪除,這樣可以保證每個工作節(jié)點獲得的ID是唯一的。
以上就是各種分布式ID生成方案的詳細(xì)代碼demo,實際上,每種方案都有其優(yōu)缺點,應(yīng)根據(jù)具體業(yè)務(wù)場景和系統(tǒng)架構(gòu)選擇合適的方案。