如何在 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++;
}

似乎有两种方法:

  1. 将唯一值存储到所需大小的 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;
    }   
}
  1. 使用具有随机 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;
  } 

所以基本上,这意味着如果你给这个算法在 016777215 之间的任何数字 ( = 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;
}

Here's the full code that I used.

既然您在问题中提到了 Oracle,您会考虑 PL/SQL 解决方案吗?

  1. 创建一个数据库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)
)
  1. 创建一个 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'