高并发redis自增increment使用

本文介绍了如何在SpringBoot应用中使用Redis的自增功能解决并发环境下订单号的唯一性和冲突问题,并探讨了同步锁和双检查锁的优化策略。作者还提到了使用Java的LongAdder或AtomicLong替代Redis的可能性。
部署运行你感兴趣的模型镜像

1.前言

最近修改了一个bug,类型多个用户同时创建订单,需要从数据库获取最大订单号,然后在此基础上为每个订单加1,且不能重复。因为用户可能同时创建订单,导致订单号重复,所以用redis的自增incr,这个像oarcle序列一样不会重复。

2.具体实现

  1. springboot启动类中添加如下代码
	@Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        // 使用GenericJackson2JsonRedisSerializer替换默认序列化
        GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
        // 设置 Key 和 Value 的序列化规则
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        // 初始化 RedisTemplate 序列化完成
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
  1. service层加入依赖
	@Autowired
	RedisTemplate redisTemplate;
  1. 具体取值方法
	public String getMaxID() {
		String maxId = "";
		try {
			synchronized (当前类.class){
				Object redisKey = redisTemplate.opsForValue().get("orderId");
				if (redisKey == null) {
					// 假设此处maxId:"1234"是通过sql查询出来的
					maxId = "1234";
					if (maxId  == "" || maxId == null) {
						maxId = "0001";
						redisTemplate.opsForValue().increment("orderId", 1);
					} else {
						Integer order = Integer.parseInt(maxId);
						redisTemplate.opsForValue().set("orderId", order);
						redisTemplate.opsForValue().increment("orderId", 1);
						Integer value = Integer.parseInt(redisTemplate.opsForValue().get("orderId").toString());
						// 格式化成4位字符串
						maxId= String.format("%04d",value);
					}
				} else {
					redisTemplate.opsForValue().increment("orderId", 1);
					Integer value = Integer.parseInt(redisTemplate.opsForValue().get("orderId").toString());
					// 格式化成4位字符串
					maxId= String.format("%04d",value);
				}
			}
		} catch (Exception e) {
		} finally {
			redisTemplate.expire("orderId", 60, TimeUnit.SECONDS); // 设置过期时间 60秒
		}
		return maxId;
	}

3.后记

  • 这个方法的关键有两点,一是用redis自增,二是锁住当前类,但锁住当前类还有优化空间,应该用双检查锁。
  • 可以使用Java的LongAdder或AtomicLong替换redis来实现自增。

您可能感兴趣的与本文相关的镜像

Llama Factory

Llama Factory

模型微调
LLama-Factory

LLaMA Factory 是一个简单易用且高效的大型语言模型(Large Language Model)训练与微调平台。通过 LLaMA Factory,可以在无需编写任何代码的前提下,在本地完成上百种预训练模型的微调

