订单场景-基于Redisson实现订单号生成

订单、指定长度随机码生成是业务系统中重要且不可避免的一个需求,往往在电商系统中,业务量、并发量庞大,如何不重复、快速、安全的生成一个订单号成了需要重点考虑的问题。这篇文章我将举一个实际的订单号生成需求,来和大家一起探究基于Redisson实现订单号的生成。

业务场景

如何避免重复下单? 由于用户误操作多次点击、网络延迟等情况可能会出现用户多次点击提交订单按钮,这样会导致多个相同 的创建订单请求到达后端服务,执行订单生成逻辑,数据库中新增多条一致的订单信息,在实际业务场景中,这种情况一定是要极力避免的。

解决思路: 保证用户提交多次相同的数据,产生的结果一致,即:保证订单创建时的接口幂等性。 当生成订单号的逻辑和订单创建、落库逻辑分开,每次点击提交订单时,前端调用单独的生成订单号接口,再拿着生成的订单号去请求订单创建、落库的逻辑,每次生成的订单号都不一致,这样便保证了每次的请求都不是重复的,接下来实现不重复的订单号逻辑即可。

图片来源:

图片来源

不重复订单号生成

不重复订单号生成实现方式有:

  • UUID
  • 雪花算法
  • 时间戳+随机数+序列号

时间戳+随机数+序列号相比于UUID、雪花算法的优势主要包括以下几点:

  1. 可读性:时间戳+随机数+序列号生成的订单号通常比较短,且包含了时间信息,可以方便地进行人工识别和查询。
  2. 可控性:时间戳+随机数+序列号生成的订单号中包含了序列号,可以方便地控制其长度和生成规则,以满足不同业务场景下的需求。
  3. 稳定性:时间戳+随机数+序列号生成的订单号的唯一性依赖于时间戳和序列号的组合,不会因为系统时间异常或者分布式环境下的节点标识冲突等原因导致重复。
  4. 性能:时间戳+随机数+序列号的生成过程比较简单,不需要复杂的算法和存储结构,因此性能较高。

当然,UUID、雪花算法等也有其自身的优势,比如在分布式环境中可以保证全局唯一性,且不需要进行存储等操作。选择何种生成方式需要根据实际业务场景和需求进行权衡和选择。本文主要讲述时间戳+随机数+序列号的方式。

代码实现

如果您当前团队暂时无法使用Redisson技术栈时,请自行替换成RedisTemplateincr实现即可。Redisson并非硬性实现要求,文章更多展示的实现思路,技术栈只是载体。

基础准备

本文主要以Redisson技术栈实现,因此需要引入Redisson,以及配置RedissonRedis相关服务。

xml 复制代码
  <properties>
      <java.version>8</java.version>
      <redisson.version>3.8.2</redisson.version>
  </properties>
  
  <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-redis</artifactId>
  </dependency>
  <dependency>
      <groupId>org.redisson</groupId>
      <artifactId>redisson</artifactId>
      <version>${redisson.version}</version>
      <optional>true</optional>
  </dependency>
  <dependency>
      <groupId>org.redisson</groupId>
      <artifactId>redisson-spring-boot-starter</artifactId>
      <version>${redisson.version}</version>
  </dependency>

Redission配置

在项目的resources目录下,添加redisson的配置文件。

文件名称、配置按自己需求变更即可。

yaml 复制代码
#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

java 复制代码
/**
 * @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);
    }
}

实现逻辑

java 复制代码
/**
 * @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入参不同在不同业务模块复用。

java 复制代码
/**
 * @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);
	}
}

测试

json 复制代码
{
  "success": true,
  "trace": "704acdb4-b694-4cb7-a6a0-aaf034e96eb4",
  "code": "OK",
  "message": "操作成功",
  "data": "231201cwmRZKTPPySiJvJvgjzeb1121"
}

incr操作为什么使用分布式锁

在使用 RedisINCR 命令进行递增操作时,是否需要使用分布式锁锁住这个操作,需要根据具体的场景和需求来进行考虑。

当多个并发请求同时对同一个键执行 INCR 操作时,由于 Redis 是单线程处理命令,所以可以保证 INCR 操作的原子性。这意味着 Redis 会按照收到的请求顺序执行这些递增操作,并且不会发生竞争条件。

然而,在某些特定的场景下,可能仍然需要使用分布式锁来保护 INCR 操作,例如:

  1. 高并发情况: 如果在高并发的情况下,有多个客户端同时进行 INCR 操作,虽然 Redis 能够保证原子性,但是可能会因为竞争而导致性能问题,此时可以使用分布式锁确保每次只有一个客户端进行递增操作。
  2. 多实例环境(集群): 如果 Redis 是在多个实例之间进行数据共享和同步的情况下,可以考虑使用分布式锁来保证不同实例之间的递增操作的顺序一致性。

需要注意的是,使用分布式锁会增加系统的复杂度和开销,可能会影响系统的性能和可用性。因此,在决定是否使用分布式锁时,需要综合考虑系统的实际情况、性能要求和可用性需求。

另外,Redis 还提供了一些原子操作来实现类似递增的功能,例如 INCRBYINCRBYFLOAT 等命令,可以根据具体的需求选择合适的命令来进行操作。

Reference

我们都是站在巨人的肩膀上,感谢他们。

本文Redisson相关配置参考:

Redis 客户端之Redisson 配置使用(基于Spring Boot 2.x)

后续内容文章持续更新中...

近期发布。


关于我

👋🏻你好,我是Debug.c。微信公众号:种颗代码技术树 的维护者,一个跨专业自学Java,对技术保持热爱的bug猿,同样也是在某二线城市打拼四年余的Java Coder。

🏆在掘金、CSDN、公众号我将分享我最近学习的内容、踩过的坑以及自己对技术的理解。

📞如果您对我感兴趣,请联系我。

若有收获,就点个赞吧,喜欢原图请私信我。

相关推荐
hnlucky12 分钟前
redis 数据类型新手练习系列——Hash类型
数据库·redis·学习·哈希算法
丘山子18 分钟前
一些鲜为人知的 IP 地址怪异写法
前端·后端·tcp/ip
CopyLower42 分钟前
在 Spring Boot 中实现 WebSockets
spring boot·后端·iphone
IT小辉同学1 小时前
Docker如何更换镜像源提高拉取速度
spring cloud·docker·eureka
.生产的驴2 小时前
SpringBoot 封装统一API返回格式对象 标准化开发 请求封装 统一格式处理
java·数据库·spring boot·后端·spring·eclipse·maven
景天科技苑2 小时前
【Rust】Rust中的枚举与模式匹配,原理解析与应用实战
开发语言·后端·rust·match·enum·枚举与模式匹配·rust枚举与模式匹配
AnsenZhu2 小时前
2025年Redis分片存储性能优化指南
数据库·redis·性能优化·分片
追逐时光者2 小时前
MongoDB从入门到实战之Docker快速安装MongoDB
后端·mongodb
方圆想当图灵3 小时前
深入理解 AOP:使用 AspectJ 实现对 Maven 依赖中 Jar 包类的织入
后端·maven
豌豆花下猫3 小时前
Python 潮流周刊#99:如何在生产环境中运行 Python?(摘要)
后端·python·ai