概述
設計目標:每秒最大生成10萬個ID,ID單調遞增且唯一。Reidis可以不需要持久化ID。
要求:集羣時鐘不能倒退。
總體思路:集羣中每個節點預生成生成ID;然後與redis的已經存在的ID做比較。如果大於,則取節點生成的ID;小於的話,取Redis中最大ID自增。
Java代碼
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.FastDateFormat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import static com.google.common.base.Preconditions.checkArgument;
/**
* 生成遞增的唯一序列號, 可以用來生成訂單號,例如216081817202494579
* <p/>
* 生成規則:
* 業務類型 + redis中最大的序列號
* <p/>
* 約定:
* redis中最大的序列號長度爲17,包括{6位日期 + 6位時間 + 3位毫秒數 + 2位隨機}
* <p/>
* 建議:
* 爲了容錯和服務降級, SeqGenerator生成失敗時最好採用UUID替換
* <p/>
* Created by juemingzi on 16/8/19.
*/
public class SeqGenerator {
private static final Logger logger = LoggerFactory.getLogger(SeqGenerator.class);
private static final Path filePath = Paths.get(Thread.currentThread().getContextClassLoader().getResource("lua/get_next_seq.lua").getPath());
//線程安全
private static final FastDateFormat seqDateFormat = FastDateFormat.getInstance("yyMMddHHmmssSSS");
private static final RedisExtraService redisExtraService = SpringContext.getBean(RedisExtraService.class);
private final byte[] keyName;
private final byte[] incrby;
private byte[] sha1;
public SeqGenerator(String keyName) throws IOException {
this(keyName, 1);
}
/**
* @param keyName
* @param incrby
*/
public SeqGenerator(String keyName, int incrby) throws IOException {
checkArgument(keyName != null && incrby > 0);
this.keyName = keyName.getBytes();
this.incrby = Integer.toString(incrby).getBytes();
init();
}
private void init() throws IOException {
byte[] script;
try {
script = Files.readAllBytes(filePath);
} catch (IOException e) {
logger.error("讀取文件出錯, path: {}", filePath);
throw e;
}
sha1 = redisExtraService.scriptLoad(script);
}
public String getNextSeq(String bizType) {
checkArgument(StringUtils.isNotBlank(bizType));
return bizType + getMaxSeq();
}
private String generateSeq() {
String seqDate = seqDateFormat.format(System.currentTimeMillis());
String candidateSeq = new StringBuilder(17).append(seqDate).append(RandomStringUtils.randomNumeric(2)).toString();
return candidateSeq;
}
/**
* 通過redis生成17位的序列號,lua腳本保證序列號的唯一性
*
* @return
*/
public String getMaxSeq() {
String maxSeq = new String((byte[]) redisExtraService.evalsha(sha1, 3, keyName, incrby, generateSeq().getBytes()));
return maxSeq;
}
}
lua腳本
--
-- 獲取最大的序列號,樣例爲16081817202494579
--
-- Created by IntelliJ IDEA.
-- User: juemingzi
-- Date: 16/8/18
-- Time: 17:22
local function get_max_seq()
local key = tostring(KEYS[1])
local incr_amoutt = tonumber(KEYS[2])
local seq = tostring(KEYS[3])
local month_in_seconds = 24 * 60 * 60 * 30
if (1 == redis.call(\'setnx\', key, seq))
then
redis.call(\'expire\', key, month_in_seconds)
return seq
else
local prev_seq = redis.call(\'get\', key)
if (prev_seq < seq)
then
redis.call(\'set\', key, seq)
return seq
else
--[[
不能直接返回redis.call(\'incr\', key),因爲返回的是number浮點數類型,會出現不精確情況。
注意: 類似"16081817202494579"數字大小已經快超時lua和reids最大數值,請謹慎的增加seq的位數
--]]
redis.call(\'incrby\', key, incr_amoutt)
return redis.call(\'get\', key)
end
end
end
return get_max_seq()
測試代碼
public class SeqGeneratorTest extends BaseTest {
@Test
public void testGetNextSeq() throws Exception {
final SeqGenerator seqGenerater = new SeqGenerator("orderId");
String orderId = seqGenerater.getNextSeq(Integer.toString(WaitingOrder.KIND_TAKE_OUT));
assertNotNull(orderId);
System.out.println("orderId is: " + orderId);
}
@Test
public void testGetNextSeqWithMultiThread() throws Exception {
int cpus = Runtime.getRuntime().availableProcessors();
CountDownLatch begin = new CountDownLatch(1);
CountDownLatch end = new CountDownLatch(cpus);
final Set<String> seqSet = new ConcurrentSkipListSet<>();
ExecutorService executorService = Executors.newFixedThreadPool(cpus);
final SeqGenerator seqGenerater = new SeqGenerator("orderId");
for (int i = 0; i < cpus; i++) {
executorService.execute(new Worker(seqGenerater, seqSet, begin, end));
}
begin.countDown();
end.await();
assertEquals(seqSet.size(), cpus * 10000);
System.out.println("finish!");
}
private static class Worker implements Runnable {
private final CountDownLatch begin;
private final CountDownLatch end;
private final Set<String> seqSet;
private final SeqGenerator seqGenerator;
public Worker(SeqGenerator seqGenerator, Set<String> seqSet, CountDownLatch begin, CountDownLatch end) {
this.seqGenerator = seqGenerator;
this.seqSet = seqSet;
this.begin = begin;
this.end = end;
}
@Override
public void run() {
try {
begin.await();
for (int i = 0; i < 10000; i++) {
String seq = seqGenerator.getNextSeq("2");
if (!seqSet.add(seq)) {
System.out.println(seq);
fail();
}
}
System.out.println("end");
} catch (Exception e) {
e.printStackTrace();
} finally {
end.countDown();
}
}
}
}