目录
[HR企业入驻 - 认证流程解析](#HR企业入驻 - 认证流程解析)
[HR企业入驻 - 查询企业是否存在](#HR企业入驻 - 查询企业是否存在)
[HR企业入驻 - 上传企业logo与营业执照](#HR企业入驻 - 上传企业logo与营业执照)
[HR企业入驻 - 新企业(数据字典与行业tree结构解析)](#HR企业入驻 - 新企业(数据字典与行业tree结构解析))
[行业tree - 创建节点](#行业tree - 创建节点)
[行业tree - 查询一级分类](#行业tree - 查询一级分类)
[行业tree - 查询子分类列表](#行业tree - 查询子分类列表)
[行业tree - 修改分类](#行业tree - 修改分类)
[行业tree - 删除分类](#行业tree - 删除分类)
[HR企业入驻 - 业务松耦合原则](#HR企业入驻 - 业务松耦合原则)
[HR企业入驻 - 自连接多表查询](#HR企业入驻 - 自连接多表查询)
[延迟队列 - 缓存弱一致性](#延迟队列 - 缓存弱一致性)
[延迟队列 - 插件安装与配置](#延迟队列 - 插件安装与配置)
[延迟队列 - 发送并监听延迟消息](#延迟队列 - 发送并监听延迟消息)
[延迟队列 - 延时更新缓存](#延迟队列 - 延时更新缓存)
HR企业入驻
HR企业入驻 - 认证流程解析
HR企业入驻 - 查询企业是否存在
涉及页面
根据企业状态判断
接口开发
controller:
service:
HR企业入驻 - 上传企业logo与营业执照
如果企业不存在,则跳转到创建新公司页面:
上传logo
java
@PostMapping("uploadLogo")
public GraceJSONResult uploadLogo(@RequestParam("file") MultipartFile file) throws Exception {
// 获得文件原始名称
String filename = file.getOriginalFilename();
if (StringUtils.isBlank(filename)) {
return GraceJSONResult.errorCustom(ResponseStatusEnum.FILE_UPLOAD_NULL_ERROR);
}
filename = "company/logo/" + filename;
MinIOUtils.uploadFile(minIOConfig.getBucketName(), filename, file.getInputStream());
String imageUrl = MinIOUtils.uploadFile(minIOConfig.getBucketName(),
filename,
file.getInputStream(),
true);
return GraceJSONResult.ok(imageUrl);
}
上传营业执照
java
@PostMapping("uploadBizLicense")
public GraceJSONResult uploadBizLicense(@RequestParam("file") MultipartFile file) throws Exception {
// 获得文件原始名称
String filename = file.getOriginalFilename();
if (StringUtils.isBlank(filename)) {
return GraceJSONResult.errorCustom(ResponseStatusEnum.FILE_UPLOAD_NULL_ERROR);
}
filename = "company/bizLicense/" + filename;
String imageUrl = MinIOUtils.uploadFile(minIOConfig.getBucketName(),
filename,
file.getInputStream(),
true);
return GraceJSONResult.ok(imageUrl);
}
思考
为啥上传接口不统一作为一个独立接口去提供给所有上传组件使用呢?
HR企业入驻 - 新企业(数据字典与行业tree结构解析)
跳转到选择行业与人员数量:
数据字典与枚举的区别:
- 数据字典是可以人物可控的,可以删除或新增,可以显示或者不显示;
- 枚举是固定的,有多少值就是多少,如果要改需要修改代码并且重启服务。像:男女性别,是否,则可以作为枚举会更好。
行业tree
行业tree - 创建节点
前端代码
后端代码
在网关中添加路由:
把行业service和mapper从逆向工具中拷贝到项目中:
service:
再去前端测试即可。
行业tree - 查询一级分类
前端代码
后端代码
controller:
service:
行业tree - 查询子分类列表
前端代码
后端代码
行业tree - 修改分类
前端代码
后端代码
行业tree - 删除分类
前端代码
后端代码
HR企业入驻 - 业务松耦合原则
一级列表
HR企业入驻 - 自连接多表查询
三级列表
controller:
service:
创建自定义mapper:
结合Redis提升接口QPS
顶级列表
先从redis中查询,如果没有,再从db查询并且放入redis中。
别忘记加配置:
三级列表
DB数据修改并重置Redis
增删改:一级节点
一旦对行业进行增删改,那么必须进行对redis中的已有数据进行删除或修改。
一级节点增删改,则删除Redis中现有所有的TOPINDUSTRYLIST。 在对应的增删改3个接口中增加如下:
增删改:三级节点
service:
sql脚本:
java
SELECT top.id from industry third
RIGHT JOIN industry `second`
ON third.father_id = `second`.id
RIGHT JOIN industry top
ON `second`.father_id = top.id
WHERE third.id = '1539849596215492610'
mapper:
在controller中新增一个方法去重置redis的行业list即可:
说明
为行业添加高性能缓存机制: 当前页面虽然比不过主页,但是这也是包含在主要业务中的,发布职位或者企业相关都会涉及到。
此外,我们这里不要做强关联,什么意思呢。比如把电子商务改成电商直播,用户下次修改不应该显示电商直播,只能显示为老的数据,我们不能去改,所以这就是强弱关联。用户如果要修改就修改为新的即可。
如果我们强制让他显示电商直播,那么他对别的用户来说显示就不够好了,我们并没有通知他,但是却修改了显示,所以不能做强关联。不仅仅是行业,其实很多类似的KV显示,都不能做强关联。
缓存双删原理解析
直接删除所带来的危害
容易缓存雪崩,一开始没有缓存,如果这个时候有高并发流量进来,瞬时会打中数据库,导致数据库崩溃
所以删除完毕之后,需要重新把数据设置到redis中。
缓存不一致的问题出现
按照上面的方式做了,那么有没有可能会出现其他问题呢?
这里需要进行查询并且重新覆盖,如果查询后在覆盖的同时,app端发起的请求,正好恰巧也查询到原来的脏数据,则会直接覆盖,如此导致两边的数据不一致。
到达箭头位置,正好前端app也请求一次,由于没有缓存,那么是不是前端也会从数据库查询一次呀,但是查询到的数据,可能是脏数据,因为之前的事务可能还没有提交,随后在下面的set新数据以后,被覆盖了旧数据,在某些极端情况下,会出现这样的情况,这个虽然有点钻牛角尖,但是确确实实,有概率会发生。 (我们没有在同一个service中去做,所以这样的概率其实已经规避了,如果是在同一个service里的话,则必定会出现这样的情况)
来,咱们通过下图来演示:
所以,为了确保数据两边一致,我们在存储完毕之后,再从redis中删除一次,那么后续前端app的请求则是最新数据了,如此就是双删,我也见过有的项目是三删,总共删3次。。。为了确保数据一致。
用户更新数据前,先把缓存数据删除,然后更新到数据库,再同步到redis中,哪怕redis存入不成功,那么后续用户发起请求还是可以先查库后存缓存,达到一致性。
缓存双删,用户把新数据保存到数据库后,sleep1秒或半秒后再次删除。再次删除redis中的内容可能是脏数据,如果前端再一次查询,哪怕先执行,那么查询到的也是最新的数据了。这也是一个双保险。
注意:从业务角度分析:并发请求的时候,用户的查询是很多的。如果出现了1-2秒的脏数据缓存,那么显示的数据就会有部分是老数据,但是对于整个系统来讲无所谓,没有太大的影响,而且用户的注意力是在列表上,具体是什么行业分类其实还好,没有太大的影响,因为行业哪怕修改了,相关性还是有的,所以有几秒的不一致是无所谓的,因为热点数据的并发读是很大的,一旦删除,那么这个时候由于缓存击穿,数据库可能会瞬间被炸了,直接宕机。所以务必以系统可用性为优先考虑。
阿里的内部规范,是可以允许存在脏数据的,因为哪怕有脏数据,也要保证数据库正常运作不被打死。因为数据库死了,必定有资损,会亏钱,脏数据不一致了,不会导致资损的,所以系统设计务必以高可用性为优先考虑。
双删生活小实例
延迟队列 - 缓存弱一致性
大家有没有觉得现在这种缓存更新方式太麻烦了?全靠代码控制,太复杂了?
又或者说,我们有没有这么必要有这么强的一致性让前端用户去获得,如果获得的是老数据又怎样?大不了用户以后修改呗,对系统毫无影响。用户端展示的企业行业只不过是老的数据,以后可以改啊。
我们来看一下弱一致性的表现:
所以说我们当前的场景完全可以使用弱一致性来做,因为本身在之前运营修改的前几天,只要有用户使用行业,则必定在当前运营修改之后,他们的行业就变成了老数据,所以这完全是可以行的,现在到第二天凌晨更新一下,也顶多是多了1天的用户老数据,系统是完全可以容忍的。
所以说大家在学习老师的课程的时候,我一直在强调业务场景,业务是非常重要的,我们一定要根据业务去做技术,不要为了技术而技术,不能太死板,要灵活取巧。
延迟队列 - 插件安装与配置
进入rabbitmq控制台:
docker exec -it rabbitmq bash
查看mq的插件列表
rabbitmq-plugins list
以下这个位置,如果安装好延迟队列的插件,会出现,现没有。则需要下载并配置安装
前往如下地址并且下载延迟插件: https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases
可以根据如下命令得到mq的版本号,根据版本号去下载延迟插件
没有3.9.11那么下载3.9.0也是可以的:
下载后并且拖入到linux中:
从虚拟机拷贝到docker的rabbitmq插件中:
java
docker cp rabbitmq_delayed_message_exchange-3.9.0.ez rabbitmq:/plugins
再次进入到rabbitmq控制台,可以查看到插件已经存在:
运行如下命令开启延迟插件:
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
再次查看插件列表,ok~
rabbitmq-plugins list
ctrl+d
退出控制台并且重启rabbitmq:
docker restart rabbitmq
延迟队列 - 发送并监听延迟消息
创建一个接口
前端调用代码
增加一个按钮:
java
<el-button
icon="el-icon-upload"
size="mini"
type="success"
@click="doRefreshIndustry">刷新缓存</el-button>
增加接口调用api:
javascript
export function refreshIndustry() {
return request({
url: '/industry/refreshIndustry',
method: 'post'
})
}
导入接口api:
增加按钮点击事件:
javascript
doRefreshIndustry() {
refreshIndustry().then(response => {
var data = response.data;
console.log(data);
this.$notify({
title: "刷新成功",
message: "最新行业数据将在第二天被刷新~~",
type: "success",
duration: 2000,
});
});
},
后端处理代码
延迟MQ配置类 复制一个MQ配置类,取名为如下,并且修改交换机和队列名称
交换机需要设置延迟特性:
修改绑定名与路由key:
添加一个消息属性处理器,目的是设置延迟的时间:
接口调用
java
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 调用刷新行业缓存的接口(延迟队列)
* @return
*/
@PostMapping("refreshIndustry")
public GraceJSONResult refreshIndustry() {
// 计算凌晨三点到现在的时间
LocalDateTime futureTime = LocalDateUtils.parseLocalDateTime(
LocalDateUtils.getTomorrow() + " 03:00:00",
LocalDateUtils.DATETIME_PATTERN);
// 计算当前时间和凌晨发布的时间差
Long publishTimes = LocalDateUtils.getChronoUnitBetween(LocalDateTime.now(),
futureTime,
ChronoUnit.MILLIS,
true);
// int delayTimes = publishTimes.intValue();
int delayTimes = 10*1000; // 固定时间,用于写死10秒进行延迟的测试
// 发送延迟队列
MessagePostProcessor processor = DelayConfig_Industry.setDelayTimes(delayTimes);
// 发送延迟消息
rabbitTemplate.convertAndSend(
DelayConfig_Industry.EXCHANGE_DELAY_REFRESH,
DelayConfig_Industry.DELAY_REFRESH_INDUSTRY,
"123456",
processor);
return GraceJSONResult.ok();
}
监听延迟消息:
java
@Slf4j
@Component
public class RabbitMQDelayConsumer_Industry {
@RabbitListener(queues = {DelayConfig_Industry.QUEUE_DELAY_REFRESH})
public void watchQueue(Message message, Channel channel) throws Exception {
String routingKey = message.getMessageProperties().getReceivedRoutingKey();
log.info("routingKey = " + routingKey);
String msg = new String(message.getBody());
log.info("msg = " + msg);
log.info("当前时间为:" + LocalDateTime.now());
if (routingKey.equalsIgnoreCase(DelayConfig_Industry.DELAY_REFRESH_INDUSTRY)) {
log.info("10秒后监听到延迟队列");
}
channel.basicAck(message.getMessageProperties().getDeliveryTag(),
true);
}
}
测试延迟的时间是否正确即可。
延迟队列 - 延时更新缓存
可以在此测试批量删除。
查询一级分类行业下的所有三级列表:
sql
SELECT third.*,`second`.*,`top`.* FROM industry third
left JOIN industry `second`
ON third.father_id = `second`.id
left JOIN industry top
ON `second`.father_id = top.id
WHERE third.`level` = 3
-- 优化为如下:
SELECT third.*,`top`.id as topId FROM industry third
left JOIN industry `second`
ON third.father_id = `second`.id
left JOIN industry top
ON `second`.father_id = top.id
WHERE third.`level` = 3
mapper:
由于查询出来的数据,同一个topId对应多个不同三级行业,所以可以用1对多的关系来构造这个返回对象。可以利用mybatis的resultMap来进行改造为如下:
service:
获得三级列表:
JSON格式转换一下,可以得到一个大list。
循环设置到redis中:
测试如下:
定时任务
能不能用定时任务来做? 可以!但是每天都会定时去查询,这些数据并不是每天都会修改,难得改一下,所以非必要不要查询数据库,况且这些数据还是挺多挺大的。降低数据库被查导致的风险发生。
作业:优化全量缓存同步
- 分级分类用批量,不要全量查询,修改操作了哪些,则记录id,再操作,降低数据表的查询总量。
- 不要直接查询全部三级list,上一点提出的id列表,循环后逐个查询,循环去查询进行拼接,把性能放在服务中进行损耗,不要把一个大的sql放在数据库里执行,降低数据库的损耗。
- 前端刷新控制按钮刷新次数,每天只能3次或者5次。