需求
-
已獲得的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生成
如何實現同一臺伺服器在高併發場景,讓大家順序拿號,別拿重複,也別漏拿?
其實就是保持這個號段物件隔離性的問題,可以使用原子變數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.");
}
複製程式碼
}