千万级电子卡密生成简单实现

一、项目需求

批量生成16位由26个英文字母和10数字组成的电子卡密,字母区分大小写,且卡密具有唯一性。

二、技术方案分析

如何确保生成的卡密具有唯一性?这个是技术实现关键点。

技术实现方案:

  1. 使用apache common lang3包的随机字符串工具类org.apache.commons.lang3.RandomStringUtils
  2. 使用HashSet集合去重,保证卡密的唯一性

总的来说,比较适合一次生成卡密数在千万量级一下的,卡密重复率为0,性能也可接受。

以下场景不适合,请慎用

  1. 卡密长度过短,比如低于8位;
  2. 超过2千万量级性能急剧下降。

三、方案验证

接下来将从卡密重复率及性能方面进行测试验证,验证方案:

  1. 测试5论。
  2. 分别以生成100万、1000万、2000万条卡密进行测试。

1、批量生成100万条卡密

@Test
public void test(){

    //循环5次
    for (int j = 0; j < 5; j++) {
        long start = System.currentTimeMillis();
        //计划生成电子卡密数量,1百万条
        int totalNum = 1000000;
        Set<String> set = new HashSet<>();

        for (int i = 0; i < totalNum; i++) {
            set.add(RandomStringUtils.random(16,true,true));
        }

        System.out.println("计划生成count="+totalNum+", 实际生成数="+set.size()+", 耗时ms="+(System.currentTimeMillis()-start));
    }
}

测试结果:

计划生成数量与实际生成数量一致,没有生成重发的卡密,耗时不到1秒

2、批量生成1000万条卡密

@Test
public void test(){

    for (int j = 0; j < 5; j++) {
        long start = System.currentTimeMillis();
        //计划生成电子卡密数量,1千万条
        int totalNum = 10000000;
        Set<String> set = new HashSet<>();

        for (int i = 0; i < totalNum; i++) {
            set.add(RandomStringUtils.random(16,true,true));
        }

        System.out.println("计划生成count="+totalNum+", 实际生成数="+set.size()+", 耗时ms="+(System.currentTimeMillis()-start));
    }
}

测试结果:

计划生成数量与实际生成数量一致,没有生成重发的卡密,耗时10秒左右。

3、批量生成2000万条卡密

@Test
public void test(){

    for (int j = 0; j < 5; j++) {
        long start = System.currentTimeMillis();
        //计划生成电子卡密数量,2千万条
        int totalNum = 20000000;
        Set<String> set = new HashSet<>();

        for (int i = 0; i < totalNum; i++) {
            set.add(RandomStringUtils.random(16,true,true));
        }

        System.out.println("计划生成count="+totalNum+", 实际生成数="+set.size()+", 耗时ms="+(System.currentTimeMillis()-start));
    }
}

测试结果:

计划生成数量与实际生成数量一致,没有生成重发的卡密,耗时30秒左右。

四、问题剖析

1、问题一

问题:批量生成的唯一性是有保证的,重复为0,但是万一发生重复呢,如何通过补偿机制,保证能最终生成指定数量的卡密?

解决方案:

在正常生成卡券方法执行完后,对实际生成的卡密数量与计划生成的卡密数量进行比较,若存在差异则进入补偿方法,保证卡券数量最终满足计划生成数量。

补偿机制假定策略:将差异数量 * 100 基数(由于实际测试中未发现有重复的卡券发生,暂时权当100已足够覆盖生成差异数量的卡券)作为要循环的最大次数,待满足计划生成卡券数量时退出循环。当然这个100可以根据情况进行调整,这里不多做讨论。

/**
 * 批量生成电子卡密
 *
 * @param cardLength 电子卡密长度,比如10位,16位等
 * @param totalNum   数量
 */public static Set<String> batchCreation(int cardLength, int totalNum) throws Exception {
// 1.正常
long start = System.currentTimeMillis();
// 使用Set存放电子卡密,防止重复
Set<String> cards = new HashSet<>();

for (int i = 0; i < totalNum; i++) {
cards.add(RandomStringUtils.random(cardLength, true, true));
}

System.out.println("计划生成count=" + totalNum + ", 实际生成数=" + cards.size() + ", 耗时ms=" + (System.currentTimeMillis() - start));

// 2. 处理差异数量的卡券策略,补足差异卡券
appendNotEnough(cards, cardLength, totalNum);
return cards;
}

