自增ID的實現

hatch發表於2018-04-08

需求

  • 已獲得的ID不可再次獲取

  • 需持久化

  • 範圍有限

  • 每次取出的數值都比上一次的+1[避免浪費]

  • 高效能

藉助MongoDB方式

使用MongoDB的原子操作findAndModify自增後然後取出自增後的值,實現簡單。

@Service
public class IdService {

@Resource
private MongoTemplate mongoTemplate;

private static final String COLLECTION = "ids";

private static final String ID = "id";

private static final String PRIMARY_KEY = "_id";

private static final String ID_KEY = "last_id";

//初始化id的值
@PostConstruct
public void init() {
	BasicDBObject json = new BasicDBObject();
	json.put(PRIMARY_KEY, ID_KEY);
	json.put(ID, 1);

	try {
		mongoTemplate.insert(json, COLLECTION);
	} catch (DuplicateKeyException ex) {
	
	}
}

public long nextId() {

	Query query = new Query(Criteria.where(PRIMARY_KEY).is(ID_KEY));
	
	Update update = new Update().inc(ID, 1);
	final FindAndModifyOptions option = new FindAndModifyOptions();
	option.returnNew(true);

	BasicDBObject json = mongoTemplate.findAndModify(query, update, option, BasicDBObject.class, COLLECTION);

	return json.getLong(ID);
}
複製程式碼

}

藉助Redis方式

當使用資料庫來生成ID效能不夠要求的時候,我們可以嘗試使用Redis來生成ID。

可以用Redis的原子操作INCR和INCRBY來實現 不依賴於資料庫,靈活方便,且效能優於資料庫。

@Service
public class IdService {

@Resource
private JedisPool jedisPool;

public Long nextId(){

    return PoolUtils.doWorkInPool(jedisPool, new PoolUtils.PoolWork<Long>(){

        @Override
        public Long doWork(Jedis poolResource) {
            return poolResource.incr("id");
        }
    });
}
複製程式碼

}

public final class PoolUtils {

public static <V> V doWorkInPool(final JedisPool pool, final PoolWork<V> work) {
	if (pool == null) {
		throw new IllegalArgumentException("pool must not be null");
	}
	if (work == null) {
		throw new IllegalArgumentException("work must not be null");
	}
	Jedis poolResource = null;
	final V result;
	try {
		poolResource = pool.getResource();
		result = work.doWork(poolResource);
	} finally {
		if (poolResource != null) {
			poolResource.close();
		}
	}
	return result;
}

public interface PoolWork<V> {
	V doWork(Jedis poolResource);
}

private PoolUtils() {
}
複製程式碼

}

@Bean(destroyMethod = "close")
public JedisPool jedisPool() {

    JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
    jedisPoolConfig.setMaxTotal(Integer.valueOf(env.getProperty("redis.pool.maxActive").trim()));
    jedisPoolConfig.setMaxIdle(Integer.valueOf(env.getProperty("redis.pool.maxIdle").trim()));
    jedisPoolConfig.setMinIdle(Integer.valueOf(env.getProperty("redis.pool.minIdle").trim()));
    jedisPoolConfig.setMaxWaitMillis(Long.valueOf(env.getProperty("redis.pool.maxWaitMillis").trim()));
    jedisPoolConfig.setTestOnBorrow(Boolean.valueOf(env.getProperty("redis.pool.testOnBorrow").trim()));
    jedisPoolConfig.setTestOnReturn(Boolean.valueOf(env.getProperty("redis.pool.testOnReturn").trim()));
    jedisPoolConfig.setTestWhileIdle(Boolean.valueOf(env.getProperty("redis.pool.testWhileIdle").trim()));
    jedisPoolConfig.setBlockWhenExhausted(Boolean.valueOf(env.getProperty("redis.pool.blockWhenExhausted").trim()));
    jedisPoolConfig.setEvictionPolicyClassName(env.getProperty("redis.pool.evictionPolicyClassName").trim());
    jedisPoolConfig.setLifo(Boolean.valueOf(env.getProperty("redis.pool.lifo").trim()));
    jedisPoolConfig.setNumTestsPerEvictionRun(Integer.parseInt(env.getProperty("redis.pool.numTestsPerEvictionRun").trim()));
    jedisPoolConfig.setMinEvictableIdleTimeMillis(Long.parseLong(env.getProperty("redis.pool.minEvictableIdleTimeMillis").trim()));
    jedisPoolConfig.setTimeBetweenEvictionRunsMillis(Long.parseLong(env.getProperty("redis.pool.timeBetweenEvictionRunsMillis").trim()));
    jedisPoolConfig.setTestWhileIdle(Boolean.parseBoolean(env.getProperty("redis.pool.testWhileIdle").trim()));

    if (!StringUtils.isEmpty(env.getProperty("redis.password"))) {
        return new JedisPool(jedisPoolConfig, env.getProperty("redis.host").trim(), Integer.parseInt(env.getProperty("redis.port").trim()), Integer.parseInt(env.getProperty("redis.timeout").trim()), env.getProperty("redis.password").trim());
    } else {
        return new JedisPool(jedisPoolConfig, env.getProperty("redis.host").trim(), Integer.parseInt(env.getProperty("redis.port").trim()), Integer.parseInt(env.getProperty("redis.timeout").trim()));
    }
}
複製程式碼

