订单场景

订单场景

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

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

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

图片来源:

图片来源

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

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

可读性:时间戳+随机数+序列号生成的订单号通常比较短,且包含了时间信息,可以方便地进行人工识别和查询。可控性:时间戳+随机数+序列号生成的订单号中包含了序列号,可以方便地控制其长度和生成规则,以满足不同业务场景下的需求。稳定性:时间戳+随机数+序列号生成的订单号的唯一性依赖于时间戳和序列号的组合,不会因为系统时间异常或者分布式环境下的节点标识冲突等原因导致重复。性能:时间戳+随机数+序列号的生成过程比较简单,不需要复杂的算法和存储结构,因此性能较高。当然,UUID、雪花算法等也有其自身的优势,比如在分布式环境中可以保证全局唯一性,且不需要进行存储等操作。选择何种生成方式需要根据实际业务场景和需求进行权衡和选择。本文主要讲述时间戳+随机数+序列号的方式。

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

基础准备本文主要以Redisson技术栈实现,因此需要引入Redisson,以及配置Redisson、Redis相关服务。

代码语言:javascript代码运行次数:0运行复制

8

3.8.2

org.springframework.boot

spring-boot-starter-data-redis

org.redisson

redisson

${redisson.version}

true

org.redisson

redisson-spring-boot-starter

${redisson.version}

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

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

代码语言:javascript代码运行次数:0运行复制#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

代码语言:javascript代码运行次数:0运行复制/**

* @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);

}

}实现逻辑代码语言:javascript代码运行次数:0运行复制/**

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

代码语言:javascript代码运行次数:0运行复制/**

* @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 orderNo() {

ResultResponse 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);

}

}测试代码语言:javascript代码运行次数:0运行复制{

"success": true,

"trace": "704acdb4-b694-4cb7-a6a0-aaf034e96eb4",

"code": "OK",

"message": "操作成功",

"data": "231201cwmRZKTPPySiJvJvgjzeb1121"

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

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

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

高并发情况: 如果在高并发的情况下,有多个客户端同时进行 INCR 操作,虽然 Redis 能够保证原子性,但是可能会因为竞争而导致性能问题,此时可以使用分布式锁确保每次只有一个客户端进行递增操作。多实例环境(集群): 如果 Redis 是在多个实例之间进行数据共享和同步的情况下,可以考虑使用分布式锁来保证不同实例之间的递增操作的顺序一致性。需要注意的是,使用分布式锁会增加系统的复杂度和开销,可能会影响系统的性能和可用性。因此,在决定是否使用分布式锁时,需要综合考虑系统的实际情况、性能要求和可用性需求。

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

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

本文Redisson相关配置参考:

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

后续内容文章持续更新中…近期发布。

关于我

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

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

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

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

相关推荐

韩国队庆祝连续11次晋级世界杯
bt365官网是多少

韩国队庆祝连续11次晋级世界杯

📅 06-27 👁️ 716
(冫+良)是什么字?
bt365官网是多少

(冫+良)是什么字?

📅 06-28 👁️ 3254
最全自测 | 丁丁的长度、硬度、持久度,你达标了吗?
365体育平台bet下载入口

最全自测 | 丁丁的长度、硬度、持久度,你达标了吗?

📅 06-27 👁️ 7682