文章内容
一、项目需求
批量生成16位由26个英文字母和10数字组成的电子卡密,字母区分大小写,且卡密具有唯一性。
二、技术方案分析
如何确保生成的卡密具有唯一性?这个是技术实现关键点。
技术实现方案:
- 使用apache common lang3包的随机字符串工具类org.apache.commons.lang3.RandomStringUtils
- 使用HashSet集合去重,保证卡密的唯一性
总的来说,比较适合一次生成卡密数在千万量级一下的,卡密重复率为0,性能也可接受。
以下场景不适合,请慎用:
- 卡密长度过短,比如低于8位;
- 超过2千万量级性能急剧下降。
三、方案验证
接下来将从卡密重复率及性能方面进行测试验证,验证方案:
- 测试5论。
- 分别以生成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在出现了性能瓶颈,所以还是建议以单线程为宜。