Redis生成分布式唯一流水号实践
场景
在工作中,想必都接触过这样一个场景:生成具有一定规则的编码。
比如,合同编号。要求格式为<HT前缀><4位年><2位月><2位类型><N位流水号>。
前面都好说,只有这个流水号,很容易就出现重复、跨越等问题。
如何解决呢?其实办法也有好多种,能想到的最多就是加锁。无论是synchronized关键字、还是Lock锁、Zookeeper锁、Redis锁等,都是通过阻塞其它请求,即同步阻塞模式,一次只处理一个流水号生成请求,以达到唯一性目的。
那么有没有同步非阻塞模式呢?答案是有的,且使用起来也比较简单,即采用Redis的自增特性。
配置
首先需要配置Redis链接信息,这里分为单机环境、集群(哨兵模式)环境。这两种环境对于流水号生成并无二致,只是集群环境更能确保流水号生成服务稳定、可靠。
单机配置如下。
spring:
redis:
database: 0
host: localhost
port: 6379
password:
timeout: 1000
集群(哨兵模式)配置如下。
spring:
redis:
database: 0
password:
timeout: 1000
sentinel:
master: mymaster
nodes:
- 192.168.182.131:26379
- 192.168.182.132:26379
- 192.168.182.133:26379
实现
无论是单机环境,还是集群环境,Redis生成流水号的逻辑都并无二致,一模一样,和部署方式没有关系。
关于key
比如合同编号,可以定义为<4位年><2位月><HTCODE>。这里key的定义与自身业务场景有很大关系。举个例子,假设业务规定,流水号以年为单位循环,那么,key的定义最好就只有年和固定后缀,即<4位年><HTCODE>;如果以月未单位循环,那么,key则需要带上月份以区分不同月份的数据,即<4位年><2位月><HTCODE>。
存储形式
可使用string类型存储,也可以使用hash存储,都可以。还是那句话,根据业务场景不同,做不同的适应处理。脱离业务谈实践,就是耍流氓。
string类型存储好理解,那hash存储适用于哪些场景呢?比如,存在这样一个业务场景:系统是多租户的,每个租户都需要生成合同编号,后台需要实时查看所有租户的流水号情况。那么此时,就需要把Redis中所有的流水号信息取出来。
如果要使用string类型存储,那么在key的定义上,势必就要加上租户的标识来区分。然后通过scan也好,循环也好,找到所有租户的流水号信息,比较繁琐。
如果使用hash存储,则只需在value对应的key上,加上租户标识来区分,key值则是统一的<HTCODE>。无论租户使用怎样的流水号生成、循环规则,只需调整其Redis中value对应的key值规则即可。此时,查找Redis中所有的流水号信息则变得异常方便,把此key值hash表的值全部拿到,即找到了所有租户的流水号信息。
所以说,代码实现还是要看具体业务场景,只有业务场景明确了,才能根据具体的业务场景,来做不同的代码实现。
实现
实现非常简单,以string存储类型为例,只需调用 redisTemplate.opsForValue().increment(key, delta) 方法即可。
附上测试用例,其中有单次调用版、高并发版。
注:本例只专注于实现流水号生成,不做具体合同编号按照规则拼装的逻辑。
package cn.com.trade365.redisdemo.util;
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
public class HTCODEGeneratorTest {
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");
}
}
}
执行测试用例,发现确实是高并发逻辑,各流水号生成线程也不是按顺序的,确实是同步非阻塞模式。
查看Redis,indexkey对应的值,刚好就是并发线程数2000。
回复以下关键字,获取更多资源
SpringCloud进阶之路 | Java 基础 | 微服务 | JAVA WEB | JAVA 进阶 | JAVA 面试 | MK 精讲
笔者开通了个人微信公众号【银河架构师】,分享工作、生活过程中的心得体会,填坑指南,技术感悟等内容,会比博客提前更新,欢迎订阅。
上一篇: 网站黑链防范,5招足矣!
推荐阅读
-
一线大厂的分布式唯一ID生成方案是什么样的?
-
Spring Boot分布式系统实践【扩展1】shiro+redis实现session共享、simplesession反序列化失败的问题定位及反思改进
-
通过Zookeeper学习在分布式系统中生成全局唯一ID
-
Redis从入门到高可用,分布式实践 Redis分布式高可用数据
-
Redis生成分布式系统全局唯一ID的实现
-
基于Redis实现分布式单号及分布式ID(自定义规则生成)
-
PHP实现Snowflake生成分布式唯一ID的方法示例
-
使用redis生成唯一编号及原理示例详解
-
分布式系统ID的生成方法之UUID、数据库、算法、Redis、Leaf方案
-
如何使用redis生成唯一编号及原理