订单、指定长度随机码生成是业务系统中重要且不可避免的一个需求,往往在电商系统中,业务量、并发量庞大,如何不重复、快速、安全的生成一个订单号成了需要重点考虑的问题。这篇文章我将举一个实际的订单号生成需求,来和大家一起探究基于Redisson实现订单号的生成。
如何避免重复下单? 由于用户误操作多次点击、网络延迟等情况可能会出现用户多次点击提交订单按钮,这样会导致多个相同的创建订单请求到达后端服务,执行订单生成逻辑,数据库中新增多条一致的订单信息,在实际业务场景中,这种情况一定是要极力避免的。
解决思路: 保证用户提交多次相同的数据,产生的结果一致,即:保证订单创建时的接口幂等性。 当生成订单号的逻辑和订单创建、落库逻辑分开,每次点击提交订单时,前端调用单独的生成订单号接口,再拿着生成的订单号去请求订单创建、落库的逻辑,每次生成的订单号都不一致,这样便保证了每次的请求都不是重复的,接下来实现不重复的订单号逻辑即可。
图片来源:
图片来源
不重复订单号生成实现方式有:
时间戳+随机数+序列号相比于UUID、雪花算法的优势主要包括以下几点:
当然,UUID、雪花算法等也有其自身的优势,比如在分布式环境中可以保证全局唯一性,且不需要进行存储等操作。选择何种生成方式需要根据实际业务场景和需求进行权衡和选择。本文主要讲述时间戳+随机数+序列号的方式。
如果您当前团队暂时无法使用Redisson
技术栈时,请自行替换成RedisTemplate
的incr
实现即可。Redisson
并非硬性实现要求,文章更多展示的实现思路,技术栈只是载体。
本文主要以Redisson
技术栈实现,因此需要引入Redisson
,以及配置Redisson
、Redis
相关服务。
<properties>
<java.version>8java.version>
<redisson.version>3.8.2redisson.version>
properties>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.redissongroupId>
<artifactId>redissonartifactId>
<version>${redisson.version}version>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.redissongroupId>
<artifactId>redisson-spring-boot-starterartifactId>
<version>${redisson.version}version>
dependency>
在项目的resources目录下,添加redisson的配置文件。
文件名称、配置按自己需求变更即可。
#Redisson配置
singleServerConfig:
address: "redis://127.0.0.1:6379"
password: 123456
clientName: Geo
#选择使用哪个数据库0~15
database: 7
idleConnectionTimeout: 10000
pingTimeout: 1000
connectTimeout: 10000
timeout: 3000
retryAttempts: 3
retryInterval: 1500
reconnectionTimeout: 3000
failedAttempts: 3
subscriptionsPerConnection: 5
subscriptionConnectionMinimumIdleSize: 1
subscriptionConnectionPoolSize: 50
connectionMinimumIdleSize: 32
connectionPoolSize: 64
dnsMonitoringInterval: 5000
#dnsMonitoring: false
threads: 0
nettyThreads: 0
codec:
class: "org.redisson.codec.JsonJacksonCodec"
transportMode: "NIO"
getResource(“xxx”)中的参数请保持和配置文件一致,如:redisson-config.yml
/**
* @author Liutx
* @since 2023-12-01 10:29
*/
@Slf4j
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redisson() throws IOException {
// 本例子使用的是yaml格式的配置文件,读取使用Config.fromYAML,如果是Json文件,则使用Config.fromJSON
Config config = Config.fromYAML(RedissonConfig.class.getClassLoader().getResource("redisson-config.yml"));
return Redisson.create(config);
}
}
/**
* @author Liutx
* @since 2023-12-01 09:23
*/
@Slf4j
@Component
public class GenOrderCode {
// 创建Redisson客户端实例
private final RedissonClient redissonClient;
public GenOrderCode(RedissonClient redissonClient) {
this.redissonClient = redissonClient;
}
/**
* 生成指定长度的订单号
*
* @param length 订单总长度
* @param prefix 前缀 当天日期-yyMMdd
* @param lockKey 分布式锁id
* @return 订单号
*/
public String genOrderCode(int length, String prefix, String lockKey) {
// 检查参数合法性
if (length <= 0) {
log.warn("获取订单号:订单总长度不能小于0");
throw new RuntimeException("订单总长度或随机码长度不能小于0");
}
if (length <= prefix.length()) {
log.warn("获取订单号:订单总长度长度小于前缀长度");
throw new RuntimeException("订单总长度长度小于前缀长度");
}
// 获取分布式锁
RLock lock = redissonClient.getLock(lockKey);
lock.lock();
try {
// 从Redis中获取递增的序列号
RAtomicLong counter = redissonClient.getAtomicLong("counter");
// 递增计数器
long incrementedValue = counter.incrementAndGet();
String counterValue = String.valueOf(incrementedValue);
int incrLength = counterValue.length();
// 如果前缀长度加数字自增长度大于指定位数,则直接使用自增数据
if (incrLength + prefix.length() > length) {
return prefix + counterValue;
}
// 生成随机码
int randomLength = length - incrLength;
String randomAlphabetic = RandomStringUtils.randomAlphabetic(randomLength);
// 格式化订单号
String orderCode = prefix + randomAlphabetic + counterValue;
log.info("根据规则生成的订单号:{}", orderCode);
return orderCode;
} finally {
// 释放锁
lock.unlock();
}
}
}
在Service层调用,前缀、分布式锁的Key、生成长度均可按需配置,可以通过前缀、分布式锁Key入参不同在不同业务模块复用。
/**
* @author Liutx
* @since 2023-12-01 14:17
*/
@Service
public class OrderServiceImpl implements OrderService {
private final GenOrderCode genOrderCode;
public OrderServiceImpl(GenOrderCode genOrderCode) {
this.genOrderCode = genOrderCode;
}
@Override
public ResultResponse<String> orderNo() {
ResultResponse<String> resultResponse = ResultResponse.newSuccessInstance();
LocalDate currentDate = LocalDate.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyMMdd");
String orderPrefix = currentDate.format(formatter);
//redisKey = Enum + ":" + orderPrefix;=> order:231201,每天的key都不一样,生成的订单号以天区分
String key = "com" + orderPrefix;
String orderCode = genOrderCode.genOrderCode(25, orderPrefix, key);
return resultResponse.succeed().data(orderCode);
}
}
{
"success": true,
"trace": "704acdb4-b694-4cb7-a6a0-aaf034e96eb4",
"code": "OK",
"message": "操作成功",
"data": "231201cwmRZKTPPySiJvJvgjzeb1121"
}
在使用 Redis
的 INCR
命令进行递增操作时,是否需要使用分布式锁锁住这个操作,需要根据具体的场景和需求来进行考虑。
当多个并发请求同时对同一个键执行 INCR
操作时,由于 Redis
是单线程处理命令,所以可以保证 INCR
操作的原子性。这意味着 Redis
会按照收到的请求顺序执行这些递增操作,并且不会发生竞争条件。
然而,在某些特定的场景下,可能仍然需要使用分布式锁来保护 INCR
操作,例如:
INCR
操作,虽然 Redis
能够保证原子性,但是可能会因为竞争而导致性能问题,此时可以使用分布式锁确保每次只有一个客户端进行递增操作。Redis
是在多个实例之间进行数据共享和同步的情况下,可以考虑使用分布式锁来保证不同实例之间的递增操作的顺序一致性。需要注意的是,使用分布式锁会增加系统的复杂度和开销,可能会影响系统的性能和可用性。因此,在决定是否使用分布式锁时,需要综合考虑系统的实际情况、性能要求和可用性需求。
另外,Redis
还提供了一些原子操作来实现类似递增的功能,例如 INCRBY
、INCRBYFLOAT
等命令,可以根据具体的需求选择合适的命令来进行操作。
我们都是站在巨人的肩膀上,感谢他们。
本文Redisson
相关配置参考:
Redis 客户端之Redisson 配置使用(基于Spring Boot 2.x)
近期发布。
关于我
你好,我是Debug.c。微信公众号:种颗代码技术树 的维护者,一个跨专业自学Java,对技术保持热爱的bug猿,同样也是在某二线城市打拼四年余的Java Coder。
在掘金、CSDN、公众号我将分享我最近学习的内容、踩过的坑以及自己对技术的理解。
如果您对我感兴趣,请联系我。
若有收获,就点个赞吧,喜欢原图请私信我。