使用Redis实现高并发分布式序列号生成服务

原文地址:http://www.cnblogs.com/mignet/p/Redis_Concurrent_generate_sequence_no.html?utm_source=tuicool&utm_medium=referral

序列号的构成

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

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

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

其标识码为:62X19001 001 46A

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

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

序列号生成方案

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

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

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

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

序列号实现方案

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本身可以集群扩展,完全能够应对将来更高的并发需求。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 在高并发下,可以使用 Redis 的 SETNX 命令来实现分布式锁。SETNX 指令是将 key 的值设为 value,当且仅当 key 不存在。 步骤如下: 1. 使用 SETNX 命令尝试获取锁,如果返回值为 1,则说明获取锁成功。 2. 使用 EXPIRE 命令为锁设置过期时间,防止死锁。 3. 在业务代码执行完成后,使用 DEL 命令释放锁。 为了防止网络延迟等问题,可以在获取锁时设置一个随机值,并在释放锁时判断该值是否与当前锁对应的值相同,以确保只有持有锁的客户端才能释放锁。 ### 回答2: 高并发Redis分布式锁的实现是通过Redis的setnx命令和expire命令来实现的。 首先,通过setnx命令尝试设置一个带有过期时间的key,如果成功设置,则表示获取到了分布式锁。如果设置失败,说明锁已经被其他客户端占用,需要等待或进行重试。 为了防止因为某个客户端处理时间过长而导致锁过期的情况,可以为锁的过期时间设置一个合理的值。在获取到锁后,可以使用expire命令为锁的key设置过期时间,确保在一定时间内释放锁。 为了提高锁的安全性,可以为每个客户端设置一个唯一的ID作为锁的值,并将锁名称与该ID进行绑定。这样可以确保只有获取锁的客户端才能释放锁,防止其他客户端误释放锁。 另外,考虑到高并发情况下的锁竞争,可以在获取锁失败后进行等待一段时间再进行重试,避免频繁的锁竞争对系统性能造成负面影响。 需要注意的是,Redis分布式锁的实现并不能解决所有并发问题,仅适用于单个业务场景下的加锁和释放锁操作。在设计和使用时需要考虑到具体业务需求和场景,并进行适当的优化和调整。 ### 回答3: 在高并发场景下,为了保证数据的一致性和并发执行的正确性,常常需要使用分布式锁来控制对共享资源的访问。Redis作为一个高性能的内存键值存储系统,也可以用来实现分布式锁。 Redis实现分布式锁的一种常见方式是使用SETNX命令。当多个客户端同时尝试获取锁时,只有一个客户端能成功执行SETNX操作,即将对应的key设置为1,表示获取了锁。其他客户端获取锁失败,需要等待锁被释放后重新尝试。 为了保证锁的正确性和防止死锁,还需要为锁设置一个合理的过期时间,以防止获取锁的客户端因为异常情况导致无法及时释放锁。 在高并发环境下,为了提高锁的性能,可以考虑使用红锁机制。红锁是将分布式锁与Redis的复制功能相结合,确保在大部分节点上都获取到锁后,才认为锁已经获得。这样可以避免某个节点出现故障或网络异常导致锁丢失的情况。 另外,为了避免因为程序异常导致锁无法释放的情况,可以在获取到锁之后,使用Lua脚本来保证在一个原子操作中判断锁的状态并释放锁。 在实现分布式锁时,还需要考虑高并发下锁的性能问题。可以通过优化Redis的部署结构、增加Redis的内存和CPU资源,以及使用连接池等方法来提高Redis的性能和并发能力,从而提高分布式锁的性能。 综上所述,高并发下的Redis分布式锁的实现主要包括使用SETNX命令获取锁、设置合理的超时时间、使用红锁机制增强锁的可靠性、使用Lua脚本保证原子操作、优化Redis的性能和并发能力等方面。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值