/**
 * 针对首次批量生成卡券数量不足时的异常处理
 *
 * @param cards
 * @param cardLength
 * @param totalNum
 * @throws Exception
 */private static void appendNotEnough(Set<String> cards, int cardLength, int totalNum) throws Exception {
// 实际生成的卡券数量小于计划生成的卡券数量
if (cards.size() < totalNum) {
int diffCount = totalNum - cards.size();
// 假定策略:将diffCount * 100 作为要循环的最大次数,待满足计划生成卡券数量时退出循环
int diffCountMax = diffCount * 100;

for (int i = 0; i < diffCountMax; i++) {
if (cards.size() == totalNum) {
break;
}

cards.add(RandomStringUtils.random(cardLength, true, true));
}

// 若仍生成失败,则抛出异常
throw new Exception("生成电子卡券失败");
}
}

2、问题二

问题:就算单次生成的卡密是唯一性的了,如何保证生成的卡密在全局卡密数据库是唯一的呢?

解决方案:

要保证全局上卡密是唯一,就要每次批量生成都要有唯一的业务ID或批次号ID作为卡密的一部分(比如前缀或后缀),这样就保证了每次批量生成卡密与历史库中的卡密是不一样的了。

简单来说公式:卡密=批次ID(全局唯一)+随机生成的字符串

这里暂定批次ID为3位,由数字(10个)和字母(区分大小写52个)组成,则批次ID最大个数23万多次(62的3次幂),对于中小规模的卡券平台已经足够了。当然也可以增加到4位,这样批次ID就有1400多万次。

/**
 * 批量生成电子卡密
 *
 * @param uniqueSuffix 自增业务批次号,作为卡密后缀
 * @param cardLength   电子卡密长度,比如10位,16位等
 * @param totalNum     数量
 */public static Set<String> batchCreation2(String uniqueSuffix, int cardLength, int totalNum) throws Exception {
// 1.正常
long start = System.currentTimeMillis();
// 使用Set存放电子卡密,防止重复
Set<String> cards = new HashSet<>();
int randomLength = cardLength - uniqueSuffix.length();

for (int i = 0; i < totalNum; i++) {
// 电子卡密=随机字符串+批次ID后缀 组成
cards.add(RandomStringUtils.random(randomLength, true, true) + uniqueSuffix);
}

System.out.println("计划生成count=" + totalNum + ", 实际生成数=" + cards.size() + ", 耗时ms=" + (System.currentTimeMillis() - start));

// 2. 处理差异数量的卡券策略,补足差异卡券
appendNotEnough(cards, uniqueSuffix, cardLength, totalNum);
return cards;
}

/**
 * 针对首次批量生成卡券数量不足时的异常处理
 *
 * @param cards
 * @param uniqueSuffix 卡密后缀
 * @param cardLength
 * @param totalNum
 * @throws Exception
 */private static void appendNotEnough(Set<String> cards, String uniqueSuffix, int cardLength, int totalNum) throws Exception {
// 实际生成的卡券数量小于计划生成的卡券数量
if (cards.size() < totalNum) {
int diffCount = totalNum - cards.size();
// 假定策略:将diffCount * 100 作为要循环的最大次数,待满足计划生成卡券数量时退出循环
int diffCountMax = diffCount * 100;
int randomLength = cardLength - uniqueSuffix.length();

for (int i = 0; i < diffCountMax; i++) {
if (cards.size() == totalNum) {
 // 满足计划卡券数量后跳出循环
 break;
}

cards.add(RandomStringUtils.random(randomLength, true, true) + uniqueSuffix);
}

// 若仍生成失败,则抛出异常
throw new Exception("生成电子卡券失败");
}
}

3、问题三

问题:卡密数量达到千万级之后,耗时还是相对较长,如何保证性能呢?

解决方案:

对于性能优化问题,无非就是使用线程池了,理论上100万数量卡密生成时间1秒以下,可以把100万分配给一个线程,这样下来基本上生成1000万卡券耗时在1秒上下,性能上也是可接受的。

但是实际使用线程池进行测试后性能反而更慢了,2个线程耗时在15秒左右,5个线程耗时30秒左右,这个与Set在出现了性能瓶颈,所以还是建议以单线程为宜。

发表评论