zoukankan      html  css  js  c++  java
  • 使用Redis实现高并发分布式序列号生成服务

    序列号的构成

    为建立良好的数据治理方案,作数据掌握、分析、统计、商业智能等用途,业务数据的编码制定通常都会遵循一定的规则,一般来讲,都会有自己的编码规则和自增序列构成。比如我们常见的身份证号、银行卡号、社保电脑号等等。

    以某公司产品标识码(代表该产品的唯一编码)的构成为例:

    规则定义:商品款号(8位)+颜色号(3位)+号型码(3位) (共14位)

    其标识码为:62X19001 001 46A 

    业务含义为: 2009年男装秋冬季仿毛套西黑色170A版 

    简单来讲,业务编码是由规则序列构成,规则是允许定义和编辑的,序列通常要求并发安全。整个序列号生成规则要求读写并发安全。

    序列号生成方案

    由于Redis的高性能,高并发和数据一致性的保证,以及断电数据不丢失,分布式扩展能力等优势。我们采用Redis存储并持久化序列和业务规则来配置和管理整个序列号的产生。

    规则定义举例:前缀+时间(YYYYMMDD)+所使用的序列(指定长度),那么产生的序列号类似于SO20150520000124

    具体规则可根据实际业务需求来设计。

    实现要求:整个生成过程使用Jedis完成,保证原子事务。并通过压力测试。

    尽管规则的配置更适合使用表结构来存储,但为了构建分布式的数据库集群,通常都会采用分库和分表(分片)的方式,在这种要求下,我们将规则的配置和序列都放在Redis,以便于提供独立的全局序列生成服务.而不用担心数据库伸缩带来的影响.

    序列号生成服务部署图

    序列号实现方案

    1.         规则配置管理

    在Redis的设计中,要想实现比如

    select * from users where user.location="shanghai"

    这样的查询,是没办法通过value进行比较得出结果的。但是可以通过不同的数据结构类型组合来做到这一点。比如如下的数据定义

    users:1 {name:Jack,age:28,location:shanghai}

    users:2 {name:Frank,age:30,location:beijing}

    users:location:shanghai [1]

    其中users:1 users:2 分别定义了两个用户信息,通过Redis中的hash数据结构,而users:location:shanghai 记录了所有上海的用户id,通过集合数据结构实现。

    这样通过两次简单的Redis命令调用就可以实现我们上面的查询。

    Jedis jedis = jedisPool.getResource();

    Set<String> shanghaiIDs = jedis.smembers("users:location:shanghai"); //遍历该set

     //... //通过hgetall获取对应的user信息

    jedis.hgetAll("users:" + shanghaiIDs[0]);

    通过诸如以上的设计,可以实现简单的条件查询。但是这样的问题也很多,首先需要多维护一个ID索引的集合,其次对于一些复杂查询无能为力(当然也不能期望Redis实现像关系数据库那样的查询,Redis不是干这的)。针对本序列号生成方案,这种方式完全是够用的,可以直接参考本节的代码示例。

    如果想更进一步,Redis2.6集成了Lua(Redis是用ANSI C写的,可以想象支持Lua是一件很自然的事),可以通过eval命令,直接在RedisServer环境中执行Lua脚本,就是说可以让你用Lua脚本,对Redis中存储的key value进行操作,这个意义就大了,甚至可以将系统所需的各种业务写成一个个lua脚本,提前加载进入Redis,然后对于请求的响应,只需要调用一个个lua脚本就行。(当然这些操作也完全可以使用Jedis来完成,但显然lua效率更高)

    比如,现在我们要实现一个‘所有年龄(age)大于28岁的用户(user)’这样一个查询,那么通过以下的Lua脚本就可以实现

    public static final String SCRIPT = "local resultKeys={};"

     + "for k,v in ipairs(KEYS) do "

     + "  local tmp = redis.call('hget', v, 'age');"

    + "  if tmp > ARGV[1] then "

    + "   table.insert(resultKeys,v);"

     + "  end;"

     + " end;"

     + "return resultKeys;";

    执行脚本代码

    Jedis jedis = jedisPool.getResource();

    jedis.auth(auth);

    List<String> keys = Arrays.asList(allUserKeys);

    List<String> args = new ArrayList<>();

    args.add("28");

    List<String> resultKeys = (List<String>)jedis.evalsha(funcKey, keys, args);

    return resultKeys;

    注意,以上的代码中使用的是evalsha命令,该命令参数的不是直接用Lua脚本字符串,而是提前已经加载到Redis中的函数的一个SHA索引,通过以下的代码将系统中所有需要执行的函数提前加载到Redis中,通常在自己的系统中维护一个函数哈希表funcTable,后续需要实现什么功能,就从函数表中获取对应功能的SHA索引,通过evalsha调用就行。

    String shaFuncKey = jedis.scriptLoad(SCRIPT);//加载脚本到Redis中,获取sha索引 funcTable.put(funcName_age, shaFuncKey);//添加到系统维护的函数表中

    通过以上的方法,便可以使较为复杂的查询放到Redis中去执行,提高效率。

    可见,想要将全部业务代码都使用lua脚本来实现的业务系统是可能的,lua脚本等同于关系型数据库中的存储过程或者函数。当然,全部使用lua的开发成本未必不大,毕竟不是关系型数据库,存储思维不同。

    代码示例:

    //配置生成规则(CRUD):

    //假设销售单号生成规则:prefix+time+seq

    //生成之后的结果类似于:SO20150520023014

    //------模拟常规数据库操作------

    //添加数据

    shardedJedis.hset("rules", "somaster",

     "name:销售单号,prefix:SO,time:YYYYMMDD,seq:seq_so,seq_len:6");

    shardedJedis.hset("rules", "pomaster",

     "name:采购单号,prefix:PO,time:YYYYMMDD,seq:seq_po,seq_len:6");

    shardedJedis.hset("rules", "test", "name:test,prefix:PO,time:YYYYMMDD,seq:seq_po,seq_len:6");

    //判断某个值是否存在

    System.out.println(shardedJedis.hexists("rules", "test"));

    // 删除指定的值

    System.out.println(shardedJedis.hdel("rules", "test"));

    // 获取指定的值

    System.out.println(shardedJedis.hget("rules", "somaster"));

    // 获取所有的keys

    System.out.println(shardedJedis.hkeys("rules"));

    // 获取所有的values

    System.out.println(shardedJedis.hvals("rules"));

    //更新 = 插入同名的key

    System.out.println("update before:"+shardedJedis.hvals("rules"));

    System.out.println(shardedJedis.hset("rules", "somaster", "new test somaster"));

    System.out.println("update after:"+shardedJedis.hvals("rules"));

     

    我示例代码中使用的是hash而不是直接使用key-value来存储,是更优的方案。至此CRUD都能直接满足了,最后,你获取到所有values,需要自己处理分页。

    也可以使用list和set组合的方式存储。这种方式是将list index和set key对应起来,根据序号进行分页是容易的,但在每次新增和删除时,都需要修改序号和key的对应关系。

    两者相比,使用hash的成本显然更低,也不易出错。

    2.         序列号的使用

    Redis中对序列的生成早已考虑周到,使用单线程操作序列的方式以保证并发安全,同时,使用也极其简单。更多操作详见官网API

    代码示例

    //sequence

    System.out.println("seq:"+shardedJedis.incr("seq"));

    System.out.println("seq:"+shardedJedis.incr("seq"));

    System.out.println("another_seq:"+shardedJedis.incr("another_seq"));

    最后,生成序列服务只需要通过对应的规则名,获取规则表达式,解析之后结合序列号,最终生成即可。

    并发测试

    这里我们使用CyclicBarrier做并发测试,CyclicBarrier会开启指定数量的线程,等待这些线程就绪之后,同时执行测试内容,以达到真实并发的测试目的。

    Loadrunner等压力测试工具也能完成测试任务。

    public class CyclicBarrierTest {
    	//初始化
    	JedisPoolTest test = new JedisPoolTest();
        public static void main(String[] args) {
             int count = 1000;
             CyclicBarrier cyclicBarrier = new CyclicBarrier(count);
             ExecutorService executorService = Executors.newFixedThreadPool(count);
             for (int i = 0; i < count; i++)
                  executorService.execute(new CyclicBarrierTest().new Task(cyclicBarrier));
    
             executorService.shutdown();
             while (!executorService.isTerminated()) {
                  try {
                       Thread.sleep(10);
                  } catch (InterruptedException e) {
                       e.printStackTrace();
                  }
             }
        }
    
        public class Task implements Runnable {
             private CyclicBarrier cyclicBarrier;
    
             public Task(CyclicBarrier cyclicBarrier) {
                  this.cyclicBarrier = cyclicBarrier;
             }
    
             @Override
             public void run() {
                  try {
                       // 等待所有任务准备就绪
                       cyclicBarrier.await();
                       // 测试内容
                    // 待测试的url
    					String host = "http://172.25.2.14/seqno?";
    					String para = "sysTemNo=ERP&seqName=WH-ZONE-ID&iVar=00";
    					System.out.println(host + para);
    					URL url = new URL(host);
    					HttpURLConnection connection = (HttpURLConnection) url.openConnection();
    					// connection.setRequestMethod("POST");
    					// connection.setRequestProperty("Proxy-Connection", "Keep-Alive");
    					connection.setDoOutput(true);
    					connection.setDoInput(true);
    					PrintWriter out = new PrintWriter(connection.getOutputStream());
    					out.print(para);
    					out.flush();
    					out.close();
    					BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
    					String line = "";
    					String result = "";
    					while ((line = in.readLine()) != null) {
    						result += line;
    					}
    					System.out.println(result);
    //                   System.out.println(test.getJedis().incr("seq"));
    //             	  System.out.println(test.getShardedJedis().incr("seq"));
                  } catch (Exception e) {
                       e.printStackTrace();
                  }
             }
        }
    }
    

    测试结果:

    单台Redis未经任何设置,500并发100% pass,到1000并发时只有67%pass率,此时存在连接超时和被拒的情形。但不存在任何重复号码或丢失号码。500并发数其实已经完全满足我当前系统的要求。考虑到Redis本身可以集群扩展,完全能够应对将来更高的并发需求。  

  • 相关阅读:
    克如斯卡尔 P1546
    真正的spfa
    第四课 最小生成树 要点
    关于vscode中nullptr未定义
    cmake学习笔记
    python学习笔记
    (BFS 图的遍历) 2906. kotori和迷宫
    (图论基础题) leetcode 997. Find the Town Judge
    (BFS DFS 并查集) leetcode 547. Friend Circles
    (BFS DFS 图的遍历) leetcode 841. Keys and Rooms
  • 原文地址:https://www.cnblogs.com/mignet/p/Redis_Concurrent_generate_sequence_no.html
Copyright © 2011-2022 走看看