前言
分佈式ID生成策略基本要求就是全局不重複,最好還能遞增,長度較短,性能高,可用性強。關於相關的實現方案有很多,本文着重使用美團開源的分佈式ID生成解決方案——Leaf。
關於Leaf,美團官方的介紹文檔主要如下,強烈建議閱讀文章大致瞭解Leaf的工作流程與原理,這對本文後續的源碼解析有很大的幫助。
本系列Leaf源碼解析部分按照使用的方式也分爲號段模式和snowflake模式兩篇文章,本文就來着重研究號段模式的源碼實現。
本文的Leaf源碼註釋地址:https://github.com/MrSorrow/Leaf
I. 導入項目
Leaf由Maven構建,源碼地址:https://github.com/Meituan-Dianping/Leaf
首先先Fork官方倉庫到自己的倉庫,我的源碼註釋版:https://github.com/MrSorrow/Leaf
下載源碼,導入IDEA,導入成功依賴下載完成後項目結構大致如下:
II. 測試號段模式
「創建數據庫表」
DROP TABLE IF EXISTS `leaf_alloc`;
CREATE TABLE `leaf_alloc` (
`biz_tag` varchar(128) NOT NULL DEFAULT '' COMMENT '業務key',
`max_id` bigint(20) NOT NULL DEFAULT '1' COMMENT '當前已經分配了的最大id',
`step` int(11) NOT NULL COMMENT '初始步長,也是動態調整的最小步長',
`description` varchar(256) DEFAULT NULL COMMENT '業務key的描述',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '數據庫維護的更新時間',
PRIMARY KEY (`biz_tag`)
) ENGINE=InnoDB;
「開啓號段模式」
leaf.name=com.sankuai.leaf.opensource.test
leaf.segment.enable=true
leaf.jdbc.url=jdbc:mysql://localhost:3306/leaf_test?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8
leaf.jdbc.username=root
leaf.jdbc.password=1234
leaf.snowflake.enable=false
#leaf.snowflake.zk.address=
#leaf.snowflake.port=
「測試號段模式」
開啓號段模式並配置好數據庫連接後,點擊啓動 leaf-server
模塊的 LeafServerApplication
,將服務跑起來。
瀏覽器輸入http://localhost:8080/api/segment/get/leaf-segment-test來獲取分佈式遞增id;
監控號段模式:http://localhost:8080/cache
數據庫表:
III. 號段模式源碼解析
正式進入源碼前,再強烈建議閱讀官方的兩篇博客,對Leaf的號段模式工作模式有個大致的理解。
我們從http://localhost:8080/api/segment/get/leaf-segment-test入口來分析。該請求會交由 com.sankuai.inf.leaf.server.LeafController
處理:
@Autowired
SegmentService segmentService;
/**
* 號段模式獲取id
* @param key 對應數據庫表的biz_tag
* @return
*/
@RequestMapping(value = "/api/segment/get/{key}")
public String getSegmentID(@PathVariable("key") String key) {
// 核心是segmentService的getId方法
return get(key, segmentService.getId(key));
}
private String get(@PathVariable("key") String key, Result id) {
Result result;
if (key == null || key.isEmpty()) {
throw new NoKeyException();
}
result = id;
if (result.getStatus().equals(Status.EXCEPTION)) {
throw new LeafServerException(result.toString());
}
return String.valueOf(result.getId());
}
可以看到主要是調用 SegmentService
的 getId(key)
方法。key
參數其實就是路徑上對應的 leaf-segment-test
,也就是數據庫對應的 biz_tag
。 getId(key)
方法返回的是 com.sankuai.inf.leaf.common.Result
對象,封裝了 id
和 狀態 status
:
public class Result {
private long id;
private Status status;
// getter and setter....
}
public enum Status {
SUCCESS,
EXCEPTION
}
創建SegmentService
我們進入 SegmentService
類中,再調用 getId(key)
方法之前,我們先看一下 SegmentService
類的實例化構造函數邏輯。可以看到:
package com.sankuai.inf.leaf.server;
@Service("SegmentService")
public class SegmentService {
private Logger logger = LoggerFactory.getLogger(SegmentService.class);
IDGen idGen;
DruidDataSource dataSource;
/**
* 構造函數,注入單例SegmentService時,完成以下幾件事:
* 1. 加載leaf.properties配置文件解析配置
* 2. 創建Druid dataSource
* 3. 創建IDAllocDao
* 4. 創建ID生成器實例SegmentIDGenImpl並初始化
* @throws SQLException
* @throws InitException
*/
public SegmentService() throws SQLException, InitException {
// 1. 加載leaf.properties配置文件
Properties properties = PropertyFactory.getProperties();
// 是否開啓號段模式
boolean flag = Boolean.parseBoolean(properties.getProperty(Constants.LEAF_SEGMENT_ENABLE, "true"));
if (flag) {
// 2. 創建Druid dataSource
dataSource = new DruidDataSource();
dataSource.setUrl(properties.getProperty(Constants.LEAF_JDBC_URL));
dataSource.setUsername(properties.getProperty(Constants.LEAF_JDBC_USERNAME));
dataSource.setPassword(properties.getProperty(Constants.LEAF_JDBC_PASSWORD));
dataSource.init();
// 3. 創建Dao
IDAllocDao dao = new IDAllocDaoImpl(dataSource);
// 4. 創建ID生成器實例SegmentIDGenImpl
idGen = new SegmentIDGenImpl();
((SegmentIDGenImpl) idGen).setDao(dao);
// 初始化SegmentIDGenImpl(加載db的tags至內存cache中,並開啓定時同步更新任務)
if (idGen.init()) {
logger.info("Segment Service Init Successfully");
} else {
throw new InitException("Segment Service Init Fail");
}
} else {
// ZeroIDGen一直返回id=0
idGen = new ZeroIDGen();
logger.info("Zero ID Gen Service Init Successfully");
}
}
/**
* 根據key獲取id
* @param key
* @return
*/
public Result getId(String key) {
return idGen.get(key);
}
/**
* 獲取號段模式id生成器SegmentIDGenImpl
* @return
*/
public SegmentIDGenImpl getIdGen() {
if (idGen instanceof SegmentIDGenImpl) {
return (SegmentIDGenImpl) idGen;
}
return null;
}
}
SegmentService
類的構造函數,主要完成以下幾件事:
- 加載
leaf.properties
配置文件,並解析配置 - 創建
Druid
數據源對象dataSource
- 創建
IDAllocDao
接口實例IDAllocDaoImpl
- 創建ID生成器實例
SegmentIDGenImpl
並初始化
① 解析leaf.properties配置文件
通過 PropertyFactory
讀取了 leaf.properties
配置文件並進行解析。其中所以的key-value配置信息最終封裝爲 Properties
中。
/**
* 加載leaf.properties配置文件中配置信息
*/
public class PropertyFactory {
private static final Logger logger = LoggerFactory.getLogger(PropertyFactory.class);
private static final Properties prop = new Properties();
static {
try {
prop.load(PropertyFactory.class.getClassLoader().getResourceAsStream("leaf.properties"));
logger.debug("Load leaf.properties successfully!");
} catch (IOException e) {
logger.warn("Load Properties Ex", e);
}
}
public static Properties getProperties() {
return prop;
}
}
② 手動創建數據源
解析完配置文件後需要判斷是否開啓號段模式:
// 是否開啓號段模式
boolean flag = Boolean.parseBoolean(properties.getProperty(Constants.LEAF_SEGMENT_ENABLE, "true"));
if (flag) {
// 2. 創建Druid dataSource
dataSource = new DruidDataSource();
dataSource.setUrl(properties.getProperty(Constants.LEAF_JDBC_URL));
dataSource.setUsername(properties.getProperty(Constants.LEAF_JDBC_USERNAME));
dataSource.setPassword(properties.getProperty(Constants.LEAF_JDBC_PASSWORD));
dataSource.init();
······
} else {
// ZeroIDGen一直返回id=0
idGen = new ZeroIDGen();
logger.info("Zero ID Gen Service Init Successfully");
}
如果沒有開啓號段模式,則創建默認返回id爲0的id生成器 ZeroIDGen
。
public class ZeroIDGen implements IDGen {
@Override
public Result get(String key) {
return new Result(0, Status.SUCCESS);
}
@Override
public boolean init() {
return true;
}
}
第二步主要通過配置文件配置的數據庫連接信息,手動創建出數據源 DruidDataSource
。
③ 創建IDAllocDaoImpl
我們先來查看 IDAllocDao
接口中的方法。
public interface IDAllocDao {
List<LeafAlloc> getAllLeafAllocs();
LeafAlloc updateMaxIdAndGetLeafAlloc(String tag);
LeafAlloc updateMaxIdByCustomStepAndGetLeafAlloc(LeafAlloc leafAlloc);
List<String> getAllTags();
}
再查看 IDAllocDaoImpl
實現類對應的方法實現。
public class IDAllocDaoImpl implements IDAllocDao {
SqlSessionFactory sqlSessionFactory;
public IDAllocDaoImpl(DataSource dataSource) {
// 手動初始化sqlSessionFactory
TransactionFactory transactionFactory = new JdbcTransactionFactory();
Environment environment = new Environment("development", transactionFactory, dataSource);
Configuration configuration = new Configuration(environment);
configuration.addMapper(IDAllocMapper.class);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
}
/**
* 獲取所有的業務key對應的發號配置
* @return
*/
@Override
public List<LeafAlloc> getAllLeafAllocs() {
SqlSession sqlSession = sqlSessionFactory.openSession(false);
try {
return sqlSession.selectList("com.sankuai.inf.leaf.segment.dao.IDAllocMapper.getAllLeafAllocs");
} finally {
sqlSession.close();
}
}
/**
* 更新數據庫的最大id值,並返回LeafAlloc
* @param tag
* @return
*/
@Override
public LeafAlloc updateMaxIdAndGetLeafAlloc(String tag) {
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
// 更新tag對應記錄中的max_id,max_id = max_id + step,step爲數據庫中設置的step
sqlSession.update("com.sankuai.inf.leaf.segment.dao.IDAllocMapper.updateMaxId", tag);
// 獲取更新完的記錄,封裝成LeafAlloc對象返回
LeafAlloc result = sqlSession.selectOne("com.sankuai.inf.leaf.segment.dao.IDAllocMapper.getLeafAlloc", tag);
// 提交事務
sqlSession.commit();
return result;
} finally {
sqlSession.close();
}
}
/**
* 依據動態調整的step值,更新DB的最大id值,並返回更新後的記錄
* @param leafAlloc
* @return
*/
@Override
public LeafAlloc updateMaxIdByCustomStepAndGetLeafAlloc(LeafAlloc leafAlloc) {
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
sqlSession.update("com.sankuai.inf.leaf.segment.dao.IDAllocMapper.updateMaxIdByCustomStep", leafAlloc);
LeafAlloc result = sqlSession.selectOne("com.sankuai.inf.leaf.segment.dao.IDAllocMapper.getLeafAlloc", leafAlloc.getKey());
sqlSession.commit();
return result;
} finally {
sqlSession.close();
}
}
/**
* 從數據庫查詢出所有的biz_tag
* @return
*/
@Override
public List<String> getAllTags() {
// 設置false,表示手動事務
SqlSession sqlSession = sqlSessionFactory.openSession(false);
try {
return sqlSession.selectList("com.sankuai.inf.leaf.segment.dao.IDAllocMapper.getAllTags");
} finally {
sqlSession.close();
}
}
}
對於接口的四個方法的作用都有詳細的註釋,讀者大致有個印象,我們後面解析獲取id流程時會繼續詳細查看。比較陌生的應該是方法的返回實體類 LeafAlloc
,其他它就是對應着數據庫表。
/**
* 分配bean,和數據庫表記錄基本對應
*/
public class LeafAlloc {
private String key; // 對應biz_tag
private long maxId; // 對應最大id
private int step; // 對應步長
private String updateTime; // 對應更新時間
// getter and setter
}
我們先來看一下這一步創建 IDAllocDaoImpl
中構造函數的邏輯,可以看到主要是按照使用MyBatis的流程創建出 SqlSessionFactory
對象。
④ 創建並初始化ID生成器
先來查看ID生成器接口:
public interface IDGen {
/**
* 獲取指定key下一個id
* @param key
* @return
*/
Result get(String key);
/**
* 初始化
* @return
*/
boolean init();
}
接口主要包含兩個方法,分別是獲取指定key的下一個id值,和初始化生成器的方法。
該接口的實現類有三個,分別是號段模式、snowflake以及默認一直返回0的生成器。
創建號段模式ID生成器
com.sankuai.inf.leaf.segment.SegmentIDGenImpl
是我們分析整個流程的重點,我們先來簡單的查看其內部幾個重要的成員變量:
/**
* 號段模式ID生成器
*/
public class SegmentIDGenImpl implements IDGen {
·······
/**
* 最大步長不超過100,0000
*/
private static final int MAX_STEP = 1000000;
/**
* 一個Segment維持時間爲15分鐘
*/
private static final long SEGMENT_DURATION = 15 * 60 * 1000L;
/**
* 線程池,用於執行異步任務,比如異步準備雙buffer中的另一個buffer
*/
private ExecutorService service = new ThreadPoolExecutor(5, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), new UpdateThreadFactory());
/**
* 標記自己是否初始化完畢
*/
private volatile boolean initOK = false;
/**
* cache,存儲所有業務key對應雙buffer號段,所以是基於內存的發號方式
*/
private Map<String, SegmentBuffer> cache = new ConcurrentHashMap<String, SegmentBuffer>();
/**
* 查詢數據庫的dao
*/
private IDAllocDao dao;
········
}
cache
是號段模式基於內存發號的關鍵,它是一個key爲數據庫表中不同業務的tag,value是一個 SegmentBuffer
對象,如果閱讀過官方的博客可以知道雙 buffer
優化的事情,這裏的SegmentBuffer
對象就是封裝了兩個 Segment
號段的數據結構。
回到 SegmentService
構造函數的第四步中來,創建 SegmentIDGenImpl
實例時使用的是默認構造函數,緊接着將第三步創建數據庫 dao
注入進 SegmentIDGenImpl
。然後調用生成器的初始化方法。
// 4. 創建ID生成器實例SegmentIDGenImpl
idGen = new SegmentIDGenImpl();
((SegmentIDGenImpl) idGen).setDao(dao);
// 初始化SegmentIDGenImpl(加載db的tags至內存cache中,並開啓定時同步更新任務)
if (idGen.init()) {
logger.info("Segment Service Init Successfully");
} else {
throw new InitException("Segment Service Init Fail");
}
初始化號段模式ID生成器
我們查看 SegmentIDGenImpl
的初始化方法邏輯,可以看到主要調用了兩個方法,並且設置了自己的初始化標記爲OK狀態。如果沒有初始化成功,會拋出異常,這在上面代碼可以看出。
@Override
public boolean init() {
logger.info("Init ...");
// 確保加載到kv後才初始化成功
updateCacheFromDb();
initOK = true;
// 定時1min同步一次db和cache
updateCacheFromDbAtEveryMinute();
return initOK;
}
我們具體來查看 updateCacheFromDb()
和 updateCacheFromDbAtEveryMinute()
方法邏輯。通過方法名其實我們可以推測方法含義是從數據庫中取出數據更新 cache
,第二個方法則是一個定時任務,每分鐘都執行一遍第一個方法。我們具體查看一下。
/**
* 將數據庫表中的tags同步到cache中
*/
private void updateCacheFromDb() {
logger.info("update cache from db");
StopWatch sw = new Slf4JStopWatch();
try {
// 獲取數據庫表中所有的biz_tag
List<String> dbTags = dao.getAllTags();
if (dbTags == null || dbTags.isEmpty()) {
return;
}
// 獲取當前的cache中所有的tag
List<String> cacheTags = new ArrayList<String>(cache.keySet());
// 數據庫中的tag
List<String> insertTags = new ArrayList<String>(dbTags);
List<String> removeTags = new ArrayList<String>(cacheTags);
// 下面兩步操作:保證cache和數據庫tags同步
// 1. cache新增上數據庫表後添加的tags
// 2. cache刪除掉數據庫表後刪除的tags
// 1. db中新加的tags灌進cache,並實例化初始對應的SegmentBuffer
insertTags.removeAll(cacheTags);
for (String tag : insertTags) {
SegmentBuffer buffer = new SegmentBuffer();
buffer.setKey(tag);
// 零值初始化當前正在使用的Segment號段
Segment segment = buffer.getCurrent();
segment.setValue(new AtomicLong(0));
segment.setMax(0);
segment.setStep(0);
cache.put(tag, buffer);
logger.info("Add tag {} from db to IdCache, SegmentBuffer {}", tag, buffer);
}
// 2. cache中已失效的tags從cache刪除
removeTags.removeAll(dbTags);
for (String tag : removeTags) {
cache.remove(tag);
logger.info("Remove tag {} from IdCache", tag);
}
} catch (Exception e) {
logger.warn("update cache from db exception", e);
} finally {
sw.stop("updateCacheFromDb");
}
}
首先通過dao層查詢出數據庫表中最新的所有的 biz_tag
,緊接着就是同步數據庫中的 tags
和內存中的 cache
。同步的方式包含兩步操作:
- 插入
cache
中不存在但是數據庫新增的biz_tag
; - 刪除
cache
中仍然存在但是數據庫表中已經刪除的biz_tag
。
上面這段代碼主要完成的就是這兩步操作,代碼邏輯仔細閱讀還是比較清晰的,配合註釋讀者可以相應理解,不再贅述。
需要額外提及的是 cache
的key我們已經知道是 biz_tag
,但value我們僅僅知道是封裝了兩個 Segment
號段的 SegmentBuffer
。我們具體來看看 SegmentBuffer
的定義。
/**
* 雙buffer——雙號段
* 雙Buffer的方式,保證無論何時DB出現問題,都能有一個Buffer的號段可以正常對外提供服務
* 只要DB在一個Buffer的下發的週期內恢復,就不會影響整個Leaf的可用性
*/
public class SegmentBuffer {
private String key; // 數據庫的業務tag
private Segment[] segments; //雙buffer,雙號段
private volatile int currentPos; //當前的使用的segment的index
private volatile boolean nextReady; //下一個segment是否處於可切換狀態
private volatile boolean initOk; //是否DB數據初始化完成
private final AtomicBoolean threadRunning; //線程是否在運行中
private final ReadWriteLock lock; // 讀寫鎖
private volatile int step; // 動態調整的step
private volatile int minStep; // 最小step
private volatile long updateTimestamp; // 更新時間戳
public SegmentBuffer() {
// 創建雙號段,能夠異步準備,並切換
segments = new Segment[]{new Segment(this), new Segment(this)};
currentPos = 0;
nextReady = false;
initOk = false;
threadRunning = new AtomicBoolean(false);
lock = new ReentrantReadWriteLock();
}
public int nextPos() {
return (currentPos + 1) % 2;
}
public void switchPos() {
currentPos = nextPos();
}
public Lock rLock() {
return lock.readLock();
}
public Lock wLock() {
return lock.writeLock();
}
}
可以看見 SegmentBuffer
中包含了一個號段數組,包含兩個 Segment
,每一次只用一個,另一個異步的準備好,等到當前號段用完,就可以切換另一個,像Young GC的兩個Survivor區倒來倒去的思想。我們再來看一下號段 Segment
的定義。
/**
* 號段類
*/
public class Segment {
/**
* 內存生成的每一個id號
*/
private AtomicLong value = new AtomicLong(0);
/**
* 當前號段允許的最大id值
*/
private volatile long max;
/**
* 步長,會根據數據庫的step動態調整
*/
private volatile int step;
/**
* 當前號段所屬的SegmentBuffer
*/
private SegmentBuffer buffer;
public Segment(SegmentBuffer buffer) {
this.buffer = buffer;
}
/**
* 獲取號段的剩餘量
* @return
*/
public long getIdle() {
return this.getMax() - getValue().get();
}
}
value
就是用來產生id值的,它是一個 AtomicLong
類型,多線程下可以利用它的一些原子API操作。max
則代表自己(號段對象)能產生的最大的id值,也就是value的上限,用完了就需要切換號段,自己重新從數據庫獲取下一個號段區間。step
是動態調整的步長,關於動態調整,官方博客也有所解釋,這裏先不贅述。當自己用完了,就需要從數據庫請求新的號段區間,區間大小就是由這個 step
決定的。
介紹完Leaf的號段,雙Buffer數據結構後,我們回過頭查看同步DB到 cache
的邏輯中插入新的 SegmentBuffer
是如何創建的。
for (String tag : insertTags) {
SegmentBuffer buffer = new SegmentBuffer();
buffer.setKey(tag);
// 零值初始化當前正在使用的Segment號段
Segment segment = buffer.getCurrent();
segment.setValue(new AtomicLong(0));
segment.setMax(0);
segment.setStep(0);
cache.put(tag, buffer);
}
可以看到對於 SegmentBuffer
我們僅僅設置了key,然後就是依靠 SegmentBuffer
自身的構造函數對其內部成員進行了默認初始化,也可以說是零值初始化。特別注意,此時 SegmentBuffer
的 initOk
標記還是 false
,這也說明這個標記其實並不是標記零值初始化是否完成。然後程序接着對0號 Segment
的所有成員進行了零值初始化。
同步完成後,即將數據庫中的所有 tags
記錄加載到內存後,便將ID生成器的初始化標記設置爲 true
。
我們再來查看 updateCacheFromDbAtEveryMinute()
方法邏輯。
/**
* 每分鐘同步db到cache
*/
private void updateCacheFromDbAtEveryMinute() {
ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("check-idCache-thread");
t.setDaemon(true);
return t;
}
});
service.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
updateCacheFromDb();
}
}, 60, 60, TimeUnit.SECONDS);
}
可以看到方法中創建了一個定時執行任務線程池,任務就是 updateCacheFromDb()
,也就是上面那個方法,定時時間爲60s,也就是1min。
獲取ID
上一小節我們主要是在分析創建 SegmentService
過程中做了哪些事情,總結下來最重要的就是從數據庫表中準備好 cache
, cache
中包含每個key對應的雙號段,經過第一部分已經零值初始化好雙號段的當前使用號段。接下來我們繼續分析 SegmentService
的 getId()
方法,我們的控制層就是通過該方法獲取id的。
/**
* 根據key獲取id
* @param key
* @return
*/
public Result getId(String key) {
return idGen.get(key);
}
再次分析號段生成器 SegmentIDGenImpl
的 get()
方法。
/**
* 獲取對應key的下一個id值
* @param key
* @return
*/
@Override
public Result get(final String key) {
// 必須在 SegmentIDGenImpl 初始化後執行init()方法
// 也就是必須將數據庫中的tags加載到內存cache中,並開啓定時同步任務
if (!initOK) {
return new Result(EXCEPTION_ID_IDCACHE_INIT_FALSE, Status.EXCEPTION);
}
if (cache.containsKey(key)) {
// 獲取cache中對應的SegmentBuffer,SegmentBuffer中包含雙buffer,兩個號段
SegmentBuffer buffer = cache.get(key);
// 雙重判斷,避免多線程重複執行SegmentBuffer的初始化值操作
// 在get id前檢查是否完成DB數據初始化cache中key對應的的SegmentBuffer(之前只是零值初始化),需要保證線程安全
if (!buffer.isInitOk()) {
synchronized (buffer) {
if (!buffer.isInitOk()) {
// DB數據初始化SegmentBuffer
try {
// 根據數據庫表中key對應的記錄 來初始化SegmentBuffer當前正在使用的Segment
updateSegmentFromDb(key, buffer.getCurrent());
logger.info("Init buffer. Update leafkey {} {} from db", key, buffer.getCurrent());
buffer.setInitOk(true);
} catch (Exception e) {
logger.warn("Init buffer {} exception", buffer.getCurrent(), e);
}
}
}
}
// SegmentBuffer準備好之後正常就直接從cache中生成id即可
return getIdFromSegmentBuffer(cache.get(key));
}
// cache中不存在對應的key,則返回異常錯誤
return new Result(EXCEPTION_ID_KEY_NOT_EXISTS, Status.EXCEPTION);
}
首先先從 cache
中獲取 key
對應的 SegmentBuffer
,然後判斷 SegmentBuffer
是否是初始化完成,也就是 SegmentBuffer
的 initOk
標記。這裏用了雙重判斷+synchronized
方式確保 SegmentBuffer
只被初始化一次。那麼這裏初始化究竟是指什麼,纔算初始化完成呢?
① 初始化SegmentBuffer
初始化 SegmentBuffer
的核心邏輯就是調用下面這個方法。
// 根據數據庫表中key對應的記錄 來初始化SegmentBuffer當前正在使用的Segment
updateSegmentFromDb(key, buffer.getCurrent());
查看方法名,也可以知道是從數據庫表查詢數據更新號段 Segment
,對於號段初始狀態來說,該方法含義可以理解爲初始化 Segment
的值,對於用完的號段來講,可以理解爲從數據庫獲取下一號段值。
所以這裏初始化是指DB數據初始化當前號段,初始化完成就標記 SegmentBuffer
的 initOk
爲 true
,也就表明 SegmentBuffer
中有一個號段已經準備完成了。
我們具體查看 updateSegmentFromDb(key, buffer.getCurrent())
方法:
/**
* 從數據庫表中讀取數據更新SegmentBuffer中的Segment
* @param key
* @param segment
*/
public void updateSegmentFromDb(String key, Segment segment) {
StopWatch sw = new Slf4JStopWatch();
/**
* 1. 先設置SegmentBuffer
*/
// 獲取Segment號段所屬的SegmentBuffer
SegmentBuffer buffer = segment.getBuffer();
LeafAlloc leafAlloc;
// 如果buffer沒有DB數據初始化(也就是第一次進行DB數據初始化)
if (!buffer.isInitOk()) {
// 更新數據庫中key對應記錄的maxId(maxId表示當前分配到的最大id,maxId=maxId+step),並查詢更新後的記錄返回
leafAlloc = dao.updateMaxIdAndGetLeafAlloc(key);
// 數據庫初始設置的step賦值給當前buffer的初始step,後面後動態調整
buffer.setStep(leafAlloc.getStep());
// leafAlloc中的step爲DB中設置的step,buffer這裏是未進行DB數據初始化的,所以DB中step代表動態調整的最小下限
buffer.setMinStep(leafAlloc.getStep());
}
// 如果buffer的更新時間是0(初始是0,也就是第二次調用updateSegmentFromDb())
else if (buffer.getUpdateTimestamp() == 0) {
// 更新數據庫中key對應記錄的maxId(maxId表示當前分配到的最大id,maxId=maxId+step),並查詢更新後的記錄返回
leafAlloc = dao.updateMaxIdAndGetLeafAlloc(key);
// 記錄buffer的更新時間
buffer.setUpdateTimestamp(System.currentTimeMillis());
// leafAlloc中的step爲DB中的step
buffer.setMinStep(leafAlloc.getStep());
}
// 第三次以及之後的進來 動態設置nextStep
else {
// 計算當前更新操作和上一次更新時間差
long duration = System.currentTimeMillis() - buffer.getUpdateTimestamp();
int nextStep = buffer.getStep();
/**
* 動態調整step
* 1) duration < 15 分鐘 : step 變爲原來的2倍, 最大爲 MAX_STEP
* 2) 15分鐘 <= duration < 30分鐘 : nothing
* 3) duration >= 30 分鐘 : 縮小step, 最小爲DB中配置的step
*
* 這樣做的原因是認爲15min一個號段大致滿足需求
* 如果updateSegmentFromDb()速度頻繁(15min多次),也就是
* 如果15min這個時間就把step號段用完,爲了降低數據庫訪問頻率,我們可以擴大step大小
* 相反如果將近30min才把號段內的id用完,則可以縮小step
*/
// duration < 15 分鐘 : step 變爲原來的2倍. 最大爲 MAX_STEP
if (duration < SEGMENT_DURATION) {
if (nextStep * 2 > MAX_STEP) {
//do nothing
} else {
// 步數 * 2
nextStep = nextStep * 2;
}
}
// 15分鐘 < duration < 30分鐘 : nothing
else if (duration < SEGMENT_DURATION * 2) {
//do nothing with nextStep
}
// duration > 30 分鐘 : 縮小step ,最小爲DB中配置的步數
else {
nextStep = nextStep / 2 >= buffer.getMinStep() ? nextStep / 2 : nextStep;
}
logger.info("leafKey[{}], dbStep[{}], duration[{}mins], nextStep[{}]", key, buffer.getStep(), String.format("%.2f",((double)duration / (1000 * 60))), nextStep);
/**
* 根據動態調整的nextStep更新數據庫相應的maxId
*/
// 爲了高效更新記錄,創建一個LeafAlloc,僅設置必要的字段的信息
LeafAlloc temp = new LeafAlloc();
temp.setKey(key);
temp.setStep(nextStep);
// 根據動態調整的step更新數據庫的maxId
leafAlloc = dao.updateMaxIdByCustomStepAndGetLeafAlloc(temp);
// 記錄更新時間
buffer.setUpdateTimestamp(System.currentTimeMillis());
// 記錄當前buffer的動態調整的step值
buffer.setStep(nextStep);
// leafAlloc的step爲DB中的step,所以DB中的step值代表着下限
buffer.setMinStep(leafAlloc.getStep());
}
/**
* 2. 準備當前Segment號段
*/
// 設置Segment號段id的起始值,value就是id(start=max_id-step)
long value = leafAlloc.getMaxId() - buffer.getStep();
// must set value before set max(https://github.com/Meituan-Dianping/Leaf/issues/16)
segment.getValue().set(value);
segment.setMax(leafAlloc.getMaxId());
segment.setStep(buffer.getStep());
sw.stop("updateSegmentFromDb", key + " " + segment);
}
這個函數的邏輯非常重要,還包含了動態調整步長的邏輯。首先,該方法被調用的時機我們需要明確,每當我們需要從數據庫獲取一個號段纔會被調用。方法的第一部分主要先通過數據庫並設置 SegmentBuffer
相關值,第二部分再準備 Segment
。
第一部分的邏輯按照調用該方法的次數分爲第一次準備號段、第二次準備號段和第三次及之後的準備號段。
- 第一次準備號段,也就是
SegmentBuffer
還沒有DB初始化,我們要從數據庫獲取一個號段,記錄SegmentBuffer
的當前步長、最小步長都是數據庫設置的步長; - 第二次準備號段,也就是雙buffer的異步準備另一個號段
Segment
時,會進入這一邏輯分支。仍然從數據庫獲取一個號段,此時記錄這次獲取下一個號段的時間戳,設置最小步長是數據庫設置的步長; - 之後再次準備號段,首先要動態調整這次申請號段的區間大小,也就是代碼中的
nextStep
,調整規則主要跟號段申請頻率有關,具體可以查看註釋以及代碼。計算出動態調整的步長,需要根據新的步長去數據庫申請號段,同時記錄這次獲取號段的時間戳,保存動態調整的步長到SegmentBuffer
,設置最小步長是數據庫設置的步長。
第二部分邏輯主要是準備 Segment
號段,將 Segment
號段的四個成員變量進行新一輪賦值,value
就是 id
(start=max_id-step
)。
② 從號段中獲取id
當 SegmentBuffer
和 其中一個號段 Segment
準備好,就可以進行從號段中獲取id。我們具體查看號段ID生成器 SegmentIDGenImpl
的 getIdFromSegmentBuffer()
方法。
/**
* 從SegmentBuffer生成id返回
* @param buffer
* @return
*/
public Result getIdFromSegmentBuffer(final SegmentBuffer buffer) {
// 自旋獲取id
while (true) {
try {
// 獲取buffer的共享讀鎖,在平時不操作Segment的情況下益於併發
buffer.rLock().lock();
// 獲取當前正在使用的Segment
final Segment segment = buffer.getCurrent();
// ===============異步準備雙buffer的另一個Segment==============
// 1. 另一個Segment沒有準備好
// 2. 當前Segment已經使用超過10%則開始異步準備另一個Segment
// 3. buffer中的threadRunning字段. 代表是否已經提交線程池運行,是否有其他線程已經開始進行另外號段的初始化工作.使用CAS進行更新保證buffer在任意時刻,只會有一個線程進行異步更新另外一個號段.
if (!buffer.isNextReady() && (segment.getIdle() < 0.9 * segment.getStep()) && buffer.getThreadRunning().compareAndSet(false, true)) {
// 線程池異步執行【準備Segment】任務
service.execute(new Runnable() {
@Override
public void run() {
// 獲得另一個Segment對象
Segment next = buffer.getSegments()[buffer.nextPos()];
boolean updateOk = false;
try {
// 從數據庫表中準備Segment
updateSegmentFromDb(buffer.getKey(), next);
updateOk = true;
logger.info("update segment {} from db {}", buffer.getKey(), next);
} catch (Exception e) {
logger.warn(buffer.getKey() + " updateSegmentFromDb exception", e);
} finally {
// 如果準備成功,則通過獨佔寫鎖設置另一個Segment準備標記OK,threadRunning爲false表示準備完畢
if (updateOk) {
// 讀寫鎖是不允許線程先獲得讀鎖繼續獲得寫鎖,這裏可以是因爲這一段代碼其實是線程池線程去完成的,不是獲取到讀鎖的線程
buffer.wLock().lock();
buffer.setNextReady(true);
buffer.getThreadRunning().set(false);
buffer.wLock().unlock();
} else {
// 失敗了,則還是沒有準備好,threadRunning恢復false,以便於下次獲取id時重新再異步準備Segment
buffer.getThreadRunning().set(false);
}
}
}
});
}
// 原子value++(返回舊值),也就是下一個id,這一步是多線程操作的,每一個線程加1都是原子的,但不一定保證順序性
long value = segment.getValue().getAndIncrement();
// 如果獲取到的id小於maxId
if (value < segment.getMax()) {
return new Result(value, Status.SUCCESS);
}
} finally {
// 釋放讀鎖
buffer.rLock().unlock();
}
// 等待線程池異步準備號段完畢
waitAndSleep(buffer);
// 執行到這裏,說明當前號段已經用完,應該切換另一個Segment號段使用
try {
// 獲取獨佔式寫鎖
buffer.wLock().lock();
// 獲取當前使用的Segment號段
final Segment segment = buffer.getCurrent();
// 重複獲取value, 多線程執行時,Segment可能已經被其他線程切換。再次判斷, 防止重複切換Segment
long value = segment.getValue().getAndIncrement();
if (value < segment.getMax()) {
return new Result(value, Status.SUCCESS);
}
// 執行到這裏, 說明其他的線程沒有進行Segment切換,並且當前號段所有號碼用完,需要進行切換Segment
// 如果準備好另一個Segment,直接切換
if (buffer.isNextReady()) {
buffer.switchPos();
buffer.setNextReady(false);
}
// 如果另一個Segment沒有準備好,則返回異常雙buffer全部用完
else {
logger.error("Both two segments in {} are not ready!", buffer);
return new Result(EXCEPTION_ID_TWO_SEGMENTS_ARE_NULL, Status.EXCEPTION);
}
} finally {
// 釋放寫鎖
buffer.wLock().unlock();
}
}
}
首先該方法最外層套了一個循環,不斷地嘗試獲取id。整個方法的邏輯大致包含:
- 首先獲取共享讀鎖,多個線程能夠同時進來獲取id。如果能夠不需要異步準備雙buffer的另一個
Segment
且分發的id號沒有超出maxId,那麼可以直接返回id號。多個線程併發獲取id號,靠AtomicLong
的getAndIncrement()
原子操作保證不出問題。 - 如果需要異步準備另一個
Segment
,則將準備任務提交到線程池中進行完成。多線程執行下,要保證只有一個線程去提交任務。這一點是靠SegmentBuffer
中的threadRunning
字段實現的。threadRunning
字段用volatile
修飾保證多線程可見性,其含義代表了異步準備號段任務是否已經提交線程池運行,是否有其他線程已經開始進行另外號段的初始化工作。使用CAS操作進行更新,保證SegmentBuffer
在任意時刻只會有一個線程進行異步更新另外一個號段。 - 如果號段分配的
id
號超出了maxId,則需要進行切換雙buffer的操作。在進行直接切換之前,需要再次判斷是否id
還大於 maxId,因爲多線程下,號段已經被其他線程切換成功,自己還不知道,所以爲了避免重複切換出錯,需要再次判斷。切換操作爲了保證同一時間只能有一個線程切換,這裏利用了獨佔式的寫鎖。