設計目標:每秒最大生成10萬個ID,ID單調(diào)遞增且唯一。Reidis可以不需要持久化ID。
要求:集群時鐘不能倒退。
總體思路:集群中每個節(jié)點預生成生成ID;然后與redis的已經(jīng)存在的ID做比較。如果大于,則取節(jié)點生成的ID;小于的話,取Redis中最大ID自增。
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/> * 生成規(guī)則: * 業(yè)務類型 + redis中最大的序列號 * <p/> * 約定: * redis中最大的序列號長度為17,包括{6位日期 + 6位時間 + 3位毫秒數(shù) + 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; }}
---- 獲取最大的序列號,樣例為16081817202494579---- Created by IntelliJ IDEA.-- User: juemingzi-- Date: 16/8/18-- Time: 17:22local 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浮點數(shù)類型,會出現(xiàn)不精確情況。 注意: 類似"16081817202494579"數(shù)字大小已經(jīng)快超時lua和reids最大數(shù)值,請謹慎的增加seq的位數(shù) --]] redis.call(\'incrby\', key, incr_amoutt) return redis.call(\'get\', key) end endendreturn 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(); } } }}