Redis流水号生成


一、场景:生成具有一定规则的编码。

在工作中,想必都接触过这样一个场景:生成具有一定规则的编码。

比如,合同编号。要求格式为<HT前缀><4位年><2位月><2位类型><N位流水号>。

前面都好说,只有这个流水号,很容易就出现重复、跨越等问题。
如何解决呢?其实办法也有好多种,能想到的最多就是加锁。无论是synchronized关键字、还是Lock锁、Zookeeper锁、Redis锁等,都是通过阻塞其它请求,即同步阻塞模式,一次只处理一个流水号生成请求,以达到唯一性目的。
那么有没有同步非阻塞模式呢?答案是有的,且使用起来也比较简单,即采用Redis的自增特性。

二、技术方案:利用Redis的原子操作incr命令

Redis Incr 命令将 key 中储存的数字值增一。
如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作。
如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。
本操作的值限制在 64 位(bit)有符号数字表示之内。

127.0.0.1:6379> set num 10
OK
127.0.0.1:6379> incr num
(integer) 11
127.0.0.1:6379> get num    # 数字值在 Redis 中以字符串的形式保存
"11"

二、存储形式

可使用string类型存储,也可以使用hash存储,都可以。还是那句话,根据业务场景不同,做不同的适应处理。脱离业务谈实践,就是耍流氓。

string类型存储好理解,那hash存储适用于哪些场景呢?比如,存在这样一个业务场景:系统是多租户的,每个租户都需要生成合同编号,后台需要实时查看所有租户的流水号情况。那么此时,就需要把Redis中所有的流水号信息取出来。

如果要使用string类型存储,那么在key的定义上,势必就要加上租户的标识来区分。然后通过scan也好,循环也好,找到所有租户的流水号信息,比较繁琐。

如果使用hash存储,则只需在value对应的key上,加上租户标识来区分,key值则是统一的。无论租户使用怎样的流水号生成、循环规则,只需调整其Redis中value对应的key值规则即可。此时,查找Redis中所有的流水号信息则变得异常方便,把此key值hash表的值全部拿到,即找到了所有租户的流水号信息。

所以说,代码实现还是要看具体业务场景,只有业务场景明确了,才能根据具体的业务场景,来做不同的代码实现。

三、关于key

比如合同编号,可以定义为<4位年><2位月>。这里key的定义与自身业务场景有很大关系。举个例子,假设业务规定,流水号以年为单位循环,那么,key的定义最好就只有年和固定后缀,即<4位年>;如果以月未单位循环,那么,key则需要带上月份以区分不同月份的数据,即<4位年><2位月>

四、实现

实现非常简单,以string存储类型为例,只需调用 redisTemplate.opsForValue().increment(key, delta) 方法即可。
注:本例只专注于实现流水号生成,不做具体合同编号按照规则拼装的逻辑。

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class CodeGeneratorTest {

    private CountDownLatch cd = new CountDownLatch(2001);

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    private AtomicInteger atomicInteger = new AtomicInteger(0);

    @Test
    public void single() {
        System.out.println(this.redisTemplate.opsForValue().increment("index", 1));
    }

    @Test
    public void concurrent() {
        // 两千个线程,等待全部创建完成后,再同时执行
        for (int i = 0; i < 2000; i++) {

            new Thread(() -> {
                try {
                    // 当前线程阻塞等待
                    cd.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println(this.redisTemplate.opsForValue().increment("index", 1));
            }).start();


            cd.countDown();
        }

        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                cd.countDown();
            }
        }, 3000L);

        try {
            cd.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        long currentTimeMillis = System.currentTimeMillis();

        System.out.println("当前用时:" + (System.currentTimeMillis() - currentTimeMillis));

        int index = (int) this.redisTemplate.opsForValue().get("index");

        while (index % 2000 != 0) {
            index = (int) this.redisTemplate.opsForValue().get("index");
        }
    }


}

文章作者: niww
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 niww !
 上一篇
CentOS下的运维操作 CentOS下的运维操作
Java Centos7部署jar包开机自启MySQL Centos7下mysql大小写敏感问题Redis Centos7下配置Redis开机自启动 document.querySelectorAll('.github-
2020-11-09
下一篇 
RS485通信协议(二) RS485通信协议(二)
RS485通信协议RS-485总线技术只是规定了接口的电气标准,并没有规定RS-485接口的电缆,插件以及通信协议,只是OSI规范中物理层的一个标准,由于RS-485总线采用差分平衡传输方式,一般使用的电缆建议采用屏蔽双绞线,使得485+与
2020-07-06
  目录