背景
在许多系统中,需要按照一定的要求生成唯一且递增的流水号。流水号通常由特定的前缀、当前日期以及一个递增的数字组成,以便于唯一性和追踪。在单机环境下生成流水号相对简单,但在多服务器环境或高并发场景下,保证流水号的唯一性和递增性可能会带来额外的挑战。
1. 单机场景下生成流水号
1.1 代码实现
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.atomic.AtomicInteger;
/**
- 流水号生成器。
*
- <p>
- 单机环境下的线程安全流水号生成器,基于日期和计数器生成唯一流水号。
- </p>
*
- @author wangguangwu
*/
public final class RequestKeyNoGenerateUtil {
private static final String PREFIX = "REQ_";
private static final String DATE_FORMAT = "yyyyMMdd";
private static final AtomicInteger COUNTER = new AtomicInteger(1);
private static String currentDate = LocalDate.now().format(DateTimeFormatter.ofPattern(DATE_FORMAT));
private RequestKeyNoGenerateUtil() {
}
/**
- 生成唯一的流水号
*
- @return 生成的流水号
*/
public static synchronized String generateRequestKeyNo() {
String today = LocalDate.now().format(DateTimeFormatter.ofPattern(DATE_FORMAT));
// 如果日期变更,重置计数器
if (!today.equals(currentDate)) {
currentDate = today;
COUNTER.set(1);
}
// 获取当前计数器的值并格式化为五位数
String formattedNumber = String.format("%05d", COUNTER.getAndIncrement());
// 拼接生成的流水号
return PREFIX + today + formattedNumber;
}
}
1.2 代码说明
- AtomicInteger COUNTER:使用 AtomicInteger 保证计数器的原子性递增,避免多线程环境下的竞争条件。
- synchronized 关键字:将 generateRequestKeyNo 方法声明为 synchronized,确保同一时间只有一个线程能够执行该方法,从而避免并发问题。
- 日期变更检查:每次生成流水号时,检查当前日期与保存的日期是否一致,如果不一致,则重置计数器,使新的一天从 "00001" 开始。
- 格式化流水号:将计数器的值格式化为五位数,确保流水号始终是五位数递增的格式。
1.3 优点
- 线程安全:使用 synchronized 关键字和 AtomicInteger,确保在单机环境下,生成的流水号是线程安全的。
- 避免重置问题:通过检查日期变化,确保每天的流水号从 “00001” 开始递增。
- 简单易用:不依赖外部系统(如 Redis),适合单机环境。
1.4 缺点
- 多服务器场景下的线程安全问题:在多服务器环境中,单机的 synchronized 无法保证跨服务器的唯一性和递增性。
- 服务器重启后的记录丢失问题:当前计数器保存在内存中,服务器重启会导致计数器重置,无法保持连续性。
- 性能问题:synchronized 关键字可能会导致性能瓶颈,尤其是在高并发场景下,所有的请求都被阻塞等待锁释放。
1.5 优化点
针对上述缺点,可以从以下点进行优化:
- 多服务器场景下的线程安全:使用分布式锁(如 Zookeeper、Redis 分布式锁)或基于数据库的序列生成策略,确保多服务器环境中的唯一性和顺序性。
- 服务器重启后的记录持久化:将当前日期和计数器状态持久化到外部存储(如数据库或 Redis)中,以便在服务器重启后恢复记录状态。
- 性能优化:使用 ReentrantLock 或无锁算法(如 AtomicReference 和 compareAndSet)来代替 synchronized,提高并发性能。
2. 单机场景下的并发性能优化
在单机环境下,synchronized
关键字虽然能够保证线程安全,但它可能会导致性能瓶颈,因为它会阻塞其他线程的访问。为了在多线程环境中提高性能,可以使用 ReentrantLock
或无锁的 CAS(Compare-And-Swap)操作来代替 synchronized
。
2.1 使用 ReentrantLock
ReentrantLock
提供了更灵活的锁机制,例如可中断锁、尝试获取锁、定时锁等,它能够避免 synchronized
带来的线程阻塞问题。
2.1.1 代码实现
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
public final class RequestKeyNoGenerateUtil {
private static final String PREFIX = "REQ_";
private static final String DATE_FORMAT = "yyyyMMdd";
private static final AtomicInteger COUNTER = new AtomicInteger(1);
private static String currentDate = LocalDate.now().format(DateTimeFormatter.ofPattern(DATE_FORMAT));
private static final ReentrantLock lock = new ReentrantLock();
private RequestKeyNoGenerateUtil() {
}
public static String generateRequestKeyNo() {
lock.lock();
try {
String today = LocalDate.now().format(DateTimeFormatter.ofPattern(DATE_FORMAT));
// 如果日期变更,重置计数器
if (!today.equals(currentDate)) {
currentDate = today;
COUNTER.set(1);
}
// 获取当前计数器的值并格式化为五位数
String formattedNumber = String.format("%05d", COUNTER.getAndIncrement());
// 拼接生成的流水号
return PREFIX + today + formattedNumber;
} finally {
lock.unlock();
}
}
}
2.1.2 优点
- 更灵活的锁机制:
ReentrantLock
可以设置为可中断锁,支持尝试获取锁、定时锁等操作,提高系统的响应能力。 - 性能更高:相比于
synchronized
,ReentrantLock
能更细粒度地控制锁的使用,避免不必要的阻塞。
2.2 使用 CAS
CAS(Compare-And-Swap)是一种无锁的算法,能够通过原子操作来更新计数器,避免了锁竞争带来的性能问题。
CAS 的核心是通过硬件支持的原子指令来比较和更新内存中的值。
2.2.1 代码实现
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
public final class RequestKeyNoGenerateUtil {
private static final String PREFIX = "REQ_";
private static final String DATE_FORMAT = "yyyyMMdd";
private static final AtomicInteger COUNTER = new AtomicInteger(1);
private static final AtomicReference<String> currentDateRef = new AtomicReference<>(LocalDate.now().format(DateTimeFormatter.ofPattern(DATE_FORMAT)));
private RequestKeyNoGenerateUtil() {
}
public static String generateRequestKeyNo() {
String today = LocalDate.now().format(DateTimeFormatter.ofPattern(DATE_FORMAT));
String previousDate = currentDateRef.get();
// 无锁检查日期变更
if (!today.equals(previousDate) && currentDateRef.compareAndSet(previousDate, today)) {
// 日期变化且成功更新为新日期,重置计数器
COUNTER.set(1);
}
// 获取当前计数器的值并格式化为五位数
String formattedNumber = String.format("%05d", COUNTER.getAndIncrement());
// 拼接生成的流水号
return PREFIX + today + formattedNumber;
}
}
2.2.2 优点
- 无锁性能优化:CAS 是无锁的原子操作,可以减少锁竞争带来的开销,提高系统并发性能。
- 更高效的并发支持:适合高并发场景下的流水号生成,性能优于基于锁的方案。
3. 多服务器场景下使用 redis
在多服务器或分布式环境中,需要确保流水号的唯一性和递增性,同时需要在服务器重启时恢复记录。使用 Redis 分布式锁和持久化存储可以很好地解决这些问题。
使用 Redis 实现分布式锁和数据持久化:
Redis 提供了简单有效的分布式锁机制,通过 SETNX 命令可以实现一个分布式锁。同时,Redis 还可以用来持久化计数器数据,以确保在服务器重启时能够恢复流水号的生成。
3.1 代码实现
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.TimeUnit;
@Component
public class DistributedRequestKeyNoGenerateUtil {
private static final String PREFIX = "REQ_";
private static final String DATE_FORMAT = "yyyyMMdd";
private static final String REDIS_KEY_PREFIX = "request_key_no:";
@Resource
private StringRedisTemplate redisTemplate;
/**
- 生成唯一的流水号
*
- @return 生成的流水号
*/
public String generateSerialNumber() {
String today = LocalDate.now().format(DateTimeFormatter.ofPattern(DATE_FORMAT));
String redisKey = REDIS_KEY_PREFIX + today;
// 使用 Redis 原子操作获取当前计数器的值并自增
Long increment = redisTemplate.opsForValue().increment(redisKey, 1);
// 设置 Redis 键的过期时间为 24 小时
redisTemplate.expire(redisKey, 24, TimeUnit.HOURS);
// 格式化流水号,确保五位数字
String formattedNumber = String.format("%05d", increment);
// 拼接生成的流水号
return PREFIX + today + formattedNumber;
}
}
3.2 代码说明
- Redis 原子操作:使用 Redis 的
INCR
命令来保证计数器的原子性递增。 - 过期时间管理:设置 Redis 键的过期时间为 24 小时,确保每天重新生成计数器。
- 数据持久化:Redis 的持久化机制(RDB 或 AOF)确保即使服务器重启,数据也不会丢失。
3.3 优点
- 分布式锁:使用 Redis 实现分布式锁,可以在多服务器环境中确保流水号的唯一性。
- 持久化支持:Redis 提供的数据持久化功能确保数据不丢失,支持服务器重启后的数据恢复。
- 高性能:Redis 原子操作非常高效,能够支持高并发请求。
4. 总结
在单机和多服务器场景下,流水号生成的要求和解决方案各有不同。在单机场景下,可以使用 synchronized
、ReentrantLock
或 CAS 等方式保证线程安全和提高性能。而在多服务器场景下,使用 Redis 分布式锁和持久化存储能够确保流水号的唯一性、递增性和持久性。根据具体的业务需求,可以选择不同的方案来实现高效的流水号生成。