【问题标题】:How to generate a 5 character unique alphanumeric value in java?如何在 java 中生成 5 个字符的唯一字母数字值?
【发布时间】:2021-01-12 09:54:21
【问题描述】:

我为一个银行项目工作,他们的要求是为每笔交易生成唯一的交易参考。 UTR 的格式为:

.

这个 5 位序列 ID 也可以是字母数字。每天的交易量最高可达 100-200K。

如果我使用 Oracle 序列,那么我只能有 10K 个值。

我尝试使用SecureRandom 生成器并生成了 200K 5 长度的字符串,但它生成了大约 30 个重复的字符串。

下面是我使用的代码sn-p

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++;
}

【问题讨论】:

  • “这个 5 位 seqid 可以是字母数字” --- 这是什么意思?每个位置可以是0-9 还是A-Za-z 是否也被允许并被认为与 A-Z 不同?允许使用其他字符吗? --- 如果只允许 0-9A-Z,那将是 36 个不同的字符,这意味着一个 Base-36 数字。在 Base-36 中有 5 个“数字”,您可以有 36^5 = 60466176 个不同的值。 --- 仅供参考: Java Integer 类可以格式化 Base-36 数字。
  • 以 36 为基数计数,而不是使用随机数。
  • 嗯......结果是预期的。这是birthday problem 的变体。总共,您有36^5 可能的密码。提取 100k 个不重复密码的概率是((36!) / (36! - 100000)!) / ((36^5)^100000) ~ 1.70 * 10^-36,又名。不太可能。
  • 如果你想要一个序列,为什么要使用随机?
  • 是的,但序列不是确保每个值唯一的最简单方法吗?当然,除非你有其他要求。

标签: java uniqueidentifier


【解决方案1】:

似乎有两种方法:

  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");
    }
}

【讨论】:

  • 我可以看到string modificatino 在循环中。这很糟糕。
  • @oleg.cherednik 超级高效的填充零不是这个答案的重点
  • 反正不应该。
【解决方案2】:

如果你想要一个伪随机序列,我建议你使用自定义的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),你会得到一个唯一的伪随机数以 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.

【讨论】:

    【解决方案3】:

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

    1. 创建一个数据库表来保存您的序列 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 过程来填充表。
    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 SO question,标题为Generate Upper and Lowercase Alphanumeric Random String in Oracle

    另外,获取日期字符串的代码来自this SO问题,题为Oracle Julian day of year

    现在,由于数据库表UTR 中的每一行都包含一个唯一的序列ID,因此您可以选择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
    

    关于您的信息,如果您想从表 UTR 中选择随机行,请参阅标题为 如何从 oracle 数据库中随机获取记录?

    this SO 问题

    使用该序列 ID 后,更新表并将 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'
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-02-17
      • 1970-01-01
      • 2017-12-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多