项目实战,介绍一次生成10万条记录兑换码的功能,由于公司需要生成大批量的兑换码,单次生成的兑换码超过10条记录。本文用于介绍相关设计方案:
1.接口超时的问题;
由于接口的功能是一个前后端交互的处理,生成10万条记录,对于数据库来说,大概需要几分钟的时间,如果使用常规的设计就会导致接口超时,业务同事使用接口会遇到界面死机的情况,比如说生成的界面如果是同步的话,通常情况下,界面是有一个超时时间的,大概是30秒,超过了 30秒之后,就会出现接口超时的提示,这样的结果肯定是不能接收的。设计的时候需要考虑,异步的方式进行处理。
在springboot中异步的方式有以下几种:
- 异步方法的异常处理:异步方法中的异常不会直接抛给调用者。如果你需要处理这些异常,需要使用Future、CompletableFuture或ListenableFuture的返回类型,并对这些对象进行适当的异常处理。
- 方法调用的自调用问题:直接在同一个类中调用另一个@Async注解的方法,Spring将无法拦截这次调用进行异步处理。解决方案通常包括将异步方法移至另一个Bean中,或使用AopContext.currentProxy()来调用。
目前我选择的技术:
CompletableFuture future = CompletableFuture.runAsync(() -> { // 模拟长时间运行的异步任务 System.out.println("Running in another thread."); });
在具体实现的时候,就能够把核心代码写到这个方法里面来实现了。前端调用的时候,直接返回处理成功或者已提交处理;就不会超时了;
2.并发的问题;
该问题主要是生成兑换码必须是唯一值的处理,在并发的时候,可能会出现重复的情况,目前使用数据库的分布式锁与redis的分布式锁进行处理;
在方法的开始位置,进行redis的setnx进行加锁的处理,并且只有加锁成功之后,才会进行相关逻辑的处理,否则的话也不会进行生成兑换码的逻辑;加锁失败的话,直接返回:“生成兑换码处理中,请稍后”;
并且会生成一笔记录,例如:
上图是数据生成中的处理,每次生成兑换码都会产生一笔生成的记录,为了让用户不用在等待界面了。
兑换码生成成功之后,操作的按钮会显示“下载”;如下图
具体的代码:
//随机生成批次号 String batchNo = SerialNoGenerator.generateSerialNo("C", "K"); req.setBatchNo(batchNo); boolean redisLock=false; resourceRights.setCreateBy(UserUtil.getJobNumber()); try { redisLock= redisTemplate.setnx(redisKey, batchNo, RedisTemplate.DEFAULT_TIME); //添加并发锁 if(redisLock) { //插入兑换记录 saveInfo(req); CompletableFuture.runAsync(() -> { saveCdkey(req,batchNo,txResourceRights); }); } else { return Result.fail("生成兑换码防并发,请稍后再试"); } } catch (Exception e) { SkyLog.error("Resource", "", "", e.getMessage(), e); return Result.fail("生成兑换码失败"); } return Result.success("生成兑换码成功"); 锁的删除是在核心逻辑saveCdkey里面进行删除的:: } catch (Exception e) { SkyLog.error("Resource", "", "", e.getMessage(), e); } finally { redisTemplate.del(redisKey); }
3.导出的问题;
该问题是为了在导出大数据量问题的时候,需要把大数据量的图片进行加载出来,目前使用的是阿里巴巴的easyExcey的功能进行实现的。
添加依赖:
com.alibaba
easyexcel
3.3.2
定义实体类:
public class ExportVO { "兑换码") ( private String stringCode; "兑换状态") ( private Double status; /** * 忽略这个字段 */ private String ignore; }
导出代码的实现:
try { //赋值的特殊处理 cdkeyList.forEach(x -> { dataList.add(ExportVO.builder() .cdkeyCode(x.getCdkeyCode()) .status(x.getStatus() == 1 ? "未兑换" : (x.getStatus() == 2 ? "已兑换" : "已作废")) .build()); }); String fileName = URLEncoder.encode("兑换码" + DateUtil.format(new Date(), DatePattern.CHINESE_DATE_TIME_PATTERN), "UTF-8").replaceAll("\\+", "%20"); response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setCharacterEncoding("utf-8"); response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx"); EasyExcel.write(response.getOutputStream(), ExportVO.class) .sheet("sheet1") .doWrite(dataList); } catch (Exception e) { e.printStackTrace(); throw new ServiceException("导出失败"); }
通过该组件的使用,可以实现10万条记录进行几秒内导出,具体的实现原理,我就不做一一介绍了;
4.提升效率的问题;
主要是在插入数据库表的时候,10万条记录的插入,还是需要花费不少时间的,目前我这边通过批量保存的方式进行处理的,通过把10万条记录进行拆分,分批次进行插入数据库的 处理,目前mybatis plu是支持批量导入的,具体实现的代码如下:
List keyList = new ArrayList<>(); int splitSize = 500; int n = 0; for (int i = 0; i < req.getSize(); i++) { keyList.add(createCode(batchNo, rights)); n++; if (n % splitSize == 0 || n == req.getSize()) { resourceService.commonSaveBatch(cdkeyList); keyList = new ArrayList<>(); } }
实现的逻辑,就像这段代码写的一样,每次进行判断是否产生了500的整数倍数,如果是的话,就进行批量的插入处理;这个批量插入是一次进行执行的。这样的话可以大大缩减插入数据库的时间。
5.唯一值的设计问题;
唯一值的设计,目前是通过分布式锁的进行处理,每次生成的随机码都会查询一篇数据库,如果存在的话就重新 生成一个随机码进行实体的处理;这样的处理,可以避免出现重复兑换码的问题;
具体的代码如下:
import java.security.SecureRandom; public class RandomCodeGenerator { public static void main(String[] args) { String charSet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; StringBuilder randomCode = new StringBuilder(); SecureRandom random = new SecureRandom(); for (int i = 0; i < 10; i++) { int index = random.nextInt(charSet.length()); randomCode.append(charSet.charAt(index)); } System.out.println("Random Code: " + randomCode.toString()); } }
这段代码的工作原理如下:
- 定义一个字符串charSet,其中包含了所有可能使用的字符:26个大写字母、26个小写字母和10个数字,共62个字符。
- 创建一个StringBuilder实例randomCode来构建最终的随机码。
- 创建一个SecureRandom实例来生成随机数。与Random相比,SecureRandom生成的随机数更适合用于安全敏感的应用。
- 在一个循环中,生成10次随机数。每次随机数的生成都是通过random.nextInt(charSet.length())来实现的,确保随机数的范围在0到charSet.length()(不包含)之间。这个随机数被用作索引从charSet中选取一个字符,然后将这个字符附加到randomCode上。
- 循环结束后,randomCode将包含一个10位的由数字和大小写字母组成的随机码。
- 最后,打印生成的随机码。
这种方法生成的随机码具有良好的随机性和较高的安全性,适用于需要随机码作为识别码、密码或者其他安全要求较高的场景。
总结:
整个流程一共有六个问题点,但是每个问题点都是一个不小的坑。经过本次的小功能的开发,也学习了不少的知识点,希望通过本次的分享,能帮助到一些同学,也能给同学们提供下解决方案的思路。