如何在 java 中生成 5 个字符的唯一字母数字值?
How to generate a 5 character unique alphanumeric value in java?
我在一个银行项目工作,他们的要求是为每笔交易生成唯一的交易参考。 UTR 的格式为:
<5 digit SequenceId> .
这个 5 位序列 ID 也可以是字母数字。每天的交易量可达 100-200K。
如果我使用 Oracle 序列,那么我只能有 10K 个值。
我尝试使用 SecureRandom
生成器并生成了 200K 5 长度的字符串,但它生成了大约 30 个重复的字符串。
下面是我使用的代码片段
int leftLimit = 48;
int rightLimit = 122;
int i1=0;
Random random = new SecureRandom();
while (i1<200000) {
String generatedString = random.ints(leftLimit, rightLimit+1)
.filter(i -> (i<=57||i>=65) && ( i<=90|| i>=97))
.limit(5)
.collect(StringBuilder::new,
StringBuilder::appendCodePoint,
StringBuilder::append)
.toString();
System.out.println(generatedString);
i1++;
}
似乎有两种方法:
- 将唯一值存储到所需大小的
Set
中,并在使用时删除其元素。
class UniqueIdGenerator {
private static final int CODE_LENGTH = 5;
private static final int RANGE = (int) Math.pow(36, CODE_LENGTH);
private final Random random = new SecureRandom();
private final int initSize;
private final Set<String> memo = new HashSet<>();
public UniqueIdGenerator(int size) {
this.initSize = size;
generate();
}
private void generate() {
int dups = 0;
while (memo.size() < initSize) {
String code = Formatter.padZeros(Integer.toString(random.nextInt(RANGE), 36), CODE_LENGTH);
if (memo.contains(code)) {
dups++;
} else {
memo.add(code);
}
}
System.out.println("Duplicates occurred: " + dups);
}
public String getNext() {
String code = memo.iterator().next();
memo.remove(code);
return code;
}
}
- 使用具有随机
start
和随机增量的序列。
class RandomSequencer {
private static final int CODE_LENGTH = 5;
private Random random = new SecureRandom();
private int start = random.nextInt(100_000);
public String getNext() {
String code = Formatter.padZeros(Integer.toString(start, 36), CODE_LENGTH);
start += random.nextInt(300) + 1;
return code;
}
}
更新
零填充可以通过多种方式实现:
class Formatter {
private static String[] pads = {"", "0", "00", "000", "0000"};
public static String padZeros(String str, int maxLength) {
if (str.length() >= maxLength) {
return str;
}
return pads[maxLength - str.length()] + str;
}
private static final String ZEROS = "0000";
public static String padZeros2(String str, int maxLength) {
if (str.length() >= maxLength) {
return str;
}
return ZEROS.substring(0, maxLength - str.length()) + str;
}
public static String padZeros3(String str, int maxLength) {
if (str.length() >= maxLength) {
return str;
}
return String.format("%1$" + maxLength + "s", str).replace(" ", "0");
}
}
如果您想要 pseudo-random 序列,我建议您使用自定义 Feistel 实现。 Feistel 被设计成一种互惠机制,因此您可以通过重新应用它来解码 Feistel,这意味着 i == feistel(feistel(i))
如果您从 1 到 X,您将获得 1 和 X 之间的所有数字恰好一次。所以没有碰撞。
基本上,您可以使用 36 个字符。所以您有 60,466,176 个可能的值,但您只需要其中的 200,000 个。但实际上,我们不关心你想要多少,因为 Feistel 确保没有碰撞。
你会注意到二进制的 60,466,176 是 0b11100110101010010000000000
,这是一个 26 位数字。 26 对代码不是很友好,所以我们将我们的自定义 feistel 映射器包装为 24 位。 Feistel 必须将一个数字分成两个偶数部分,每个部分将是 12 位。这只是为了解释您将在下面的代码中看到的值,如果您查看其他实现,它是 12 而不是 16。此外,0xFFF
是 12 位的掩码。
现在算法本身:
static int feistel24bits(int value) {
int l1 = (value >> 12) & 0xFFF;
int r1 = value & 0xFFF;
for (int i = 0; i < 3; i++) {
int key = (int)((((1366 * r1 + 150889) % 714025) / 714025d) * 0xFFF);
int l2 = r1;
int r2 = l1 ^ key;
l1 = l2;
r1 = r2;
}
return (r1 << 12) | l1;
}
所以基本上,这意味着如果你给这个算法在 0
和 16777215
之间的任何数字 ( = 224-1),你将得到一个唯一的 pseudo-random 数字,当以 base-36 编写时,该数字可以放入 5 个字符的字符串中。
那么你是如何让它全部工作的呢?嗯,很简单:
String nextId() {
int sequence = (retrieveSequence() + 1) & 0xFFFFFF; // bound to 24 bits
int nextId = feistel24bits(sequence);
storeSequence(sequence);
return intIdToString(nextId);
}
static String intIdToString(int id) {
String str = Integer.toString(id, 36);
while(str.length() < 5) { str = "0" + str; }
return str;
}
既然您在问题中提到了 Oracle,您会考虑 PL/SQL 解决方案吗?
- 创建一个数据库table 来保存您的序列 ID。
create table UTR (
BANK_CODE number(4)
,TXN_DATE_STR char(5)
,SEQUENCE_ID char(5)
,USED_FLAG char(1)
,constraint USED_FLAG_VALID check (USED_FLAG in ('N', 'Y'))
,constraint UTR_PK primary key (BANK_CODE, TXN_DATE_STR, SEQUENCE_ID)
)
- 创建一个 PL/SQL 过程来填充 table。
create or replace procedure POPULATE_UTR
is
L_COUNT number(6);
L_BANK number(4);
L_DATE_STR char(5);
L_SEQUENCE varchar2(5);
begin
L_BANK := 3210;
select to_char(sysdate, 'YYDDD')
into L_DATE_STR
from DUAL;
L_COUNT := 0;
while L_COUNT < 200000 loop
L_SEQUENCE := '';
for K in 1..5 loop
L_SEQUENCE := L_SEQUENCE || substr('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',
mod(abs(dbms_random.random), 62) + 1,
1);
end loop;
begin
insert into UTR values (L_BANK, L_DATE_STR, L_SEQUENCE, 'N');
L_COUNT := L_COUNT + 1;
exception
when dup_val_on_index then
null; -- ignore.
end;
end loop;
end;
请注意,生成序列 ID 的代码来自 this 标题为 Generate Upper and Lowercase Alphanumeric Random String in Oracle
的 SO question
此外,获取日期字符串的代码来自 this 题为 Oracle Julian day of year
的问题
现在,由于数据库 table UTR
中的每一行都包含一个唯一的序列 ID,您可以 select 第一行 USED_FLAG
等于 N
select SEQUENCE_ID
from UTR
where BANK_CODE = 1234 -- i.e. whatever the relevant bank code is
and TXN_DATE_STR = 'whatever is relevant'
and USED_FLAG = 'N'
and rownum < 2
供您参考,如果您想 select 来自 table UTR 的随机行,请参阅标题为 How to get records randomly 的 this SO 问题来自 oracle 数据库?
使用该序列 ID 后,更新 table 并将 USED_FLAG
设置为 Y
,即
update UTR
set USED_FLAG = 'Y'
where BANK_CODE = 1234 -- what you used in the select
and TXN_DATE_STR = 'what you used in the select'
and SEQUENCE_ID = 'what was returned by the select'
我在一个银行项目工作,他们的要求是为每笔交易生成唯一的交易参考。 UTR 的格式为:
这个 5 位序列 ID 也可以是字母数字。每天的交易量可达 100-200K。
如果我使用 Oracle 序列,那么我只能有 10K 个值。
我尝试使用 SecureRandom
生成器并生成了 200K 5 长度的字符串,但它生成了大约 30 个重复的字符串。
下面是我使用的代码片段
int leftLimit = 48;
int rightLimit = 122;
int i1=0;
Random random = new SecureRandom();
while (i1<200000) {
String generatedString = random.ints(leftLimit, rightLimit+1)
.filter(i -> (i<=57||i>=65) && ( i<=90|| i>=97))
.limit(5)
.collect(StringBuilder::new,
StringBuilder::appendCodePoint,
StringBuilder::append)
.toString();
System.out.println(generatedString);
i1++;
}
似乎有两种方法:
- 将唯一值存储到所需大小的
Set
中,并在使用时删除其元素。
class UniqueIdGenerator {
private static final int CODE_LENGTH = 5;
private static final int RANGE = (int) Math.pow(36, CODE_LENGTH);
private final Random random = new SecureRandom();
private final int initSize;
private final Set<String> memo = new HashSet<>();
public UniqueIdGenerator(int size) {
this.initSize = size;
generate();
}
private void generate() {
int dups = 0;
while (memo.size() < initSize) {
String code = Formatter.padZeros(Integer.toString(random.nextInt(RANGE), 36), CODE_LENGTH);
if (memo.contains(code)) {
dups++;
} else {
memo.add(code);
}
}
System.out.println("Duplicates occurred: " + dups);
}
public String getNext() {
String code = memo.iterator().next();
memo.remove(code);
return code;
}
}
- 使用具有随机
start
和随机增量的序列。
class RandomSequencer {
private static final int CODE_LENGTH = 5;
private Random random = new SecureRandom();
private int start = random.nextInt(100_000);
public String getNext() {
String code = Formatter.padZeros(Integer.toString(start, 36), CODE_LENGTH);
start += random.nextInt(300) + 1;
return code;
}
}
更新 零填充可以通过多种方式实现:
class Formatter {
private static String[] pads = {"", "0", "00", "000", "0000"};
public static String padZeros(String str, int maxLength) {
if (str.length() >= maxLength) {
return str;
}
return pads[maxLength - str.length()] + str;
}
private static final String ZEROS = "0000";
public static String padZeros2(String str, int maxLength) {
if (str.length() >= maxLength) {
return str;
}
return ZEROS.substring(0, maxLength - str.length()) + str;
}
public static String padZeros3(String str, int maxLength) {
if (str.length() >= maxLength) {
return str;
}
return String.format("%1$" + maxLength + "s", str).replace(" ", "0");
}
}
如果您想要 pseudo-random 序列,我建议您使用自定义 Feistel 实现。 Feistel 被设计成一种互惠机制,因此您可以通过重新应用它来解码 Feistel,这意味着 i == feistel(feistel(i))
如果您从 1 到 X,您将获得 1 和 X 之间的所有数字恰好一次。所以没有碰撞。
基本上,您可以使用 36 个字符。所以您有 60,466,176 个可能的值,但您只需要其中的 200,000 个。但实际上,我们不关心你想要多少,因为 Feistel 确保没有碰撞。
你会注意到二进制的 60,466,176 是 0b11100110101010010000000000
,这是一个 26 位数字。 26 对代码不是很友好,所以我们将我们的自定义 feistel 映射器包装为 24 位。 Feistel 必须将一个数字分成两个偶数部分,每个部分将是 12 位。这只是为了解释您将在下面的代码中看到的值,如果您查看其他实现,它是 12 而不是 16。此外,0xFFF
是 12 位的掩码。
现在算法本身:
static int feistel24bits(int value) {
int l1 = (value >> 12) & 0xFFF;
int r1 = value & 0xFFF;
for (int i = 0; i < 3; i++) {
int key = (int)((((1366 * r1 + 150889) % 714025) / 714025d) * 0xFFF);
int l2 = r1;
int r2 = l1 ^ key;
l1 = l2;
r1 = r2;
}
return (r1 << 12) | l1;
}
所以基本上,这意味着如果你给这个算法在 0
和 16777215
之间的任何数字 ( = 224-1),你将得到一个唯一的 pseudo-random 数字,当以 base-36 编写时,该数字可以放入 5 个字符的字符串中。
那么你是如何让它全部工作的呢?嗯,很简单:
String nextId() {
int sequence = (retrieveSequence() + 1) & 0xFFFFFF; // bound to 24 bits
int nextId = feistel24bits(sequence);
storeSequence(sequence);
return intIdToString(nextId);
}
static String intIdToString(int id) {
String str = Integer.toString(id, 36);
while(str.length() < 5) { str = "0" + str; }
return str;
}
既然您在问题中提到了 Oracle,您会考虑 PL/SQL 解决方案吗?
- 创建一个数据库table 来保存您的序列 ID。
create table UTR (
BANK_CODE number(4)
,TXN_DATE_STR char(5)
,SEQUENCE_ID char(5)
,USED_FLAG char(1)
,constraint USED_FLAG_VALID check (USED_FLAG in ('N', 'Y'))
,constraint UTR_PK primary key (BANK_CODE, TXN_DATE_STR, SEQUENCE_ID)
)
- 创建一个 PL/SQL 过程来填充 table。
create or replace procedure POPULATE_UTR
is
L_COUNT number(6);
L_BANK number(4);
L_DATE_STR char(5);
L_SEQUENCE varchar2(5);
begin
L_BANK := 3210;
select to_char(sysdate, 'YYDDD')
into L_DATE_STR
from DUAL;
L_COUNT := 0;
while L_COUNT < 200000 loop
L_SEQUENCE := '';
for K in 1..5 loop
L_SEQUENCE := L_SEQUENCE || substr('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',
mod(abs(dbms_random.random), 62) + 1,
1);
end loop;
begin
insert into UTR values (L_BANK, L_DATE_STR, L_SEQUENCE, 'N');
L_COUNT := L_COUNT + 1;
exception
when dup_val_on_index then
null; -- ignore.
end;
end loop;
end;
请注意,生成序列 ID 的代码来自 this 标题为 Generate Upper and Lowercase Alphanumeric Random String in Oracle
的 SO question此外,获取日期字符串的代码来自 this 题为 Oracle Julian day of year
的问题现在,由于数据库 table UTR
中的每一行都包含一个唯一的序列 ID,您可以 select 第一行 USED_FLAG
等于 N
select SEQUENCE_ID
from UTR
where BANK_CODE = 1234 -- i.e. whatever the relevant bank code is
and TXN_DATE_STR = 'whatever is relevant'
and USED_FLAG = 'N'
and rownum < 2
供您参考,如果您想 select 来自 table UTR 的随机行,请参阅标题为 How to get records randomly 的 this SO 问题来自 oracle 数据库?
使用该序列 ID 后,更新 table 并将 USED_FLAG
设置为 Y
,即
update UTR
set USED_FLAG = 'Y'
where BANK_CODE = 1234 -- what you used in the select
and TXN_DATE_STR = 'what you used in the select'
and SEQUENCE_ID = 'what was returned by the select'