Redis在集群环境中生成唯一ID

原创 2016年08月28日 21:38:22

概述

设计目标:每秒最大生成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();
            }
        }
    }
}
版权声明:本文为博主原创文章,未经博主允许不得转载。

基于redis的分布式ID生成器

项目地址https://github.com/hengyunabc/redis-id-generator基于redis的分布式ID生成器。准备首先,要知道redis的EVAL,EVALSHA命令:ht...

全局唯一ID生成方案

一旦数据库被切分到多个物理结点上,我们将不能再依赖数据库自身的主键生成机制。一方面,某个分区数据库自生成的ID无法保证在全局上是唯一的;另一方面,应用程序在插入数据之前需要先获得ID,以便进行SQL路...

高并发分布式系统中生成全局唯一Id汇总

数据在分片时,典型的是分库分表,就有一个全局ID生成的问题。 单纯的生成全局ID并不是什么难题,但是生成的ID通常要满足分片的一些要求:    1 不能有单点故障。    2 以时间为序,或者I...

如何在高并发分布式系统中生成全局唯一Id

但这篇博文实际上是“半分享半讨论”的博文: 1)         半分享是我将说下我所了解到的关于今天主题所涉及的几种方案。 2)         半讨论是我希望大家对各个方案都说说自己的见解,更加希...
  • houkai6
  • houkai6
  • 2013年12月31日 13:59
  • 22185

高并发分布式环境中获取全局唯一ID[分布式数据库全局唯一主键生成]

需求说明在过去单机系统中,生成唯一ID比较简单,可以使用mysql的自增主键或者oracle中的sequence, 在现在的大型高并发分布式系统中,以上策略就会有问题了,因为不同的数据库会部署到不同的...

分布式系统唯一ID生成方案汇总

系统唯一ID是我们在设计一个系统的时候常常会遇见的问题,也常常为这个问题而纠结。生成ID的方法有很多,适应不同的场景、需求以及性能要求。所以有些比较复杂的系统会有多个ID生成的策略。下面就介绍一些常见...

分布式系统唯一ID生成方案

分布式系统唯一ID生成方案汇总 系统唯一ID是我们在设计一个系统的时候常常会遇见的问题,也常常为这个问题而纠结。生成ID的方法有很多,适应不同的场景、需求以及性能要求。所以有些比较复杂的系统...

如何用redis来生成唯一Id

在之前的项目中需要用到一个自动增长的主键,该主键需要包含字母,所以没有办法用到数据库的自增主键。楼主要高手的指导下,发现redis的RedisAtomicLong类可以解决这个麻烦。而且redis为单...

【redis】使用redis RedisAtomicLong生成自增的ID值

本文介绍在spring+redis组合时,使用redis的RedisAtomicLong生成自增的ID值。 1、自增ID生成类 RedisSequenceFactory是一个简单封装类,用于使用r...
  • yxtouch
  • yxtouch
  • 2017年06月20日 19:40
  • 2771

如何快速开发一个支持高效、高并发的分布式ID生成器(三)

前面两个ID生成器只是简单的完成功能,如果实际应用到生产环境,则对ID生成器的要求更高,具体包括但不限于以下几点:(1) 产生全局唯一、且单调递增的ID;(2) 任何情况下ID不能重复或者回退;(3)...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:Redis在集群环境中生成唯一ID
举报原因:
原因补充:

(最多只允许输入30个字)