MySQL批量ID生成

自增ID的實現

如何實現同一臺伺服器在高併發場景,讓大家順序拿號,別拿重複,也別漏拿?

其實就是保持這個號段物件隔離性的問題,可以使用原子變數AtomicLong. 記憶體中快取了一段ID號段,此時每次有請求來取號時候,判斷一下有沒有到最後一個號碼,沒有到,就拿個號,走人

Long id = currentVal.incrementAndGet();
複製程式碼

如果到達了最後一個號碼,那麼阻塞住其他請求執行緒,最早的那個執行緒去db取個號段,再更新一下號段的兩個值,就可以了。

我們似乎解決了同一臺伺服器在高併發下的問題,但是如果idService服務多點部署,多個服務在啟動過程中,進行ID批量申請時,可能由於併發導致資料不一致。

解決方案:

1、利用資料庫悲觀鎖機制,查詢時SQL:select last_id from id for update
2、實施CAS樂觀鎖,在寫回時對last_id的初始條件進行比對,就能避免資料的不一致,寫回時SQL:
update id set last_id = last_id +size and last_id = last_id 
複製程式碼

兩種鎖各有優缺點,不可認為一種好於另一種,像樂觀鎖適用於寫比較少的情況下,即衝突真的很少發生的時候,這樣可以省去了鎖的開銷,加大了系統的整個吞吐量。但如果經常產生衝突,上層應用會不斷的進行retry,這樣反倒是降低了效能,所以這種情況下用悲觀鎖就比較合適。

ID生成實現

資料庫設計:

 CREATE TABLE IF NOT EXISTS `id` (
  `last_id` bigint(20) unsigned NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

--
-- 轉存表中的資料 `id`
--

INSERT INTO `id` (`last_id`) VALUES (0);

--
-- Indexes for dumped tables
--

--
-- Indexes for table `id`
--
ALTER TABLE `id`
  ADD PRIMARY KEY (`last_id`);
複製程式碼

實現:

@Service
public class IdService {

private static final Logger logger = LoggerFactory.getLogger(IdService.class);

private AtomicLong currentVal = new AtomicLong(0L);

private AtomicLong maxVal = new AtomicLong(0L);

private static final long FETCH_SIZE = 50;// 每次生成50個id

@Resource
private IdMapper idMapper;

@PostConstruct
public void init() {
	fecth();
}

/**
 * 獲取自增ID序列
 * 
 * @return
 */
public Long nextId() {

	if (currentVal.get() >= maxVal.get()) {

		synchronized (this) {

			if (currentVal.get() >= maxVal.get()) {
				fecth();
			}
		}

	}

	return currentVal.incrementAndGet();
}

private void fecth() {

	int retry = 0;

	while (retry < 10) {

		IdCriteria idCriteria = new IdCriteria();
		idCriteria.setLimitEnd(1);

		final List<Id> ids = idMapper.selectByExample(idCriteria);

		int row = idMapper.inc(FETCH_SIZE, ids.get(0).getLastId());

		if (row > 0) {

			currentVal.set(ids.get(0).getLastId());

			maxVal.set(ids.get(0).getLastId() + FETCH_SIZE);

			return;
		}

		retry++;
	}


        logger.error(Constants.MARKER_INT, "update id failed after 10 times.");
	throw new RuntimeException("update id failed after 10 times.");
	
}
複製程式碼

}

相關文章