代码拉取完成,页面将自动刷新
同步操作将从 xiaohuee/interview 强制同步,此操作会覆盖自 Fork 仓库以来所做的任何修改,且无法恢复!!!
确定后同步将在后台操作,完成时将刷新页面,请耐心等待。
Day1 - 认识项目、搭建环境、修复BUG、部署
1、vi修改ifcfg-ens33文件,将DNS1修改为114.114.114.114,保存退出
2、重启网卡:service network restart
3、测试是否可以访问外网:ping www.baidu.com
3、安装vim和lrzsz
yum -y install vim
yum -y install lrzsz
4、Linux安装coplar(外网穿透工具,用于支付回调)
curl -L https://www.cpolar.com/static/downloads/install-release-cpolar.sh | sudo bash
卸载
curl -L https://www.cpolar.com/static/downloads/install-release-cpolar.sh | sudo bash -s -- --remove
查看版本号,有正常显示版本号即为安装成功
cpolar version
token认证,登录cpolar官网后台,点击左侧的验证,查看自己的认证token,之后将token贴在命令行里
cpolar authtoken xxxxxxx
启动:
systemctl start cpolar
查看服务状态:
systemctl status cpolar
将本地80服务暴露至公网:
cpolar http 8080
设为开机自启
systemctl enable cpolar
查看服务状态:
systemctl status cpolar
其他安装说明:
cpolar默认安装路径 /usr/local/bin/cpolar,
安装脚本会自动配置systemd服务脚本,启动以后,可以开机自启动。
如果第一次安装,会默认配置一个简单的样例配置文件,创建了两个样例隧道,一个web,一个ssh
cpolar配置文件路径: /usr/local/etc/cpolar/cpolar.yml
修改占用的端口,如cpolar web UI端口为9200,ES也是9200,可以将端口修改为9201
vim /usr/local/etc/cpolar/cpolar.yml
在配置文件中,增加一行参数::
client_dashboard_addr: 192.168.150.101:9201
重启cpolar服务
windows系统:在控制面板–管理工具—服务—cpolar service,重启服务。
linux系统:执行命令sudo systemctl restart cpolar
5、软件
默认启动的docker容器
1、jenkins http://192.168.150.101:18080 root 123
2、gogs http://192.168.150.101:10880 tjxt 123321
3、seata - 7099和8099
4、mysql 3306 root 123
5、xxl-job-admin http://192.168.150.101:8880/xxl-job-admin admin 123456
6、nacos-server 1 http://192.168.150.101:8848/nacos nacos nacos
7、rabbitmq http://192.168.150.101:15672 tjxt 123321
8、redis 6379 123321
9、elasticsearch http://192.168.150.101:9200
非默认启动的docker容器
1、MongoDB
2、kibana http://192.168.150.101:5601
3、Consul http://192.168.150.101:8500
4、Emqx http://192.168.150.101:18083 admin public
5、Sentinel http://192.168.150.101:80901
非容器软件
1、Tomcat
2、Nginx
3、Logstash
默认启动服务
1、tj-user 用户服务
2、tj-auth 权限服务
3、tj-gateway 网关
4、tj-course 课程服务
5、tj-exam 考试服务 (先设为开机不自启) 部分
6、tj-search 搜索服务
7、tj-data 数据服务 部分
8、tj-media 媒资服务
未启动服务
1、tj-learning 学习服务 待完成
2、tj-trade 交易服务 (先设为开机自启) 部分
3、tj-message 消息中心
4、tj-pay 支付服务
尚未开发
1、tj-remark 评价服务 待完成
2、tj-promotion (模块还没建)促销服务 待完成
公共模块
1、tj-api (feign接口)
2、tj-common 通用工程
6、涉及分布式事务库
`tj_course`
`tj_exam`
`tj_trade`
7、为linux命令起别名
1、临时别名
命令格式:alias ***='***'
比如:alias nt='netstat -tunlp'
注:其他终端不可用,关闭终端后会失效
2、永久别名
1、用vim打开隐藏文件 ~/.bashrc 修改文件
2、在.bashrc添加命令,如: alias ds='du -h --max-depth=0 *'
alias rm='rm -i'
alias cp='cp -i'
alias mv='mv -i'
alias dps='docker ps --format "table{{.ID}}\t{{.Names}}\t{{.Status}}\t{{.Ports}}"'
alias dis='docker images'
alias dexe='docker exec -it'
alias dlog='docker logs'
alias ds='du -h --max-depth=0 *'
3、令文件生效:source ~/.bashrc
4、验证是否生效
8、持续集成
1、远程调试
1)、Edit Configuration,添加Remote JVM Debug
2)、指定Name(随意)、Host(远程IP)、Use model classpath(需要调试的模块)
3)、将以下命令参数添加至远程服务启动脚本中(已添加)
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005
4)、使用添加5005命令参数的远程服务,进行构建部署启动(点一下debug组的tj-trade-debug构建按钮即可)
5)、将本地的服务debug启动
6)、访问相应功能,会进入本地断点
2、本地swagger测试
1)、访问对应微服务IP:端口/doc.html 和 网关 IP:端口/doc.html 都可以(注意添加user-info 请求头,值为用户ID)
3、组件测试
1)、启动本地需要调试的微服务(注意配置文件修改为local环境的)
2)、停掉需要测试微服务 远程对应的微服务(让该微服务只留下本地的实例,将来消费者拉取实例列表时就只剩下本地的了)
3)、本地打断点,访问相应功能调试即可
4、项目部署
1)、修改jenkins 的dev组中tjxt-dev-build配置(只用做一次)
1、将Branch Filter改为 my-dev (将来只有my-dev分支push了代码才会触发web-hook钩子)
2、将Branches to build 改为 */my-dev (触发钩子后,基于哪个分支去拉取代码)
2)、IDEA中将本地修改的代码提交push至远程 my-dev分支,触发钩子
3)、jenkins 中 tjxt-dev-build自动清理编译打包,但是不会部署,可以跟踪查看日志,看是否是构建的指定分支代码
4)、tjxt-dev-build构建完毕后,在dev组中选择需要部署的微服务(修改push过代码),进行构建部署,查看日志,等待构建完成
5)、docker中,利用dlog -f 容器名称,查看日志,等待启动完成
6)、测试
9、 重要流程:
1、Jenkins持续集成流程图
1、本地编写代码、本地测试调试
2、push代码至gogs
3、触发gogs设置的webhook钩子(http://192.168.150.101:18080/gogs-webhook/?job=tjxt-dev-build)
4、通知jenkins触发tjxt-dev-build任务开始整个项目的构建
1)、拉取代码(jenkins配置中指定了push哪个分支会触发钩子、指定了拉取哪个分支代码)
2)、整个项目所有模块的清理、跳过测试编译、打包
3)、执行shell命令拷贝startup.sh脚本和dockerfile到某个目录
4)、等待构建完毕
5、手动触发push过代码的具体微服务构建任务(提前建好的构建任务)
1)、执行startup.sh shell脚本(指定了很多个性化的参数)
1、删除【该微服务】之前构建的镜像和正在运行的容器
2、基于dockerfile构建镜像
3、基于构建的镜像运行容器
2、几种测试方式示意图
3、项目架构图
10、虚拟机网络重启失败
1、关闭NetworkManager:service NetworkManager stop
2、禁用NetworkManager:systemctl disable NetworkManager
3、重启网络:service network restart
4、如果重启不成功,查看 /etc/sysconfig/network-scripts/ 下有无 ifcfg-eth0 文件,如果有,删除,然后重启网络
5、执行ip addr > 1.txt , vi 1.txt查看该文件中 ens33下是否有ip,如果没有,执行 dhclient ens33, 然后重启网络
Day2 - 我的课表
course - 课程
lesson - 课表 (某一个课表关联着对应的课程) tj-learning
1、支付或报名课程后,立刻加入【课表】(MQ通知) MQ通知 LessonChangeListener
2、分页查询【我的课程】 GET /lessons/page LearningLessonController
3、查询我【最近】【正在学习的课程】 GET /lessons/now LearningLessonController
4、根据ID查询【指定课程】的学习状态 GET /lessons/{courseId} LearningLessonController
5、删除课表中的某课程 DELETE /lessons/{courseId} LearningLessonController
6、退款后,立刻移除课表中的课程 MQ通知 LessonChangeListener
7、校验指定课程是否是课表中的有效课程(Feign) GET /lessons/{courseId}/valid LearningLessonController
8、统计课程学习人数(Feign) GET /lessons/{courseId}/count LearningLessonController
9、重要流程:
1、系统访问流程示意图(Nginx - 网关 - 微服务 - 拦截器 - controller ...)
2、课程业务流程图
10、涉及的重要表结构
CREATE TABLE IF NOT EXISTS `learning_lesson` (
`id` bigint NOT NULL COMMENT '主键',
`user_id` bigint NOT NULL COMMENT '学员id',
`course_id` bigint NOT NULL COMMENT '课程id',
`status` tinyint DEFAULT '0' COMMENT '课程状态,0-未学习,1-学习中,2-已学完,3-已失效',
`week_freq` tinyint DEFAULT NULL COMMENT '每周学习频率,例如每周学习6小节,则频率为6',
`plan_status` tinyint NOT NULL DEFAULT '0' COMMENT '学习计划状态,0-没有计划,1-计划进行中',
`learned_sections` int NOT NULL DEFAULT '0' COMMENT '已学习小节数量',
`latest_section_id` bigint DEFAULT NULL COMMENT '最近一次学习的小节id',
`latest_learn_time` datetime DEFAULT NULL COMMENT '最近一次学习的时间',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`expire_time` datetime DEFAULT NULL COMMENT '过期时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `idx_user_id` (`user_id`,`course_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='学生课程表';
Day3 - 学习计划和进度
1、创建学习计划
0)、需求:我的课程页面,凡是没有学习计划的课程都可以创建学习计划(向数据库课程表中保存该课程每周学习节数即可)
1)、参数:课程ID、每周学习章节数
2、提交学习记录
0)、需求:考试考完提交 或者 视频播放时定期15S提交学习记录
1)、参数:课表ID、小节ID、小节类型(考试、视频)、提交时间(做周统计)、视频时长(秒)、播放进度(秒)
2)、前端自动定期提交视频播放进度,进度超50%视为本小节已学完
3)、考试提交(结束提交),直接视为本节学完
3、根据ID查询指定课程的学习记录(供Course课程微服务调用)
0)、需求:需要查询当前课程的每一个小节基本信息、每个小节对应的学习记录(是否学习过了?学习到哪儿了?)
Course课程微服务(课程基本信息) 调用 Learning学习微服务(每个小节学习记录),最终一次性返回数据给前端
1、课表id (将来前端自动提交学习记录时需要)
2、最近学习的小节id
3、学习过的小节的记录
1)、小节的id
2)、视频的当前观看时长,单位秒
3)、是否完成学习,默认false
4、学习计划统计(按周)
0)、需求:在我的课程页面中,需要统计用户本周学习计划及进度(分页查询)
5、重要流程:
1、课程业务流程图(升级版)
2、提交学习记录业务流程
6、涉及重要表结构
CREATE TABLE IF NOT EXISTS `learning_record` (
`id` bigint NOT NULL COMMENT '学习记录的id',
`lesson_id` bigint NOT NULL COMMENT '对应课表的id',
`section_id` bigint NOT NULL COMMENT '对应小节的id',
`user_id` bigint NOT NULL COMMENT '用户id',
`moment` int DEFAULT '0' COMMENT '视频的当前观看时间点,单位秒',
`finished` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否完成学习,默认false',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '第一次观看时间',
`finish_time` datetime DEFAULT NULL COMMENT '完成学习的时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间(最近一次观看时间)',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_update_time` (`update_time`) USING BTREE,
KEY `idx_user_id` (`user_id`) USING BTREE,
KEY `idx_lesson_id` (`lesson_id`,`section_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='学习记录表';
Day4 - 播放记录优化
1、高并发读写优化方案
1)、提高单机并发(提高RT)
1、读
1)、优化代码、SQL
2)、缓存(Redis)
3)、ES
2、写
1)、优化代码、SQL
2)、同步写变异步写(MQ\多线程):有多个数据库操作,需要保证事务ACID,并发写的情况
3)、合并写请求(思想):数据库操作单一、对事物要求不高,并发写的情况
2)、搭建集群(运维)
3)、做好服务保护(Sentinel、Hystrix)
2、播放记录优化具体方案
1、方案:合并写请求 + 异步延迟队列(原理图:重要!!!)
2、原理:
0)、对非首次学完课程记录提交逻辑分支优化
1)、合并写请求
1、将多次学习记录提交至Redis,播放记录持续覆盖,【将来】一次性提交至Mysql数据库
redis命令:hset learning:record:课程ID 小节ID 小节播放记录json
java代码:redisTemplate.opsForHash().put(key, record.getSectionId().toString(), json);
2、Redis数据结构选用:Hash结构(以课表为维度去存储播放记录,大大减少大Key的数量)
Key : 课表ID
HashKey :小节ID
HashValue : 该小节的播放记录json(id、moment、finished)
2)、异步延迟队列
1、在将学习记录提交至Redis时,提交一个延时任务(20S后执行)
hget learning:record:课程ID 小节ID
redisTemplate.opsForHash().get(learning:record:课程ID, 小节ID);
2、检测本地播放进度与Redis中播放进度时候一致
3、DelayQueue(Redisson、MQ、时间轮)
getDelay:待重写:获取延时任务剩余执行时间
compareTo:待重写:对队列中的任务进行排序
take:自带方法: 获取队列中的第一个任务(阻塞)
4、@PostContract(Bean初始化阶段执行) + @PreDestory(容器销毁)
5、CompletableFuture
3、重要流程:
1、播放进度记录方案流程图
Day5 - 互动问答
1、用户端涉及接口
1、新增互动问题 【随堂1】
1)、POST、/questions
2)、入参:课程ID、章ID、小节ID、问题标题、问题描述、是否匿名、(用户ID)
3)、出参:是否成功
2、编辑互动问题 【作业1】
1)、PUT、/questions
2)、入参:章ID、小节ID、问题标题、问题描述、是否匿名
3)、出参:是否成功
3、删除互动问题 【作业2】
1)、DELETE、/questions
2)、入参:问题ID
3)、出参:是否成功
4、分页查询互动问题列表 【随堂2】
1)、GET、/questions/page
2)、入参:页码、每页大小、是否只查询自己提问的、课程ID、小节ID
3)、出参:分页对象【互动问题VO【问题标题、回答数量、提问时间、是否匿名、提问者ID、提问者昵称、提问者头像、最近一次回答的用户昵称、
最近一次问答内容、问题ID】】
4)、注意:
1、如果是匿名提问,不应展示用户个人信息
2、如果是被管理端隐藏的问题,不应返回
5、根据ID查询问题详情 【随堂3】
1)、GET、/questions/{问题ID}
2)、出参:互动问题VO【问题ID、问题标题、回答数量、提问时间、是否匿名、提问者ID、提问者昵称、提问者头像】
3)、注意:如果是匿名提问,不应展示用户个人信息
6、分页查询【回答】或【评论】列表 【作业3】
1)、GET、/replies/page
2)、入参: 页码、每页大小、
问题ID(非必填):当查询某个【问题下】的所有【回答】时,需要填写该条件
回答ID(非必填):当查询某个【回答下】的所有【评论】时,需要填写该条件
3)、出参:分页对象【回答和评论VO【回答或评论ID、回答或评论内容、是否匿名、回答者或评论者ID、回答者或评论者昵称、评论目标用户昵称、
回答或评论时间、回答下的评论数量、点赞数量、当前用户是否已点赞】】
4)、注意:
1、只有回答列表,每天数据才需要展示【评论数】,回答下的评论列表不需要展示评论数
2、管理端涉及接口
1、分页查询互动问题列表 【随堂4】
1)、GET、/admin/questions/page
2)、入参:页码、每页大小、课程名称、问题状态(未查看、已查看)、提问时间开始时间、提问时间结束时间
3)、出参:分页对象【互动问题VO【问题标题、问题描述、所属章名称、所属节名称、所属课程名称、所属课程分类名称拼接、提问者昵称
、回答数量、提问时间、是否对所有用户隐藏、查看状态(未查看、已查看)、问题ID】】
2、根据ID查询问题详情 【作业4】
1)、GET、/admin/questions/{问题ID}
2)、出参:互动问题VO【分页列表返回字段 + 老师名称】
3)、注意:如果是匿名提问,不应展示用户个人信息
3、隐藏|显示指定问题 【作业5】
4、新增回复、评论接口 【公共】【作业6】
1)、POST、/replies
2)、入参:回答的内容、是否匿名、
问题ID:动作为回答或评论时都需要填该字段,指回答或评论的哪个问题
上级回答ID(非必填):动作为一级评论时,需要填写该条件,指评论的哪个回答
目标评论ID(非必填):动作为多级评论时,需要填写该条件,指评论的哪条评论
目标用户ID(非必填):动作为评论时需要填写该条件,指评论的哪个用户(冗余)
3)、出参: 是否成功
4)、注意:
1、每当有学生提交新的回答或者评论时,需要将所属的【问题】状态重置为未查看
2、每当有新的回答时,需要记录更新最近一次回答ID到问题表,并且累计回答次数
5、分页查询【回答】或【评论】列表 【作业7】
理论上可以和用户端的这个接口合二为一,数据基本一致,但是由于管理端可以隐藏某条回答或评论,所以回答数或点赞数要比用户端多,故分开设计
6、隐藏或显示指定回答或评论 【作业8】
3、涉及重要表结构
CREATE TABLE IF NOT EXISTS `interaction_question` (
`id` bigint NOT NULL COMMENT '主键,互动问题的id',
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '互动问题的标题',
`description` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '问题描述信息',
`course_id` bigint NOT NULL COMMENT '所属课程id',
`chapter_id` bigint NOT NULL COMMENT '所属课程章id',
`section_id` bigint NOT NULL COMMENT '所属课程节id',
`user_id` bigint NOT NULL COMMENT '提问学员id',
`latest_answer_id` bigint DEFAULT NULL COMMENT '最新的一个回答的id', #提高分页查询问题时查询最新回答记录的效率
`answer_times` int unsigned NOT NULL DEFAULT '0' COMMENT '问题下的回答数量',
`anonymity` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否匿名,默认false',
`hidden` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否被隐藏,默认false', #管理端使用
`status` tinyint DEFAULT '0' COMMENT '管理端问题状态:0-未查看,1-已查看', #管理端使用
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '提问时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_course_id` (`course_id`) USING BTREE,
KEY `section_id` (`section_id`),
KEY `user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='互动提问的问题表';
CREATE TABLE IF NOT EXISTS `interaction_reply` (
`id` bigint NOT NULL COMMENT '互动问题的回答id',
`question_id` bigint NOT NULL COMMENT '互动问题问题id',
`answer_id` bigint DEFAULT '0' COMMENT '回复的上级回答id',
`user_id` bigint NOT NULL COMMENT '回答者id',
`content` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '回答内容',
`target_user_id` bigint DEFAULT '0' COMMENT '回复的目标用户id',
`target_reply_id` bigint DEFAULT '0' COMMENT '回复的目标回复id',
`reply_times` int NOT NULL DEFAULT '0' COMMENT '评论数量',
`liked_times` int NOT NULL DEFAULT '0' COMMENT '点赞数量',
`hidden` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否被隐藏,默认false',
`anonymity` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否匿名,默认false',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_question_id` (`question_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='互动问题的回答或评论';
1、学习重点
1)、对原型的分析,产出实现方案(表结构、接口设计)
2)、接口的编码
Day6 - 点赞系统
1、点赞方案设计要素
1)、通用:需要适用于各种不同的业务场景,如视频点赞、问题点赞、回复点赞、笔记点赞等等
2)、独立:可以供其他业务微服务直接调用,不产生任何的业务关联(独立微服务)
3)、并发:必须能应对较高的并发
4)、安全:做好校验措施,避免用户重复点赞
2、未加入缓存\定时任务 的点赞解决方案
点赞微服务 + MQ + 其他业务系统
1)、点赞微服务:承担各业务系统点赞、取消点赞请求
1、校验:判断当前操作是点赞还是取消点赞
1)、如果是【取消点赞】:直接根据业务ID和用户ID直接【删除数据库】点赞记录即可
2)、如果是【点赞】:根据业务ID和用户ID【查询数据库】判断是否已点赞:如果是,直接结束,如果不是,【新增点赞记录】至数据库
2、统计点赞数:根据业务ID【查询数据库】统计当前业务ID下的点赞数
3、发消息通知业务系统:组装业务ID和对应点赞数量,发送消息至MQ的交换机
4、提供点赞数查询接口:另外还需要提供根据业务ID集合查询当前用户是否对这些业务已点赞的接口:
根据用户ID和业务ID集合进行【数据库批量查询】 where userid = #{} and bizId in ('','')
2)、MQ:接收点赞微服务的消息,而后投递至业务系统,routingkey为业务类型
3)、其他业务系统
1、声明队列与交换机,bindingkey为各自的业务类型
2、消费消息:根据消息中的业务ID和点赞数量更新对应业务表中的点赞数量(如:根据回答ID更新回答表中的点赞数)
3、另外点赞目标数据分页查询时,还需要组装业务ID集合,调用点赞微服务进行是否已点赞的批量查询
3、加入缓存优化的点赞解决方案
点赞微服务 + Redis 缓存 + 定时任务 + MQ + 其他业务系统
1)、点赞微服务:承担各业务系统点赞、取消点赞请求
1、校验:判断当前操作是点赞还是取消点赞
1)、如果是【取消点赞】:将该用户ID从被点赞业务ID对应的 【Redis Set集合】中删除即可
redis命令:srem likes:set:biz:点赞目标ID 用户ID
java代码: redisTemplate.opsForSet().remove(likes:set:biz:点赞目标ID, 用户ID);
2)、如果是【点赞】:将该用户ID添加至点赞业务ID对应的 【Redis Set集合】中即可
redis命令:sadd likes:set:biz:点赞目标ID 用户ID
java代码: redisTemplate.opsForSet().add(likes:set:biz:点赞目标ID, 用户ID);
2、统计点赞数:根据业务ID【查询Redis set集合】成员数即可
redis命令:scard likes:set:biz:点赞目标ID
java代码:redisTemplate.opsForSet().size(RedisConstants.LIKES_BIZ_KEY_PREFIX + recordDTO.getBizId());
3、缓存点赞数:将点赞数缓存至【Redis zset】集合中
redis命令:zadd likes:times:type:QA score1 member1
java代码:redisTemplate.opsForZSet().add(likes:times:type:QA,业务ID,点赞数);
4、提供点赞数查询接口:另外还需要提供根据业务ID集合查询当前用户是否对这些业务已点赞的接口:
查询用户ID是否在业务ID对应的点赞列表内【使用Redis批量查询(Pipeline管道技术)】
sismember likes:set:biz:点赞目标ID 用户ID
2)、Redis缓存
1、使用【Set】结构存储:点赞信息
key(业务ID) value(已点赞的用户ID)
likes:set:biz:8728 [24,63,21,67,34,98,89,23...] ismember(likes:set:biz:8728,24) true
2、使用【ZSet】结构存储点赞数量:
key(业务类型) value(业务ID及点赞数)
likes:times:type:qa [8728 728,8712 232,8274 88,8172 921,8627 232...]
likes:times:type:note [8728 728,8712 232,8274 88,8172 921,8627 232...]
3)、定时任务
1、每隔20S执行一次
2、遍历zset结构存储的点赞数量,组装消息VO(业务ID、对应点赞数)集合
redisTemplate.opsForZSet().popMin(key, maxBizSize)
3、发送消息至MQ(routingkey为业务类型)
2)、MQ:接收点赞微服务的消息,而后投递至业务系统,routingkey为业务类型
3)、其他业务系统
1、声明队列与交换机,bindingkey为各自的业务类型
2、消费消息:根据消息中的业务ID和点赞数量【批量】更新对应业务表中的点赞数量(如:根据问题ID更新问题表中的点赞数)
3、另外点赞目标数据分页查询时,还需要组装业务ID集合,调用点赞微服务进行是否已点赞的批量查询
4、涉及接口
1)、点赞、取消点赞
2)、查询点赞状态(供其他微服务Feign远程调用)
3)、监听点赞数变更消息(点赞所属微服务监听MQ消息)
5、重要流程
1、未加入缓存的点赞解决方案流程
2、加入缓存优化的点赞解决方案流程
6、涉及重要表结构
CREATE TABLE IF NOT EXISTS `liked_record` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键id',
`user_id` bigint NOT NULL COMMENT '用户id',
`biz_id` bigint NOT NULL COMMENT '点赞的业务id',
`biz_type` VARCHAR(16) NOT NULL COMMENT '点赞的业务类型',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_biz_user` (`biz_id`,`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='点赞记录表';
Day7 - 积分系统
1、打卡(签到)
1)、POST、/sign-records
2)、入参:无
3)、出参:连续签到天数、今日签到获得的积分制
4)、注意:每天只能签到一次、连续签到有积分奖励
签到:
setbit userid:12:2023:05 0 0
setbit userid:12:2023:05 1 1
setbit userid:12:2023:05 2 0
setbit userid:12:2023:05 3 1
setbit userid:12:2023:05 4 1
setbit userid:12:2023:05 5 1
010111
& 1
[0,1,0,1,1,1]
获取指定天有没有签到
getbit userid:12:2023:05 6
一次性获取bit数据
bitfield userid:12:2023:05 GET u2 0 ==> 01 2的0次方 = 1
bitfield userid:12:2023:05 GET u4 0 ==> 0101 2的0次方 + 2的2次方 = 5
bitfield userid:12:2023:05 GET u6 0 ==> 010111 2的0次方 + 2的1次方 + 2的2次方 + 2的4次方 = 23
2、查询本月历史打卡记录
1)、GET、/sign-records
2)、入参:无
3)、出参:每天签到情况的数组[0110010101000]
3、积分相关
1、保存用户积分记录
业务操作埋点发MQ消息 -> 消费者监听对应消息加积分(保存积分记录)
2、查询当天用户每一项积分渠道获取的分数
1)、GET、/points/today
2)、入参:无
3)、出参:List<VO> :积分类型、每日积分上线、今日已获取积分
4、涉及重要表结构
#使用redis bitmap代替
CREATE TABLE `sign_record` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`user_id` bigint NOT NULL COMMENT '用户id',
`year` year NOT NULL COMMENT '签到年份',
`month` tinyint NOT NULL COMMENT '签到月份',
`date` date NOT NULL COMMENT '签到日期',
`is_ backup` bit(1) NOT NULL COMMENT '是否补签',
PRIMARY KEY (`id`),
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='签到到记录表';
CREATE TABLE IF NOT EXISTS `points_record` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '积分记录表id',
`user_id` bigint NOT NULL COMMENT '用户id',
`type` tinyint NOT NULL COMMENT '积分方式:1-课程学习,2-每日签到,3-课程问答, 4-课程笔记,5-课程评价',
`points` tinyint NOT NULL COMMENT '积分值',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_user_id` (`user_id`,`type`) USING BTREE,
KEY `idx_create_time` (`create_time`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=41 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='学习积分记录,每个月底清零';
Day8 - 排行榜
1、实时排行榜(redis高性能实时排行)
1)、在mq消费者消费加积分消息时,使用redis进行积分累加
redis命令:zincrby boards:202305 100 2
java代码:redisTemplate.opsForZSet().incrementScore(boards:202305, userId.toString(), realPoints);
2)、在统计实时的积分信息时
1、个人积分查询
1)、个人分数:
redis命令: zscore boards:202305 2
java代码:redisTemplate.boundZSetOps(boards:202305).score(userId);
2)、个人排名:
redis命令:zrevrank boards:202305 2
java代码:redisTemplate.boundZSetOps(boards:202305).reverseRank(userId);
2、实时积分榜查询
redis命令:zrevrange boards:202305 0 100 [WITHSCORES]
java代码: redisTemplate.opsForZSet().reverseRangeWithScores(boards:202305, from, from + pageSize - 1);
2、历史排行榜(数据量大、海量数据存储方案 - 分库分表)
分区
分表: 水平分表(解决数据量大的问题) + 垂直分表(解决读写性能问题)
分库:水平分库(解决数据量大的问题) + 垂直分库(解决读写性能、业务问题) + 水平拓展(解决读写性能 + 单机故障)
目前历史排行(水平分表:根据赛季进行分表)
3、实时 -> 历史 (跑批 - 分布式定时任务XXL-JOB)
1)、分布式定时任务XXL-JOB(PowerJob也可以)
1、部署调度中心
1、导入XXL-JOB调度中心的SQL
2、部署任务调度中心(docker/本地项目)
3、新建配置执行器(可以理解为对任务的分组,等待微服务注册执行器实例过来)
4、新建配置任务(任务挂在执行器下,指定任务执行具体配置)
2、自定义服务集成调度中心
1、导依赖
2、加执行器配置(yaml、配置类读取)
- 每一个项目内部都有一个执行器,要注册到调度中心的执行器管理器中,用于将来
调度中心根据任务策略进行灵活的调度
3、编写任务并且标注@Xxljob注解(指定JobHandler,与调度中心新建的任务进行关联)
4、服务跑起来
5、调度中心 - 启动
3、优点:解决了SpringTask缺陷
2)、SpringTask缺陷
1、多实例部署时,无法感知对方,同一个任务同一时间多次执行,更无法实现多实例之间的负载均衡
2、没有控制台,无法监控任务执行的情况,无法跟踪任务执行日志
3、无法对同一任务中的大量子任务进行切片处理
4、无法对任务进行编排(对任务进行依赖顺序处理)
3)、实现思路
1、定义了3个分布式任务(经过了xxl-job的任务编排)
1)、创建表
2)、从redis的zset中查询数据持久化至数据库(如何进行动态表名映射)
1、计算表名,存放至ThreadLocal
2、利用MybatisPlus的动态表名插件(通过动态表名拦截器 拦截save方法,将实体上旧的表名替换成ThreadLocal中存入的表名)
3、save的时候就会使用ThreadLocal中存入的表名
3)、清理redis中的排行数据
1、从redis zset中清除就可以了,根据redis的zset key直接删除:unlink
4、涉及重要流程
1、从实时排行转换为历史排行的整体流程
5、涉及重要表结构
CREATE TABLE `${tableName}`(
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '榜单id',
`user_id` BIGINT NOT NULL COMMENT '学生id',
`points` INT NOT NULL COMMENT '积分值',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_user_id` (`user_id`) USING BTREE
) COMMENT ='学霸天梯榜' COLLATE = 'utf8mb4_0900_ai_ci' ENGINE = InnoDB ROW_FORMAT = DYNAMIC
CREATE TABLE IF NOT EXISTS `points_record` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '积分记录表id',
`user_id` bigint NOT NULL COMMENT '用户id',
`type` tinyint NOT NULL COMMENT '积分方式:1-课程学习,2-每日签到,3-课程问答, 4-课程笔记,5-课程评价',
`points` tinyint NOT NULL COMMENT '积分值',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_user_id` (`user_id`,`type`) USING BTREE,
KEY `idx_create_time` (`create_time`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=41 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='学习积分记录,每个月底清零';
CREATE TABLE IF NOT EXISTS `points_board` (
`id` bigint NOT NULL COMMENT '榜单id',
`user_id` bigint NOT NULL COMMENT '学生id',
`points` int NOT NULL COMMENT '积分值',
`rank` tinyint NOT NULL COMMENT '名次,只记录赛季前100',
`season` smallint NOT NULL COMMENT '赛季,例如 1,就是第一赛季,2-就是第二赛季',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `idx_season_user` (`season`,`user_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='学霸天梯榜';
CREATE TABLE IF NOT EXISTS `points_board_season` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '自增长id,season标示',
`name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '赛季名称,例如:第1赛季',
`begin_time` date NOT NULL COMMENT '赛季开始时间',
`end_time` date NOT NULL COMMENT '赛季结束时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC;
Day9 - 优惠券管理
1、优惠券整体亮点:
1)、设计模式:不同的优惠券涉及不同的满减规则,使用设计模式灵活解决
2)、兑换码算法:自动发放兑换码,要求兑换码简洁、可读性好、安全、高效
3)、优惠券领取:高并发下的超卖、领取效率问题
4)、拆单:领取优惠券合并购买,将来用户退单时的拆单问题
5)、其他:购买商品时优惠券的叠加问题,优惠券超时提醒问题
2、优惠券管理业务流程下的优惠券状态:新增待发放 -> 进行中 | 就绪未开始(定时任务) -> 暂停 -> 到期(定时任务)
3、优惠券管理
1)、分页查询优惠券列表接口 (随堂1)
2)、新增优惠券接口 (随堂2)
3)、根据ID查询优惠券详情(回显)
4)、修改优惠券接口
5)、删除优惠券接口(待发放才可删除)
4、优惠券发放
1)、发放优惠券 (随堂3)
2)、暂停发放优惠券
3)、生成兑换码(随堂4)
1、要求:可读性好、数据量不大、不可重复、不可重兑、防止爆刷、高效
2、兑换码生成算法(见《兑换码生成算法》)
3、利用Spring自带线程池提交生成兑换码任务,异步生成兑换码
4)、分页查询兑换码
5、定时任务
1)、定时发放优惠券
2)、定时结束优惠券
6、涉及重要表结构
CREATE TABLE IF NOT EXISTS `coupon` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '优惠券id',
`name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '0' COMMENT '优惠券名称,可以和活动名称保持一致',
`type` tinyint NOT NULL DEFAULT '1' COMMENT '优惠券类型,1:普通券。目前就一种,保留字段',
`discount_type` tinyint NOT NULL COMMENT '折扣类型,1:满减,2:每满减,3:折扣,4:无门槛',
`specific` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否限定作用范围,false:不限定,true:限定。默认false',
`discount_value` int NOT NULL DEFAULT '1' COMMENT '折扣值,如果是满减则存满减金额,如果是折扣,则存折扣率,8折就是存80',
`threshold_amount` int NOT NULL DEFAULT '0' COMMENT '使用门槛,0:表示无门槛,其他值:最低消费金额',
`max_discount_amount` int NOT NULL DEFAULT '0' COMMENT '最高优惠金额,满减最大,0:表示没有限制,不为0,则表示该券有金额的限制',
`obtain_way` tinyint NOT NULL DEFAULT '0' COMMENT '获取方式:1:手动领取,2:兑换码',
`issue_begin_time` datetime DEFAULT NULL COMMENT '开始发放时间',
`issue_end_time` datetime DEFAULT NULL COMMENT '结束发放时间',
`term_days` int NOT NULL DEFAULT '0' COMMENT '优惠券有效期天数,0:表示有效期是指定有效期的',
`term_begin_time` datetime DEFAULT NULL COMMENT '优惠券有效期开始时间',
`term_end_time` datetime DEFAULT NULL COMMENT '优惠券有效期结束时间',
`status` tinyint DEFAULT '1' COMMENT '优惠券配置状态,1:待发放,2:未开始 3:进行中,4:已结束,5:暂停',
`total_num` int NOT NULL DEFAULT '0' COMMENT '总数量,不超过5000',
`issue_num` int NOT NULL DEFAULT '0' COMMENT '已发放数量,用于判断是否超发',
`used_num` int NOT NULL DEFAULT '0' COMMENT '已使用数量',
`user_limit` int NOT NULL DEFAULT '1' COMMENT '每个人限领的数量,默认1',
`ext_param` json DEFAULT NULL COMMENT '拓展参数字段,保留字段',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`creater` bigint NOT NULL COMMENT '创建人',
`updater` bigint NOT NULL COMMENT '更新人',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1630563495906942979 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='优惠券信息';
CREATE TABLE IF NOT EXISTS `coupon_scope` (
`id` bigint NOT NULL AUTO_INCREMENT,
`type` tinyint NOT NULL COMMENT '范围限定类型:1-分类,2-课程,等等',
`coupon_id` bigint NOT NULL COMMENT '优惠券id',
`biz_id` bigint NOT NULL COMMENT '优惠券作用范围的业务id,例如分类id、课程id',
PRIMARY KEY (`id`),
KEY `idx_coupon` (`coupon_id`)
) ENGINE=InnoDB AUTO_INCREMENT=19 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='优惠券作用范围信息';
CREATE TABLE IF NOT EXISTS `exchange_code` (
`id` int NOT NULL COMMENT '兑换码id',
`code` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '兑换码',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '兑换码状态, 1:待兑换,2:已兑换,3:兑换活动已结束',
`user_id` bigint NOT NULL DEFAULT '0' COMMENT '兑换人',
`type` tinyint NOT NULL DEFAULT '1' COMMENT '兑换类型,1:优惠券,以后再添加其它类型',
`exchange_target_id` bigint NOT NULL DEFAULT '0' COMMENT '兑换码目标id,例如兑换优惠券,该id则是优惠券的配置id',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`expired_time` datetime NOT NULL COMMENT '兑换码过期时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `index_status` (`status`) USING BTREE,
KEY `index_config_id` (`exchange_target_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='兑换码';
Day10 - 领取优惠券
1、目标:
1)、弄清楚两种不同的优惠券领取方式在数据校验、业务校验上的差异;
2)、掌握企业在应对并发、线程安全问题时的多种常见解决方案
2、涉及接口
1)、优惠券中心 - 查询发放中的优惠券列表
2)、优惠券中心 - 用户手动领取优惠券
3)、个人中心 - 兑换码兑换优惠券
4)、个人中心 - 分页条件查询我的优惠券
3、并发:超卖问题(修改),场景:大量的不同用户请求涌入抢同一个优惠券
1、产生的原因:未保证【查询已发放数量】和【更新已发放数量】的原子性,给了别的线程可乘之机
2、解决:加【锁】
1)、悲观锁:效率低(synchronized)
2)、乐观锁:
1、更新的时候,使用issue_num作为判定条件: where -> issue_num = #{oldIssueNum} //产生的问题:大量的请求抢券失败
2、改进:使用issue_num跟total_num对比作为判定条件: where -> issue_num < total_num
4、并发:超卖问题(新增),场景:大量的同一用户请求涌入抢同一个优惠券
1、产生的原因:未保证【查询当前用户领取当前优惠券的数量】和【新增用户优惠券】的原子性,给了别的线程可乘之机,新增多个用户优惠券
2、解决:加【锁】
1)、悲观锁:synchronized,锁对象为当前用户ID
1、锁失效场景1:锁对象使用的是【userId.toString()】:每一个线程都会有自己的一把锁,因为toString是new的String对象
解决:userId.toString().intern() : 从常量池中获取该String的值
2、锁失效场景2:【事务 + 锁边界】问题,事务AOP的粒度比锁的粒度大,导致事务还未提交,锁就已经释放 ()
解决:将锁范围扩大:在同步代码块中去调用事务方法,让事务提交后再释放锁
由此带来的问题:事务失效了(同类中一个没有事务的方法调用了一个有事务的方法)
2)、乐观锁:
不是对数据进行修改,不适用!!
5、事务失效问题
1)、方法未被public修饰:未产生代理
2)、异常被try、catch捕获了
3)、同类中一个没有事务的方法调用了一个有事务的方法
4)、事务传播行为设定有误
5)、抛出的异常类型和声明回滚的异常类型不匹配
6)、方法被final修饰
注意:事务和分布式锁切面顺序导致的【锁失效场景】问题(本质:事务 + 锁边界问题)
6、涉及重要表结构
CREATE TABLE IF NOT EXISTS `user_coupon` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户券id',
`user_id` bigint NOT NULL COMMENT '优惠券的拥有者',
`coupon_id` bigint NOT NULL COMMENT '优惠券模板id',
`term_begin_time` datetime DEFAULT NULL COMMENT '优惠券有效期开始时间',
`term_end_time` datetime NOT NULL COMMENT '优惠券有效期结束时间',
`used_time` datetime DEFAULT NULL COMMENT '优惠券使用时间(核销时间)',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '优惠券状态,1:未使用,2:已使用,3:已失效',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_coupon` (`coupon_id`),
KEY `idx_user_coupon` (`user_id`,`coupon_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户领取优惠券的记录,是真正使用的优惠券信息';
Day11 - 领取优惠券优化
1、目标:
1)、分布式锁:
1、理解分布式锁原理和使用场景
2、掌握Redisson分布式锁的使用
2)、异步领券:能利用MQ解决高并发写的问题
3)、Redis Lua脚本
2、Redisson分布式锁控制并发问题
1)、正常使用Redisson分布式锁
2)、使用AOP(MyLockAspect) + 自定义注解(@MyLock)优化改造分布式锁(只用在需要控制并发的方法上标注@MyLock注解)
1、出现问题:Redisson分布式锁失效
2、原因:AOP切面顺序问题(事务切面通知先于分布式锁切面执行,于是造成事务尚未提交,锁先释放了)
3、解决:MyLockAspect实现Ordered接口,自定义切面顺序,让MyLockAspect分布式锁切面通知先于事务切面通知执行即可
3)、MyLockAspect切面需要支持用户灵活自定义Redisson锁的类型
1、在@MyLock上增加锁类型:MyLockType
2、在MyLockAspect切面中根据MyLockType调用工厂类MyLockFactory获取对应的锁类型【简单工厂设计模式】
使用EnumMap存储锁类型与获取锁的行为(Function),效率更高
4)、使用策略模式改造取锁与取锁结果判断的逻辑
1、枚举中直接定义策略与策略逻辑
2、MyLockAspect切面中直接根据@MyLock注解中指定的策略类型获取对应的策略,执行相应tryLock逻辑即可
4)、EL表达式解析(导入的代码)
3、领券优化(异步)
1)、思路
1、将领券的判断依据数据存放在redis中,直接基于redis做判断
判断:
1)、校验发放时间
2)、校验库存
3)、校验该用户已领取的数量
redis存放的数据
1)、优惠券信息:每张优惠券的 发放开始时间、发放结束时间、总数量、每个用户限领数量
2)、用户优惠券信息:每张优惠券 被哪些用户 领取了多少张
2、校验通过,发送MQ消息,通知消费者修改优惠券已发放数量、新增用户优惠券信息
2)、redis结构
1、优惠券信息(hash结构)
大key hashkey hashvalue
发放开始时间
发放结束时间
优惠券ID 总数量
每个用户限领数量
2、用户优惠券信息(hash结构)
大key hashkey hashvalue
优惠券ID 用户ID 已领取数量
3)、改造的点
1、优惠券成功发放后:将优惠券信息缓存至redis
2、暂停发放优惠券时:删除优惠券缓存信息
再次发放时,重新设置优惠券缓存信息至redis
3、用户领取优惠券时:
1、判断缓存中是否有该优惠券信息,如果没有,不能领取
2、判断校验发放时间、库存、用户已领取的数量
4、用户领取优惠券成功时:
1、redis中缓存的优惠券库存要 -1
2、redis中缓存的该用户已领取该优惠券数量 + 1
3、发送异步领券的MQ消息(优惠券ID + 用户ID)
5、编写MQ消息消费者
1、数据库coupon表优惠券已发放数量 + 1
2、数据库user_coupon表新增用户券信息
Day12 - 优惠券使用
1、优惠券使用时经典问题解决方案
1)、优惠券范围限定问题
2)、优惠券规则计算问题
3)、优惠券叠加式最优解问题
2、内容
1)、业务流程&接口分析
1、根据原型分析优惠券相关业务流程
1)、开始 -> 商品加入购物车
2)、购物车页面 -> 去下单 -> 进入订单确认页
3)、订单确认页进入时预下单 -> 优惠券下拉框展示【最优方案】 -> 选择方案后 -> 计算优惠金额
4)、订单确认页下单
5)、支付或取消订单
6)、查询订单 -> 结束
2、实现的具体细节
1)、【预下单】
我的购物车页面选了商品点击去下单 -> 跳转订单确认页时 -> 调用预下单接口 (trade交易微服务prePlaceOrder接口)
1、预生成订单ID(下单重复提交,防止重复下单)
2、【调用优惠券微服务,查询所有优惠方案 + 具体折扣金额】,返回页面展示
2)、【下单】
订单确认页下单 -> 调用下单接口(trade交易微服务placeOrder接口)
1、为了提交订单接口安全性,仅仅提交 预订单ID、课程ID、优惠券ID,不提交优惠金额、订单金额信息
2、所以下单接口中需要【调用优惠券微服务,根据具体优惠券ID,重新查询计算某一选定方案的优惠金额以及优惠明细信息】
3、【调用优惠券微服务,核销优惠券,修改用户券使用状态为已使用,修改优惠券已使用数量 +1】
3)、【取消订单】 -> 【调用优惠券微服务,退还优惠券】
4)、申请退款 -> 退款不退券(只退每个商品的实付部分)
5)、查询订单信息 -> 【调用优惠券微服务,根据订单ID查询的优惠券的具体规则】
3、总结整体下单业务流程
1、开始
2、加入购物车
3、【预下单】
1)、预生成订单ID
2)、查询可用优惠策略(优惠券微服务)
4、【下单】
1)、查询优惠明细(优惠券微服务)
2)、创建订单
3)、核销优惠券(优惠券微服务)
5、支付或【取消订单】
1)、更新订单状态
2)、退还优惠券(优惠券微服务)
6、查询订单
1)、查询订单进度
2)、查询订单明细
3)、查询优惠券规则(优惠券微服务)
2)、定义实现优惠券规则(订单确认页面)
1、分析:根据优惠券类型(每满减、折扣、无门槛、满减),各自定义以下三个优惠券规则
1)、判断当前订单总额是否满足优惠使用规则
1、优惠券是否可用于当前订单?
2、可用于当前订单时才展示在优惠券的下拉框中
2)、计算优惠金额
1、计算订单使用优惠券后的优惠金额 (展示优惠金额在页面上)
3)、根据优惠券规则返回规则描述信息
1、生成优惠券的规则描述(下拉框中展示优惠券时的具体优惠券信息,如满199-50)
2、实现【策略设计模式】
1)、定义策略接口:Discount,内部定义好上述的所分析出的三个优惠券规则
public interface Discount {
/**
* 判断当前价格是否满足优惠券使用限制
* @param totalAmount 订单总价
* @param coupon 优惠券信息
* @return 是否可以使用优惠券
*/
boolean canUse(int totalAmount, Coupon coupon);
/**
* 计算折扣金额
* @param totalAmount 总金额
* @param coupon 优惠券信息
* @return 折扣金额
*/
int calculateDiscount(int totalAmount, Coupon coupon);
/**
* 根据优惠券规则返回规则描述信息
* @return 规则描述信息
*/
String getRule(Coupon coupon);
}
2)、定义策略工厂:DiscountStrategy
2)、定义四个优惠券规则对应的策略类,实现策略接口
3)、开发优惠券使用接口
1、查询可用优惠策略: 查询所有优惠方案 + 具体折扣金额
1)、思路分析
根据订单中商品(课程)信息,查询当前用户所有【可用】优惠券,并给出可行的优惠券方案,注意:优惠券可叠加
1、券相同时,保留优惠金额最高的方案
2、金额相同时,保留用券最少的方案
具体步骤
1、查询所有可用优惠券
满200减25、满300打9折、每满100减10、满500减60
2、根据订单总额 -> 初步筛选可用券
3、细筛 ->
1、筛选出适用课程类型范围 不包含 订单中任一课程类型的,淘汰
2、筛选出符合使用该券的课程,但是优惠券门槛 > 课程总金额的,淘汰
4、排列所有优惠方案
5、多线程计算每种方案优惠金额
6、筛选出最优解
2)、定义接口并初步筛选
POST、user-coupons/avaliable
入参:数组 (其中存放对象)对象属性 - >【课程ID、三级分类ID、课程价格】
出参:优惠券规则集合、该组合的优惠金额、优惠券ID集合
逻辑:查询该用户已领取但是没有使用的所有用户券,并进行初次筛选
3)、组合优惠方案
1、针对用户所购买课程,逐个筛选优惠券,看看该优惠券有没有可用课程,如果没有可用课程,或者没有达到使用门槛,直接淘汰该优惠券
2、将剩下的券做排列组合
4)、计算优惠明细
1、判断限定的使用范围
2、计算课程总价
3、判断是否可用
4、计算优惠金额
5、计算优惠明细
1)、折扣明细:按照商品价格在总价中的比例计算折扣明细
2)、为了防止出现折扣明细之和与折扣总金额不相等的情况,最后一个商品用总折扣减去之前的商品折扣之和
5)、并发优化
1、CompletableFuture
2、CountDownLatch
3、Collections.synchronizedList(new ArrayList<>(solutions.size()))
6)、筛选最优解
2、查询优惠明细: 根据具体优惠券ID,重新查询计算某一选定方案的优惠金额以及优惠明细信息
3、核销优惠券: 修改用户券使用状态为已使用,修改优惠券已使用数量 +1
4、退还优惠券: 优惠券核销的反向操作
5、查询优惠券规则:根据订单ID查询的优惠券的具体规则
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。