<think> 我们正在讨论的是基于32位时间戳和Redis序列的分布式ID生成方法。根据引用[1]和[2],这种方案将ID分为两部分:一部分是时间戳(32位),另一部分是Redis生成的序列号(32位)。同时,引用[3]提到了一种结合雪花算法和RedisID分配workerId的方法,但当前问题聚焦于时间戳+Redis序列的方案。 ### 1. 基本原理 该方法的核心思想是利用时间戳保证ID的趋势递,利用Redis的自操作(`INCR`命令)保证同一时间单位(如天、小时)内的序列号唯一性。具体结构如下(参考引用[1][2]): - **时间戳部分**:32位,通常表示一个自定义时间单位(如天数、秒数等)。例如,引用[1]中使用天数(将当前日期转换为整数如`20240115`),而引用[2]中建议使用秒数(从某个固定起点开始计算)。 - **序列号部分**:32位,由Redis的`INCR`命令生成,确保同一时间单位内严格递且不重复。 生成公式: $$ \text{ID} = (\text{timestamp} \ll 32) \mid \text{sequence\_num} $$ 其中: - `<< 32` 表示左移32位,为序列号预留低位空间。 - `|` 表示按位或运算,将两部分拼接为64位整数。 ### 2. 特点与优势 1. **趋势递**:由于时间戳随时间加,生成的ID整体趋势递,有利于数据库索引优化。 2. **局部唯一性**:Redis的`INCR`命令保证同一业务键(`businessKey`)下同一时间单位内的序列号严格递且不重复。 3. **高性能**:Redis单机可支持每秒数万次操作,满足大多数高并发场景。 4. **灵活性**: - 时间单位可调整(如按天、小时、秒),适应不同业务量级(引用[2])。 - 序列号长度可调整(如引用[2]中建议时间戳用31位,序列号用32位)。 ### 3. 实现步骤(以Java+Spring Boot为例) 参考引用[1]的代码,实现流程如下: ```java @Component public class IDGenerator { @Autowired private StringRedisTemplate stringRedisTemplate; public long nextId(String businessKey) { // 1. 生成时间戳部分(32位):取当前日期整数形式(如20240115) long today = Long.parseLong(LocalDate.now().toString().replace("-", "")); // 2. 生成序列号部分(32位):使用Redis操作 Long sequence = stringRedisTemplate.opsForValue().increment(businessKey + "_id_key"); // 3. 组合ID:时间戳左移32位后与序列号拼接 return (today << 32) | sequence; } } ``` **关键细节**: - **时间戳单位**:示例使用天数(`yyyyMMdd`格式),但可按需改为秒级时间戳(如`System.currentTimeMillis()/1000`)。 - **Redis键设计**:每个`businessKey`对应独立的序列号空间(如订单业务用`order_id_key`,用户业务用`user_id_key`),避免不同业务间序列号冲突。 - **序列号重置**:由于时间戳每天变化,序列号自然按天重置(如每天最多生成$2^{32} \approx 42.9$亿个ID)。 ### 4. 潜在问题与优化 1. **时间戳回退风险**: - **场景**:服务器时间被手动调整或同步出错。 - **解决方案**:使用NTP服务同步时间;在Redis中存储上次生成ID的时间戳,拒绝回退请求(引用[2])。 2. **Redis单点故障**: - **场景**:Redis宕机导致服务不可用。 - **解决方案**:部署Redis集群(主从复制+哨兵)或使用Redis实例分片(引用[3])。 3. **序列号溢出**: - **场景**:单日ID生成量超过$2^{32}$(42.9亿)。 - **解决方案**:缩短时间单位(如按小时生成时间戳)或扩展序列号位数(需调整位分配方案)。 4. **时钟跳跃优化**: - 引用[3]提到用Redis分配workerId解决雪花算法的workerId冲突问题,但本方案无需workerId,避免了此问题。 ### 5. 性能评估 - **生成速度**:依赖Redis的`INCR`性能(单机约5万~10万次/秒),可通过集群扩展。 - **ID容量**:以天为单位时,每天最多$2^{32}$个ID;若改用秒级时间戳(31位),每秒最多$2^{32}$个ID(理论值,实际受Redis性能限制)。 ### 总结 该方案结合时间戳的有序性和Redis的原子自特性,实现简单且扩展性强,适用于订单号、用户ID等需要趋势递的场景。实际部署需考虑Redis高可用和时钟同步问题。 §§相关问题§§ 1. 如何选择适合的时间戳单位(天/秒/毫秒)? 2. Redis序列在分布式环境下如何保证高可用? 3. 时间戳+Redis方案与雪花算法的适用场景有何差异? 4. 当单日ID生成量超过42亿时,有哪些扩容方案? </think> ### 基于32位时间戳和Redis序列的分布式ID生成方案 该方法结合时间戳的趋势递特性和Redis原子操作的强一致性,实现高效分布式ID生成。以下是详细解析: #### 一、核心原理 1. **ID结构设计**(64位长整型): - **时间戳段(32位)**:采用精简时间戳(如天数或秒数) - 示例:`yyyyMMdd`格式日期整数(20240115)[^1] - 或秒级时间戳(从固定起点计算)[^2] - **序列号段(32位)**:Redis原子自生成 - 单节点每日理论上限 $2^{32} \approx 42.9$ 亿ID [^1] $$ \text{ID} = (\text{timestamp} \ll 32) \mid \text{sequence\_no} $$ 2. **分布式保障机制**: - **时间戳段**:天然保证ID趋势递 - **Redis序列号**:`INCR`命令的原子性确保序列号全局唯一 - **业务键隔离**:不同业务使用独立Redis键(如`order_id_key`),避免序列号冲突 [^1] #### 二、实现方法(Spring Boot示例) ```java @Component public class TimeRedisIDGenerator { @Autowired private StringRedisTemplate stringRedisTemplate; // 日期格式: yyyyMMdd (32位时间戳段) private long getDateSegment() { return Long.parseLong(LocalDate.now().toString().replace("-","")); } // 获取分布式ID public long nextId(String businessKey) { // 1. 生成32位时间戳段(日期整数) long timestampSeg = getDateSegment(); // 2. Redis原子自序列号(32位) Long sequence = stringRedisTemplate.opsForValue() .increment(businessKey + "_id_key"); // 3. 位运算组合ID return (timestampSeg << 32) | sequence; } } ``` [^1] #### 三、关键特性与优势 1. **时间戳优化方案**: | 时间单位 | 位数 | 可用年限 | 适用场景 | |---------|------|---------|---------| | 天 | 32位 | ≈117年 | 低频业务(用户ID)| | 秒 | 31位 | ≈68年 | 高频业务(订单ID)[^2] | ```mermaid graph LR A[时间戳选择] --> B{业务量级} B -->|低并发| C[天级单位] B -->|高并发| D[秒级单位] ``` 2. **核心优势**: - **绝对有序**:时间戳保证ID趋势递 - **零冲突**:Redis原子操作避免重复 - **弹性扩展**:通过`businessKey`支持多业务隔离 - **高性能**:单Redis节点支持5万+/秒ID生成 #### 四、典型问题与解决方案 1. **时间回拨问题**: - **场景**:服务器时间被手动回调 - **解决方案**: ```java // 在Redis存储最近时间戳 String lastDate = stringRedisTemplate.opsForValue() .get("last_timestamp"); if (currentDate < Long.parseLong(lastDate)) { throw new TimeReverseException("系统时间回拨"); } else { stringRedisTemplate.opsForValue() .set("last_timestamp", String.valueOf(currentDate)); } ``` [^2] 2. **序列号溢出问题**: - **场景**:单日ID超42亿 - **解决方案**: - 方案1:缩短时间粒度(改用小时时间戳) - 方案2:扩展序列号空间(需调整位分配) $$ \text{新ID} = (\text{hour\_timestamp} \ll 36) \mid (\text{sequence} \times 16) $$ 3. **Redis高可用保障**: | 方案 | 实现方式 | 适用场景 | |---------------------|----------------------------|--------------| | Redis Cluster | 多分片部署 | 超大规模系统 | | 主从复制+哨兵 | 自动故障转移 | 中型系统 | | 多Redis实例分片 | 按业务键哈希分布到不同实例 | 低成本方案 [^3] | #### 五、适用场景分析 1. **推荐场景**: - 电商订单系统(日均ID量<10亿) - 用户注册系统(需趋势递ID) - 日志追踪流水号 2. **慎用场景**: - 超高频系统(如物联网设备,ID量>1万/秒) - 强单调递要求(需改用雪花算法变体) > **对比传统雪花算法**:此方案避免workerId分配问题[^3],但依赖Redis存储,需权衡可用性与复杂度。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值