天机学堂踩坑笔记
相关资源链接:
Md笔记:蓝奏云地址
在线笔记:飞书笔记地址
相关视频教程及配套课件:
链接:百度云地址
提取码:hmz1
1. Day01 初识项目
1.1 OpenEuler 22.03LTS yum换源失败
适用于OpenEuler版本为22.03LTS,需要指定具体版本。
yum换源,备份源文件并更新,文件位置:/etc/yum.repos.d/openEuler.repo
[OS] name=OS baseurl=https://repo.huaweicloud.com/openeuler/openEuler-22.03-LTS/OS/$basearch/ enabled=1 gpgcheck=1 gpgkey=https://repo.huaweicloud.com/openeuler/openEuler-22.03-LTS/OS/$basearch/RPM-GPG-KEY-openEuler [everything] name=everything baseurl=https://repo.huaweicloud.com/openeuler/openEuler-22.03-LTS/everything/$basearch/ enabled=1 gpgcheck=1 gpgkey=https://repo.huaweicloud.com/openeuler/openEuler-22.03-LTS/everything/$basearch/RPM-GPG-KEY-openEuler [EPOL] name=EPOL baseurl=https://repo.huaweicloud.com/openeuler/openEuler-22.03-LTS/EPOL/multi_version/OpenStack/Train/$basearch/ enabled=1 gpgcheck=1 gpgkey=https://repo.huaweicloud.com/openeuler/openEuler-22.03-LTS/OS/$basearch/RPM-GPG-KEY-openEuler [EPOL5] name=EPOL5 baseurl=https://repo.huaweicloud.com/openeuler/openEuler-22.03-LTS/EPOL/main/$basearch/ enabled=1 gpgcheck=1 gpgkey=https://repo.huaweicloud.com/openeuler/openEuler-22.03-LTS/OS/$basearch/RPM-GPG-KEY-openEuler [EPOL2] name=EPOL2 baseurl=https://repo.huaweicloud.com/openeuler/openEuler-22.03-LTS/EPOL/multi_version/OpenStack/Wallaby/$basearch/ enabled=1 gpgcheck=1 gpgkey=https://repo.huaweicloud.com/openeuler/openEuler-22.03-LTS/OS/$basearch/RPM-GPG-KEY-openEuler [EPOL3] name=EPOL3 baseurl=https://repo.huaweicloud.com/openeuler/openEuler-22.03-LTS/EPOL/update/multi_version/OpenStack/Train/$basearch/ enabled=1 gpgcheck=1 gpgkey=https://repo.huaweicloud.com/openeuler/openEuler-22.03-LTS/OS/$basearch/RPM-GPG-KEY-openEuler [EPOL4] name=EPOL4 baseurl=https://repo.huaweicloud.com/openeuler/openEuler-22.03-LTS/EPOL/update/multi_version/OpenStack/Wallaby/$basearch/ enabled=1 gpgcheck=1 gpgkey=https://repo.huaweicloud.com/openeuler/openEuler-22.03-LTS/OS/$basearch/RPM-GPG-KEY-openEuler [debuginfo] name=debuginfo baseurl=https://repo.huaweicloud.com/openeuler/openEuler-22.03-LTS/debuginfo/$basearch/ enabled=1 gpgcheck=1 gpgkey=https://repo.huaweicloud.com/openeuler/openEuler-22.03-LTS/debuginfo/$basearch/RPM-GPG-KEY-openEuler [source] name=source baseurl=https://repo.huaweicloud.com/openeuler/openEuler-22.03-LTS/source/ enabled=1 gpgcheck=1 gpgkey=https://repo.huaweicloud.com/openeuler/openEuler-22.03-LTS/source/RPM-GPG-KEY-openEuler [update] name=update baseurl=https://repo.huaweicloud.com/openeuler/openEuler-22.03-LTS/update/$basearch/ enabled=1 gpgcheck=1 gpgkey=https://repo.huaweicloud.com/openeuler/openEuler-22.03-LTS/OS/$basearch/RPM-GPG-KEY-openEuler
经测试,华为云镜像下载速度高于阿里云
1.2 运行docker-compose.yml文件失败
错误信息如下:
ERROR: The Compose file './docker-compose.yml' is invalid because: networks.name Additional properties are not allowed ('name' was unexpected)
解决方法:运行以下命令将docker-compose升级,如1.28.5版本
# 下载 curl -L https://github.com/docker/compose/releases/download/1.28.5/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose # 安装 chmod +x /usr/local/bin/docker-compose
参考链接无法在docker-compose中提供网络名称
1.3 Docker安装seata出错
错误信息如下:
apm-skywalking not enabled JMX disabled Affected JVM parameters: -Dlog.home=/root/logs/seata -server -Dloader.path=/lib -Xmx2048m -Xms2048m -Xss640k -XX:SurvivorRatio=10 -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m -XX:MaxDirectMemorySize=1024m -XX:-OmitStackTraceInFastThrow -XX:-UseAdaptiveSizePolicy -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/root/logs/seata/java_heapdump.hprof -XX:+DisableExplicitGC -Xloggc:/root/logs/seata/seata_gc.log -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=100M -XX:+UnlockExperimentalVMOptions -XX:+UseG1GC -Dio.netty.leakDetectionLevel=advanced -Dapp.name=seata-server -Dapp.pid=1 -Dapp.home=/ -Dbasedir=/ OpenJDK 64-Bit Server VM warning: Cannot open file /root/logs/seata/seata_gc.log due to No such file or directory Exception in thread "main" java.lang.IllegalArgumentException: Cannot instantiate interface org.springframework.context.ApplicationListener : io.seata.server.ServerApplicationListener at org.springframework.boot.SpringApplication.createSpringFactoriesInstances(SpringApplication.java:450) at org.springframework.boot.SpringApplication.getSpringFactoriesInstances(SpringApplication.java:432) at org.springframework.boot.SpringApplication.getSpringFactoriesInstances(SpringApplication.java:425) at org.springframework.boot.SpringApplication.(SpringApplication.java:268) at org.springframework.boot.SpringApplication.(SpringApplication.java:246) at org.springframework.boot.SpringApplication.run(SpringApplication.java:1300) at org.springframework.boot.SpringApplication.run(SpringApplication.java:1289) at org.apache.seata.server.ServerApplication.main(ServerApplication.java:30) Caused by: java.lang.ClassNotFoundException: io.seata.server.ServerApplicationListener at java.net.URLClassLoader.findClass(URLClassLoader.java:387) at java.lang.ClassLoader.loadClass(ClassLoader.java:418) at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:352) at java.lang.ClassLoader.loadClass(ClassLoader.java:351) at java.lang.Class.forName0(Native Method) at java.lang.Class.forName(Class.java:348) at org.springframework.util.ClassUtils.forName(ClassUtils.java:291) at org.springframework.boot.SpringApplication.createSpringFactoriesInstances(SpringApplication.java:443) ... 7 more
关键一条:
OpenJDK 64-Bit Server VM warning: Cannot open file /root/logs/seata/seata_gc.log due to No such file or directory
尝试新建该目录和文件并授权,无效
尝试修改seata目录下application.yml配置文件中的日志文件输出路径:
# 日志配置 logging: config: classpath:logback-spring.xml file: path: /usr/local/src/seata/logs/ # path: ${user.home}/logs/seata
seata却能够正常启动
seata修改完配置文件后启动:
docker run -d --name seata -p 8099:8099 -p 7099:7099 -e SEATA_IP=192.168.150.101 -v /usr/local/src/seata:/seata-server/resources --network tjxt --restart always --ulimit nofile=1024 seataio/seata-server:1.5.2
1.4 Jenkins启动报错
错误信息:
DEPRECATED: The legacy builder is deprecated and will be removed in a future release. Install the buildx component to build images with BuildKit: https://docs.docker.com/go/buildx/ permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Post "http://%2Fvar%2Frun%2Fdocker.sock/v1.24/build?buildargs=%7B%22JAVA_OPTS%22%3A%22-Xms300m+-Xmx300m%22%7D&cachefrom=%5B%5D&cgroupparent=&cpuperiod=0&cpuquota=0&cpusetcpus=&cpusetmems=&cpushares=0&dockerfile=Dockerfile&labels=%7B%7D&memory=0&memswap=0&networkmode=default&rm=1&shmsize=0&t=tj-gateway&target=&ulimits=%5B%5D&version=1": dial unix /var/run/docker.sock: connect: permission denied
很明显 permission denied权少权限,这里因为是本地虚拟机,所有直接:
chmod 777 -R /var/run/docker.sock
1.5 修改nacos实力权重或者对某实例下线报错
在Nacos控制台进行上述操作,错误信息
caused: errCode: 500, errMsg: do metadata operation failed ;caused: com.alibaba.nacos.consistency.exception.ConsistencyException: The Raft Group [naming_instance_metadata] did not find the Leader node;caused: The Raft Group [naming_instance_metadata] did not find the Leader node;
原因:Nacos采用raft算法来计算Leader,并且会记录上次启动的集群地址,所以当我们自己的服务器IP改变时(网络环境不稳定,如WIFI,IP地址也经常变化),导致raft记录的集群地址失效,导致选Leader出现问题,
解决方法:删除Nacos根目录下data文件夹下的protocol文件夹,重启nacos即可
#相关命令 #进入nacos容器 docker exec -it nacos /bin/bash #进入nacos的data文件 cd data #删除protocol文件夹 rm -rf protocol/ #退出容器 exit #重启nacos容器 docker restart nacos
1.6 OpenEuler 22.03LTS系统安装问题
在VMware16 pro安装一直黑屏,无法进行安装系统
方法:VMware16 pro版本不兼容问题,升级到VMware 17pro
2. Day02 我的课表
2.1 数据库连接配置错误
错误信息如下:
java.sql.SQLNonTransientConnectionException: Could not create connection to database server. Attempted reconnect 3 times. Giving up. Caused by: com.mysql.cj.exceptions.CJException: Unknown database 'tj_learning'
原因及解决方法:以为是拉去nacos共享配置出错,经排查是新建数据库时名称写错为 tj-learning ,修改为tj_learning
2.2 RabbitMQ消息监听
第一次接触,特此记录下。
LessonChangeListener.java
package com.tianji.learning.mq; import cn.hutool.core.collection.CollUtil; import com.tianji.api.dto.trade.OrderBasicDTO; import com.tianji.common.constants.MqConstants; import com.tianji.learning.service.ILearningLessonService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.core.ExchangeTypes; import org.springframework.amqp.rabbit.annotation.Exchange; import org.springframework.amqp.rabbit.annotation.Queue; import org.springframework.amqp.rabbit.annotation.QueueBinding; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; /** * @author: hong.jian * @date 2024-03-08 17:37 */ @Component @Slf4j @RequiredArgsConstructor // 使用构造器,lombok会在编译器生成相应的方法 public class LessonChangeListener { private final ILearningLessonService lessonService; /*** * MQ消息发送 * rabbitMqHelper.send( * MqConstants.Exchange.ORDER_EXCHANGE, // Exchange * MqConstants.Key.ORDER_PAY_KEY, // Key * OrderBasicDTO.builder() * .orderId(orderId) * .userId(userId) * .courseIds(cIds) * .finishTime(order.getFinishTime()) * .build() * ); * * @param dto 接受的参数类型为OrderBasicDTO */ @RabbitListener(bindings = @QueueBinding(value = @Queue(value = "learning.lesson.pay.queue", durable = "true"), exchange = @Exchange(value = MqConstants.Exchange.ORDER_EXCHANGE, type = ExchangeTypes.TOPIC), key = MqConstants.Key.ORDER_PAY_KEY)) public void onMsg(OrderBasicDTO dto) { log.info("LessonChangeListener接收消息,用户{},添加课程{}", dto.getUserId(), dto.getCourseIds()); // 校验 if (dto.getUserId() == null || dto.getOrderId() == null || CollUtil.isEmpty(dto.getCourseIds())) { // 这里是接受MQ消息,中断即可,若抛异常,则会开启重试 return; } // 保存课程到课表 lessonService.addUserLesson(dto.getUserId(),dto.getCourseIds()); } }
相关概念详见MQ基础.md
3. Day03 学习计划和进度
3.1 前端接口请求参数异常
请求/ls/lessons/plans参数如图:
断电调试时接口如下,并未收到参数:
解决方法:前端使用application/json进行传递参数,需要使用@RequestBody进行接收
/** * 创建学习计划 * */ @ApiOperation("创建学习计划") @PostMapping("/plans") public void createLessonPlan(@RequestBody LearningPlanFormDTO dto) { learningLessonService.createLessonPlan(dto.getCourseId(),dto.getFreq()); }
4. Day04 改造提交学习记录
4.1 学习记录改造后出现NPE
控制台日志如下:
17:41:51.233-[DESKTOP-CSPG5LG][4dae58c8c33e4b1a9de75ec60e90c60a]--DEBUG 24112 --- [nio-8090-exec-1] c.t.l.m.LearningRecordMapper.update : ==> Parameters: 137(Integer), true(Boolean), 2024-03-11T17:41:51(LocalDateTime), 1767122184276279297(Long) 17:41:51.237-[DESKTOP-CSPG5LG][4dae58c8c33e4b1a9de75ec60e90c60a]--DEBUG 24112 --- [nio-8090-exec-1] c.t.l.m.LearningRecordMapper.update : java.lang.NullPointerException: null at com.tianji.learning.util.LearningRecordDelayTaskHandler.cleanRecordCache(LearningRecordDelayTaskHandler.java:148) ~[classes/:na] at com.tianji.learning.service.impl.LearningRecordServiceImpl.handleVideoRecord(LearningRecordServiceImpl.java:236) ~[classes/:na] at com.tianji.learning.service.impl.LearningRecordServiceImpl.submitLearningRecord(LearningRecordServiceImpl.java:134) ~[classes/:na]
出错部分代码如下:
Integer allSections = courseInfo.getSectionNum(); // 该课程所有小节数 // 判断该课程所有小节是否已学完 boolean allFinished = learningLesson.getLearnedSections() + 1 >= allSections; // 更新课表信息:课表状态,已学小节数,最近学习小节id,最近学习时间 learningLessonService.lambdaUpdate() // 如果当前小节未学完,更新最近学习小节id和最近学习时间 .set(LearningLesson::getLatestSectionId, learningLesson.getLatestSectionId() + 1) .set(LearningLesson::getLatestLearnTime, dto.getCommitTime()) // 如果当前小姐已学完,更新已学小节数 .set(LearningLesson::getLearnedSections, learningLesson.getLearnedSections()) // 如果该课表所以小节已学完,则更新课表状态为已学完 .set(allFinished, LearningLesson::getStatus, LessonStatus.FINISHED) // 首次学习需要将状态由未开始更新为学习中 // .set(learningLesson.getLearnedSections() == 0 , LearningLesson::getStatus, LessonStatus.LEARNING) .set(learningLesson.getStatus() == LessonStatus.NOT_BEGIN, LearningLesson::getStatus, LessonStatus.LEARNING) .eq(LearningLesson::getId, learningLesson.getId()) .update();
原因:在创建学习计划后,learned_sections字段由于未赋值为null,所以报了NPE
解决方法:在数据库表结构给该字段赋初值0。
5. Day05 问答系统
5.1 用户端分页查询评论时昵称错乱问题
原因:对于目标回复用户收集其target_user_id时和user_id混在一起,导致错误;
解决方案:将二者分开处理
/** * 管理端分页查询回答或评论列表 * @param pageQuery 分页参数 * @return 分页列表 */ @Override public PageDTO pageAdmin(ReplyPageQuery pageQuery) { // 校验问题id和回答id是否都为空 if (pageQuery.getAnswerId() == null && pageQuery.getQuestionId() == null) { throw new BadRequestException("查询参数错误"); } // 分页查询回答或评论列表 Page replyPage = this.lambdaQuery() // 如果传问题id就把问题id作查询条件 .eq(pageQuery.getQuestionId() != null, InteractionReply::getQuestionId, pageQuery.getQuestionId()) .eq(InteractionReply::getAnswerId, pageQuery.getAnswerId() == null ? 0L : pageQuery.getAnswerId()) // 字段默认值0 .page(pageQuery.toMpPage(new OrderItem(DATA_FIELD_NAME_LIKED_TIME, false), new OrderItem(DATA_FIELD_NAME_CREATE_TIME, true))); // 按照点赞次数降序排序降序,创建时间升序排序 List records = replyPage.getRecords(); if (CollUtil.isEmpty(records)) { // 查询不到,返回空集 return PageDTO.of(replyPage, Collections.emptyList()); } // 关联用户信息,先收集用户id,封装到map List userIds = new ArrayList(); List targetUserIds = new ArrayList(); // 目标用户id List targetReplyIds = new ArrayList(); // 目标回复id for (InteractionReply reply : records) { if (!reply.getAnonymity()) { // 非匿名用户需要查询 userIds.add(reply.getUserId()); // userIds.add(reply.getTargetUserId()); } // "target_user_id"字段默认值为0,查询评论时生效 if (reply.getTargetUserId() != null && reply.getTargetUserId() > 0) { targetUserIds.add(reply.getTargetUserId()); } // "target_reply_id"字段默认值为0,查询评论时生效 if (reply.getTargetReplyId() != null && reply.getTargetReplyId() > 0) { targetReplyIds.add(reply.getTargetReplyId()); } } // 查询目标回复列表并封装为Map Map targetReplyMap = new HashMap(); // targetReplyIds不为空,去查询数据库 if (!CollUtil.isEmpty(targetReplyIds)) { // 查询目标评论,并封装为Map List targetReplies = listByIds(targetReplyIds); targetReplyMap = targetReplies.stream().collect(Collectors.toMap(InteractionReply::getId, reply -> reply)); } // 查询用户和目标回复用户并封装为Map Map userMap = getUserDTOMap(userIds); Map targetUserMap = getUserDTOMap(targetUserIds); // 保存结果 List replyVOS = new ArrayList(); for (InteractionReply reply : records) { ReplyVO replyVO = BeanUtil.toBean(reply, ReplyVO.class); UserDTO userDTO = userMap.getOrDefault(reply.getUserId(), null); // 如果当前回答或评论匿名,不进行赋值 if (!replyVO.getAnonymity() && userDTO != null) { replyVO.setUserIcon(userDTO.getIcon()); // 回答人头像 replyVO.setUserName(userDTO.getName()); // 回答人昵称 replyVO.setUserType(userDTO.getType()); // 回答人类型 } UserDTO targetUserDTO = targetUserMap.getOrDefault(reply.getTargetUserId(), null); InteractionReply targetReply = targetReplyMap.getOrDefault(reply.getTargetReplyId(), null); // 如果目标评论匿名,不进行赋值 if (targetReply != null && !targetReply.getAnonymity() && targetUserDTO != null) { // 目标回复非匿名才赋值 replyVO.setTargetUserName(targetUserDTO.getName()); // 目标用户昵称 } replyVOS.add(replyVO); } // 返回结果 return PageDTO.of(replyPage, replyVOS); }
7. Day07 积分系统
7.1 mybatis-plus查询报错
错误信息:
Caused by: org.apache.ibatis.exceptions.PersistenceException: ### Error querying database. Cause: java.lang.IndexOutOfBoundsException: Index 2 out of bounds for length 2
相关代码:
/** * 获取今日积分 * * @return 今日积分列表 */ @Override public List getTodayPoints() { // 获取当前登录用户 Long userId = UserContext.getUser(); // 分类查询当前用户今日所得各类积分 LocalDateTime now = LocalDateTime.now(); LocalDateTime dayStartTime = DateUtils.getDayStartTime(now); // 当前开始时间 LocalDateTime dayEndTime = DateUtils.getDayEndTime(now); // 当天结束时间 QueryWrapper wrapper = new QueryWrapper(); // 封装查询wrapper wrapper.select("type", "sum(points) as tmp") // 查询类型和该类型积分数(使用临时变量暂存暂存BigDemical类型数据) .eq("user_id", userId) // 当前用户 .between("create_time", dayStartTime, dayEndTime) // 当天 .groupBy("type"); // 根据类型分类 wrapper.select("type", "sum(points) as userId"); List records = this.list(wrapper); // 判空 if (CollUtil.isEmpty(records)) { return Collections.emptyList(); } // 封装到VO并返回 List voList = records.stream().map(record -> { PointsStatisticsVO recordVO = new PointsStatisticsVO(); recordVO.setPoints(record.getUserId().intValue()); // 该类型今日积分数,临时变量暂存 recordVO.setType(record.getType().getDesc()); // 该类型名称 recordVO.setMaxPoints(record.getType().getMaxPoints()); // 该类型上限积分数 return recordVO; }).collect(Collectors.toList()); return voList; }
原因:使用了PointsRecord存储查询结果,但该类没有构造器
解决方案:在PointsRecord添加@NoArgsConstructor
8. Day08 排行榜
8.1 xxl-job配置
原因:xxl-job在Springboot项目中集成后,需要手动新增相应的执行器和执行任务,不会自动新增,踩坑。
8.2 SpringBoot项目启动后立即停止
原因:SpringBoot启动后控制台无报错,但立即停止,缺少web依赖
解决方法:补充依赖spring-boot-starter-web如下
org.springframework.boot spring-boot-starter-web
8.3 使用xxl-job迁移分表数据报错
错误信息:提示重复主键
Caused by: org.springframework.dao.DuplicateKeyException: com.tianji.learning.mapper.PointsRecordMapper.insert (batch index #1) failed. Cause: java.sql.BatchUpdateException: Duplicate entry '1773603384562077753' for key 'points_record.PRIMARY' ; Duplicate entry '1773603384562077753' for key 'points_record.PRIMARY';
原因:动态表名插件配置及使用错误
应该是先从当前赛季积分明细表分页查询数据后,更新动态表名,再插入到上赛季积分明细表
// 声明动态表名拦截器,使用拦截器插件 @Bean public DynamicTableNameInnerInterceptor dynamicTableNameInnerInterceptor() { // 准备一个Map,用于存储TableNameHandler Map map = new HashMap(1); // 存入一个TableNameHandler,用来替换points_board表名称 // 替换方式,就是从TableInfoContext中读取保存好的动态表名,判空是提高代码健壮性 map.put("points_board", (sql, tableName) -> TableInfoContext.getInfo() == null ? tableName : TableInfoContext.getInfo()); map.put("points_record", (sql, tableName) -> TableInfoContext.getInfo() == null ? tableName : TableInfoContext.getInfo()); return new DynamicTableNameInnerInterceptor(map); }
// 分页查询上赛季积分明细数据,先构建分页参数 PageQuery pageQuery = new PageQuery(); pageQuery.setPageNo(sharedIndex + 1); // 页码 pageQuery.setPageSize(50); // 页面记录数 while (true) { log.debug("当前页:{}", pageQuery.getPageNo()); TableInfoContext.setInfo(null); // 使用默认表名,points_record // 按照id升序查询 Page recordPage = pointsRecordService.page(pageQuery.toMpPage("id",true)); if (CollUtil.isEmpty(recordPage.getRecords())) { // 结束循环 break; } // 翻页,跳过N个页,N就是分片数量 pageQuery.setPageNo(pageQuery.getPageNo() + shardTotal); // 页码+total,跳过N页 // 使用动态表名,points_record_5 TableInfoContext.setInfo(targetTableName); // 持久化到db相应的赛季表中,批量新增 pointsRecordService.saveBatch(recordPage.getRecords()); }
9. Day09 优惠券管理
9.1 新增优惠券mybatis错误
错误信息:
### The error may exist in com/tianji/promotion/mapper/CouponMapper.java (best guess) ### The error may involve com.tianji.promotion.mapper.CouponMapper.insert-Inline ### The error occurred while setting parameters ### SQL: INSERT INTO coupon ( id, name, discount_type, specific, discount_value, threshold_amount, max_discount_amount, obtain_way, total_num, user_limit, creater, updater ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ) ### Cause: java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'specific, discount_value, threshold_amount, max_discount_amount, obtain_way,
原因:specific是mysql关键字
@ApiModelProperty(value = "是否限定作用范围,false:不限定,true:限定。默认false") private Boolean specific;
将Coupon实体类添加注解 @TableField注解
@ApiModelProperty(value = "是否限定作用范围,false:不限定,true:限定。默认false") @TableField("`specific`") private Boolean specific;
9.2 定时开始/结束发放优惠券遇到的bug
Bug1:Mybatis-Plus分页查询时碰到total有值但records为空
原因:Mybatis-Plus分页插件设置了maxLimit单页条数
// 分页插件配置 PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL); paginationInnerInterceptor.setMaxLimit(200L); // 单页分页条数限制(默认无限制) interceptor.addInnerInterceptor(paginationInnerInterceptor);
方法:可以适当增大maxLimit或者修改代码:
// 分页插件配置 PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL); paginationInnerInterceptor.setMaxLimit(200L); // 单页分页条数限制(默认无限制) paginationInnerInterceptor.setOverflow(true); // 溢出总页数后是否进行处理(默认不处理) interceptor.addInnerInterceptor(paginationInnerInterceptor);
注:
Mybatis-Plus分页参数说明如图所示:
参考链接:Mybatis-Plus分页插件参数说明
Bug2:xxl-job分片广播
原因:xxl-job结合分页时部分日志异常,如页号,推测可能局部配置异常或者内部bug,
/** * 定时开始发放优惠券 * 利用XXL-JOB的数据分片功能实现 */ @XxlJob("couponIssueJobHandler") public void issueCoupon(LocalDateTime now, int shardTotal, int shardIndex) { log.info("开始执行定时开始发放优惠券任务..."); PageQuery pageQuery = new PageQuery(); pageQuery.setPageNo(shardIndex + 1); // 页码(分片索引从0开始,页码从1开始) pageQuery.setPageSize(20); // 页面大小,根据数据量动态调整 while (true) { // 分页查询所有未开始的,发放开始时间早于当前时间的优惠券 Page couponPage = couponService.lambdaQuery() .eq(Coupon::getStatus, CouponStatus.UN_ISSUE) // 查询未开始状态的 .le(Coupon::getIssueBeginTime, now) // 发放开始时间早于当前时间 .page(pageQuery.toMpPage("id", true));// 根据id进行排序,避免重复处理数据 List records = couponPage.getRecords(); if (CollUtil.isEmpty(records)) { // 判空 break; } // 更新优惠券状态 records.stream().forEach(coupon -> coupon.setStatus(CouponStatus.ISSUING)); // 批量更新优惠券 couponService.updateBatchById(records); // 处理下一页数据 if (couponPage.hasNext()) { // 翻页,数量为总分片数 pageQuery.setPageNo(pageQuery.getPageNo() + shardTotal); } else { break; } } log.info("完成定时开始发放优惠券任务..."); }
11. Day11领取优惠券优化
11.1 RabbitMQ交换机配置错误
错误信息如下:
16:47:54.429-[DESKTOP-CSPG5LG][sys]-- WARN 20360 --- [ntContainer#0-1] o.s.a.r.listener.BlockingQueueConsumer : Failed to declare queue: coupon.receive.queue 16:47:54.430-[DESKTOP-CSPG5LG][sys]-- WARN 20360 --- [ntContainer#0-1] o.s.a.r.listener.BlockingQueueConsumer : Queue declaration failed; retries left=1 org.springframework.amqp.rabbit.listener.BlockingQueueConsumer$DeclarationException: Failed to declare queue(s):[coupon.receive.queue] at org.springframework.amqp.rabbit.listener.BlockingQueueConsumer.attemptPassiveDeclarations(BlockingQueueConsumer.java:760) ~[spring-rabbit-2.4.6.jar:2.4.6] at org.springframework.amqp.rabbit.listener.BlockingQueueConsumer.passiveDeclarations(BlockingQueueConsumer.java:637) ~[spring-rabbit-2.4.6.jar:2.4.6] at org.springframework.amqp.rabbit.listener.BlockingQueueConsumer.start(BlockingQueueConsumer.java:624) ~[spring-rabbit-2.4.6.jar:2.4.6] at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer$AsyncMessageProcessingConsumer.initialize(SimpleMessageListenerContainer.java:1376) ~[spring-rabbit-2.4.6.jar:2.4.6] at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer$AsyncMessageProcessingConsumer.run(SimpleMessageListenerContainer.java:1221) ~[spring-rabbit-2.4.6.jar:2.4.6] at java.base/java.lang.Thread.run(Thread.java:834) ~[na:na] Caused by: java.io.IOException: null at com.rabbitmq.client.impl.AMQChannel.wrap(AMQChannel.java:129) ~[amqp-client-5.14.2.jar:5.14.2] at com.rabbitmq.client.impl.AMQChannel.wrap(AMQChannel.java:125) ~[amqp-client-5.14.2.jar:5.14.2] at com.rabbitmq.client.impl.AMQChannel.exnWrappingRpc(AMQChannel.java:147) ~[amqp-client-5.14.2.jar:5.14.2] at com.rabbitmq.client.impl.ChannelN.queueDeclarePassive(ChannelN.java:1012) ~[amqp-client-5.14.2.jar:5.14.2] at com.rabbitmq.client.impl.ChannelN.queueDeclarePassive(ChannelN.java:46) ~[amqp-client-5.14.2.jar:5.14.2] at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na] at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na] at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na] at java.base/java.lang.reflect.Method.invoke(Method.java:566) ~[na:na] at org.springframework.amqp.rabbit.connection.CachingConnectionFactory$CachedChannelInvocationHandler.invoke(CachingConnectionFactory.java:1162) ~[spring-rabbit-2.4.6.jar:2.4.6] at com.sun.proxy.$Proxy223.queueDeclarePassive(Unknown Source) ~[na:na] at org.springframework.amqp.rabbit.listener.BlockingQueueConsumer.attemptPassiveDeclarations(BlockingQueueConsumer.java:738) ~[spring-rabbit-2.4.6.jar:2.4.6] ... 5 common frames omitted Caused by: com.rabbitmq.client.ShutdownSignalException: channel error; protocol method: #method(reply-code=404, reply-text=NOT_FOUND - no queue 'coupon.receive.queue' in vhost '/tjxt', class-id=50, method-id=10) at com.rabbitmq.utility.ValueOrException.getValue(ValueOrException.java:66) ~[amqp-client-5.14.2.jar:5.14.2] at com.rabbitmq.utility.BlockingValueOrException.uninterruptibleGetValue(BlockingValueOrException.java:36) ~[amqp-client-5.14.2.jar:5.14.2] at com.rabbitmq.client.impl.AMQChannel$BlockingRpcContinuation.getReply(AMQChannel.java:502) ~[amqp-client-5.14.2.jar:5.14.2] at com.rabbitmq.client.impl.AMQChannel.privateRpc(AMQChannel.java:293) ~[amqp-client-5.14.2.jar:5.14.2] at com.rabbitmq.client.impl.AMQChannel.exnWrappingRpc(AMQChannel.java:141) ~[amqp-client-5.14.2.jar:5.14.2] ... 14 common frames omitted Caused by: com.rabbitmq.client.ShutdownSignalException: channel error; protocol method: #method(reply-code=404, reply-text=NOT_FOUND - no queue 'coupon.receive.queue' in vhost '/tjxt', class-id=50, method-id=10) at com.rabbitmq.client.impl.ChannelN.asyncShutdown(ChannelN.java:517) ~[amqp-client-5.14.2.jar:5.14.2] at com.rabbitmq.client.impl.ChannelN.processAsync(ChannelN.java:341) ~[amqp-client-5.14.2.jar:5.14.2] at com.rabbitmq.client.impl.AMQChannel.handleCompleteInboundCommand(AMQChannel.java:182) ~[amqp-client-5.14.2.jar:5.14.2] at com.rabbitmq.client.impl.AMQChannel.handleFrame(AMQChannel.java:114) ~[amqp-client-5.14.2.jar:5.14.2] at com.rabbitmq.client.impl.AMQConnection.readFrame(AMQConnection.java:739) ~[amqp-client-5.14.2.jar:5.14.2] at com.rabbitmq.client.impl.AMQConnection.access$300(AMQConnection.java:47) ~[amqp-client-5.14.2.jar:5.14.2] at com.rabbitmq.client.impl.AMQConnection$MainLoop.run(AMQConnection.java:666) ~[amqp-client-5.14.2.jar:5.14.2] ... 1 common frames omitted 16:47:59.442-[DESKTOP-CSPG5LG][sys]-- WARN 20360 --- [ntContainer#0-1] o.s.a.r.listener.BlockingQueueConsumer : Failed to declare queue: coupon.receive.queue 16:47:59.444-[DESKTOP-CSPG5LG][sys]--ERROR 20360 --- [ntContainer#0-1] o.s.a.r.l.SimpleMessageListenerContainer : Consumer threw missing queues exception, fatal=true org.springframework.amqp.rabbit.listener.QueuesNotAvailableException: Cannot prepare queue for listener. Either the queue doesn't exist or the broker will not allow us to use it. at org.springframework.amqp.rabbit.listener.BlockingQueueConsumer.handleDeclarationException(BlockingQueueConsumer.java:710) ~[spring-rabbit-2.4.6.jar:2.4.6] at org.springframework.amqp.rabbit.listener.BlockingQueueConsumer.passiveDeclarations(BlockingQueueConsumer.java:644) ~[spring-rabbit-2.4.6.jar:2.4.6] at org.springframework.amqp.rabbit.listener.BlockingQueueConsumer.start(BlockingQueueConsumer.java:624) ~[spring-rabbit-2.4.6.jar:2.4.6] at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer$AsyncMessageProcessingConsumer.initialize(SimpleMessageListenerContainer.java:1376) ~[spring-rabbit-2.4.6.jar:2.4.6] at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer$AsyncMessageProcessingConsumer.run(SimpleMessageListenerContainer.java:1221) ~[spring-rabbit-2.4.6.jar:2.4.6] at java.base/java.lang.Thread.run(Thread.java:834) ~[na:na] Caused by: org.springframework.amqp.rabbit.listener.BlockingQueueConsumer$DeclarationException: Failed to declare queue(s):[coupon.receive.queue]
原因:交换机类型错误,代码里写的是topic,mq里的是direct
/** * 更新优惠券已发放数量,新增用户券 * @param uc 这里需要用到优惠券id和用户id */ @RabbitListener(bindings = @QueueBinding( value = @Queue(name = "coupon.receive.queue", durable = "true"), exchange = @Exchange(name = MqConstants.Exchange.PROMOTION_EXCHANGE,type = ExchangeTypes.TOPIC), key = MqConstants.Key.COUPON_RECEIVE )) public void listenCouponReceiveMessage(UserCouponDTO uc){ // 更新优惠券已发放数量,新增用户券 userCouponService.checkAndCreateUserCoupon(uc); }
解决方法:删掉现有MqConstants.Exchange.PROMOTION_EXCHANGE交换机,重新创建并绑定消息队列
coupon.receive.queue
11.2 Redis更新优惠券已发放数量的并发问题
问题描述及相关代码:
兑换码领券 UserCouponRedissonMQServiceImpl.exchangeCoupon接口, 更新Redis中指定优惠券的已发放数量的代码如下,在压力测试时会出现数据脏读问题,如领了3张券,issueNum只加了1
// 更新优惠券库的已发放数量+1 redisTemplate.opsForHash().put(PromotionConstants.COUPON_CACHE_KEY_PREFIX + id, "issueNum", String.valueOf(coupon.getIssueNum() + 1));
原因:并发线程角度时数据脏读问题
解决方法,修改代码:
// 更新优惠券的已发放数量+1 redisTemplate.opsForHash().increment(PromotionConstants.COUPON_CACHE_KEY_PREFIX + couponId, "issueNum", 1);