【SpringCloud】企业认证、分布式事务,分布式锁方案落地-2

目录

[高并发缓存三问 - 穿透](#高并发缓存三问 - 穿透)

缓存穿透

概念

现象举例

解决方案

[缓存穿透 - 预热架构](#缓存穿透 - 预热架构)

[缓存穿透 - 布隆过滤器](#缓存穿透 - 布隆过滤器)

布隆过滤器

布隆过滤器基本思想​编辑

了解

[高并发缓存三问 - 击穿](#高并发缓存三问 - 击穿)

缓存击穿

[高并发缓存三问 - 雪崩](#高并发缓存三问 - 雪崩)

缓存雪崩

解决方案

总结

为什么要使用数据字典?

创建数据字典

查询数据字典列表

修改删除数据字典

修改

删除

用户端查询数据字典列表

缓存同步策略

Canal工作原理与Binlog

Canal

Binlog是什么

Binlog类别

官方参考

Canal监听同步流程

开启MySql的binlog

[步骤1 - 开启mysql的biglog模式](#步骤1 - 开启mysql的biglog模式)

安装配置Canal

SpringBoot集成Canal实现监听

[anal实现0侵入缓存同步 - 新增同步](#anal实现0侵入缓存同步 - 新增同步)

[Canal实现0侵入缓存同步 - 删除与修改](#Canal实现0侵入缓存同步 - 删除与修改)

[作业:缓存预热 - 刷入数据字典缓存数据​编辑](#作业:缓存预热 - 刷入数据字典缓存数据编辑)

发起审核创建公司

Feign远程查询企业HR数

远程调用

上传企业授权书​编辑

绑定企业HR信息

提交企业审核Seata日期转换采坑

刷新用户最新信息

HR查询企业审核状态


高并发缓存三问 - 穿透

缓存穿透

概念

缓存穿透:是指查询一个在数据库中不存在的数据,缓存和数据库中都不会命中(也就是都查询不到),在我们编写代码的时候,一般数据库查询不到的是不会写入到redis的,但是前端请求却会绕过redis直接打在数据库上,高并发时造成数据库宕机。如此一来,就失去了redis存在的意义,往往黑客可能会处于某种目的来攻击服务器,又或者一些爬虫系统也会造成这个现象的出现。

可以看一下如下架构图,从图中可以看到,如果redis中没有这个key,那么所有请求都会绕过reids去访问数据库。

现象举例

打开淘宝,可以看到有很多分类。对于分类,我们可以把它们作为数据字典存储在数据库,然后再放入到缓存。当请求过来的时候直接查缓存就行了。

随便打开某一个分类,这个红框内每个分类都不一样,可以推断是分类的id。

这个id必须要缓存,肯定不可能查库的。但是这个id存在缓存的话,是可以查询到的。但是如果没有呢?没有的话如果代码逻辑不好就可能直接打在数据库上来。

我们来改一下这个id,随便改为一个不存在的数字,看看会发生什么变化:

这个时候直接跳转为404的页面了,因为不存在啊,所以返回了一个错误页面。

上面的流程是没有问题的,但是我们在编写代码的时候,可能会造成缓存穿透。可以看如下代码:

上面的红框代码,很有可能是我们平时会做的一个步骤,判空操作,但是正因为判空了,不往redis中设置,那么最后再查询的时候,redis反而会没有值,从而被请求绕过,进入到数据库。

解决方案

  • 对不存在的key做空值处理。(不管是否为空,都存在redis中)

缓存穿透 - 预热架构

  • 缓存预热。在项目启动的时候,预先把数据库的热点数据放入redis中,在前端请求的时候,只查redis,不查数据库。那么如此一来,哪怕查到空值,也不会绕过了redis,请求也不会到数据库了。

可以构建一个缓存服务,它是一个微服务节点,里面包含了很多的接口,比如刷新行业,可以直接手动发一个get请求某个接口,则刷新行业数据,或者其他数据都行,因为我们是直接查询redis的,把redis作为数据库,所以一旦有些数据在数据库里更新了,为了测试方便,我们会手动更新,也方便在线上的时候也能手动更新一些数据。

缓存穿透 - 布隆过滤器

  • 布隆过滤器:布隆过滤器可以查询并且判断某个key一定不存在,但是不能判断某个值一定存在

布隆过滤器

  • 致命弱点:

    • 无法删除
    • 误判率

    我们是做物流的,3天前的车和货,是属于过期信息,是要删除,删除以后就是不存在了,那么布隆过滤器无法删除。

    代码不去写了,只需要知道原理是啥就行了,面试的时候大概率会被问到。

布隆过滤器基本思想

了解

布谷鸟过滤器:原理同布隆过滤器,优点是可以删除

高并发缓存三问 - 击穿

缓存击穿

如上图,redis作为数据库的前置缓存,抵挡一部分的流量,我们设置数据的时候往往都是 KV 键值对,他是可以设置一个expire的有效时间的。之前我们有讲过redis的过期key策略(LRU/LFU),当一个key成为冷缓存了,那么他会有可能被清理掉,对吧。 假设这个时候,正好用户会大量请求刚刚所说的那个key的缓存数据,但是他被清理掉了,这个时候会发生什么情况?看如下图:

这个时候,用户的请求无法从redis中获得缓存数据,那么此时由于并发量十分巨大,所有的查询请求都落在了数据库上,造成数据库压力巨大,db会极易挂掉。那么这就是缓存击穿现象。 缓存击穿需要满足两个前置条件:

  1. 请求高并发;
  2. 正好某个key失效了,或者由于过期策略被删除了,还没来得及把数据缓存进redis的那一瞬间的时间。

既然有这样的现象出现,并且对整个系统造成一定的影响,如何解决?有木有解决方案?其实我们的目标其实就是要隔断请求,防止请求击穿。方案有如下:

  • 由于redis是单线程的,用户请求其实是排队一个个去访问redis,那么可以让第一个请求在从redis里查询key没有后,setnx一下,如果setnx成功,则去查询数据库,查询到数据后重新设置到缓存中。如果后面的请求setnx失败,则可以让他们sleep一下,比如随机1秒以内的时间,随后再去重复检查,直到有key被重新设置进redis,那么这么一来,不论有1万个还是1亿个请求,都只会有1个请求打到数据库,这样就保护了redis。
    • 但是这么做会有问题,就是如果在setnx之后,第一个请求后续操作失败了,那么setnx得不到释放,造成死锁,这样可以通过设置setnx的过期时间来做,如果第一个请求失败了,那么通过过期时间来释放锁也OK的。
    • 当然更好的方式就是用多线程的方式来做,1个线程用于去请求数据库,第二个线程用于定时检查数据是否成功被设置进redis,如果没有,就适当延长setnx的过期时间,去重置它。为什么要这么做,因为查询数据库,有时候可能会有拥堵现象,当然这么做了,业务代码会相当多也比较复杂。
  • 设置永不过期,如果你的内存够大,或者说缓存本来也不多,内存空间充足,那么所有key都是永不过期的,那么这样的击穿情况也不会出现。
  • 缓存预热,在项目初期就已经把所有需要用到的数据放入到redis中,用户请求查询的时候只查redis,哪怕key不存在,也不会请求到数据库的,这样是完全进行了隔断,保证不会出现击穿现象。

高并发缓存三问 - 雪崩

缓存雪崩

雪崩和之前的击穿有点类似,击穿是一个key失效,而雪崩是有大量的key同时过期,因为一个redis其实很庞大,免不了会有很多的key会同时到期的,这样用户请求像个散弹枪一样打在数据库上。

如何预防?

  • 均匀的随机的分配key的过期时间
  • 缓存预热

第二种情况的雪崩就是和时间相关的,比如有些金融项目,前一天和后一天的一些相关金融业务数据啊,计算比例,利率啊啥的,每天都不一样,前一天的会在零点或者凌晨1点全部失效,那么这个时候咋办?两种方案:

  • 在零点统一失效后,做个定时任务,跑一遍所有涉及到redis的接口,访问一遍,这么一来涉及到的缓存数据在查询db后会放入到redis中,那么这种方式比较low。
  • 第二种,就是你的key携带日期,比如可以这么设置:
    key=sales:amount-percent:20200505,
    value=0.98
    key=sales:amount-percent:20200506,
    value=0.965
    如此一来,在时间一过零点,获得的数据就可以立马生效,这样也就是提前设置好第二天的数据,前一天的所有数据全部失效就无所谓了。
  • 其实还有一种方式,也捎带说一下,这种方式的业务代码相对复杂一些,就是你在零点这个失效点过后,业务层的读取redis数据做延时,只放一部分的请求过去,避免请求大量访问到数据库,待redis缓存设置好以后,后续的请求才会放行。这种方式也可以,了解一下即可。

解决方案

  1. 集群高可用,哨兵或Cluster
    1. 目的,保证最基本的高可用
    2. redis的持久化,再此回复后,缓存数据重新加载
  2. Redis + Ehcache 两两结合,不把鸡蛋放在同一个篮子里
    1. 第一次请求数据库,数据放入redis和ehcache
    2. 以后的请求,会先请求ehcache,如果有返回,如果没有,查询redis,如果有返回,如果没有再查看
    3. 虽然,ehcache没有redis那么强大,但是有缓存总比没有来的好。目的:两两保障
  3. 限流,限制大流量,我只保证一部分的用户可以正常的访问,而不至于整体奔溃
    1. 只接受更小流量的请求,这样数据库可以处理应付的过来
    2. 目的,让系统整体还能运作起来,赚小钱总比不赚来的好,系统也不会崩。
    3. 举例:一艘船,只能做1000人,来了1万人,我只开放1000人的票,不然1万人都来了,船就沉了。
  4. 降级,限流在外的请求,没有处理,这个时候会引导至一些简单的信息提示页面,可以友情提示说请稍后啊之类的。甚至404也行。
    1. 限流+降级保证数据库肯定能够运转的过来。
    2. 少赚钱总比不赚钱要来的好
    3. 保证一部分用户能够真正的被系统处理。

总结

一定要保证数据库不能死,数据库死了资损会相当严重,所以限流+降级是必要的手段。

为什么要使用数据字典?

前面我们围绕的都是和缓存相关的内容,那么业务层面,目前行业分类就已经OK了。

接下来,再来看数据字典。先看如下表,平时我们做一些分类选择的时候,可能都会创建诸如以下这样的表,这不仅仅是人员规模,像企业福利待遇企业标签等等,都可以设计这样的表。

但是,像这样的表虽然可以用,但是如果一个系统,或者我们很多系统都要设计类似的表,但是业务点有很多,我们可能需要设计五六十张类似的表甚至上百张表,很显然,太多了,也不好管理。这个时候,数据字典的需求就出现了。

我们平时使用新华字典的时候,是可以根据拼音或者偏旁来进行搜索查询的,同理,我们能不能也根据不同数据的类别来分类汇总后进行筛选能?很明显,是可以的,所以我们完全可以通过一张表来管理所有的分类数据,这样就更能够便于我们对所有字典数据的管理了,查询起来也不会有太大的麻烦,也节约了表空间。可以做出如下的设计:

最终,我们开发成如下的功能结构就行了:

附带一提,我们以前的数据字典是一个独立的服务,专门提供给其他系统做字典类别查询使用的,不仅仅只是自己的本系统,公司的所有项目都可以依赖这个数据字典服务。而且,我们的国际化,也是通过数据字典来做的。

创建数据字典

BO直接复制:

service与mapper直接从逆向工具复制过来。

创建Controller:

创建接口:

查询数据字典列表

controller:

service:

修改删除数据字典

修改

先查询,后修改:

删除

用户端查询数据字典列表

作业:自己结合之前做行业列表的时候,结合redis来实现接口的查询调用。

缓存同步策略

之前我们所做的,都是手动同步,不管是双删还是延迟队列。其实这种方式都不太好。接下来来我们看看如何自动同步缓存。

  • 手动同步,过期时间:简单方便,但是时效性不及时,需要等到过期后才能查询到新的数据,而且有可能发送缓存击穿问题。
  • 缓存双写双删:修改数据库的同时修改缓存,非常及时,缓存时效是最新的。但是代码耦合度很高,对代码的侵入性很强烈,我们之前所写的代码就是如此,太过于冗余了,裹脚布一样,又臭又长。
  • 定时、(异步)延迟同步:低耦合,代码入侵少,可以同时对多个存储介质进行数据的更新,比如更新数据库的同时修改redis、mongodb、elasticsearch等,只需要一行代码发一个消息就行。但是时效性相对有一点点偏差,也就是中间同步过程有一定的数据不一致状态,同步完成后可以达到一致,在非金融场景业务下使用较多,多存储介质也可以使用。
  • 0侵入自动同步:MQ异步同步可以做到一行代码解耦,代码入侵就少了,如此之外,其实还有一种0侵入方案。那就是canal异步监听,他是阿里巴巴的一个开源工具。

Canal工作原理与Binlog

Canal

阿里因为自身的业务需求,需要把国内数据同步给国外,所以他们是异地机房的数据同步需求问题,他们通过对数据库日志的解析,可以获得增量的变更,对此变更在进行数据同步,所以就衍生出了数据订阅和消费的中间件,也就是Canal同步工具。

Canal是基于java开发的中间件,主要支持对mysql的binlog进行解析,解析完成后再利用客户端工具来获得相关的数据,增删改的数据都可以。

Binlog是什么

全称是binary-log,二进制日志。

Canal是通过解析mysql的日志来进行数据同步的,那么日志其实就是binlog,也就是mysql的二进制日志,他会记录所有非查询语句(增删改),也会包含操作的执行消耗的时间,另外mysql的binlog日志是事务安全的。

一般来说,开启binlog的话呢,其实也会有一定的性能损耗的,因为本来是不要的,现在开启了,那么他就需要耗费经理去做别的事,就跟你老板让你加班多做点事一样,你的时间精力自然就流失了。

binlog有两个非常重要的目的:

  • 数据库可以搭建主从,主从同步就是把master的binlog传递给slave来进行数据同步达到一致性的。
  • 数据恢复,可以通过binlog进行恢复。我以前的私活就是遇到过,人家以为mysql是流氓软件结果直接删了,但是binlog还在,所以我给他恢复了数据,硬生生含泪收了几千块钱。。。

Binlog类别

  • statement:记录每次增删改的语句,空间占用会比较节省。但是,数据恢复的时候可能会产生不同数据,因为执行的时间不同,尤其是now()这个函数的获取,所以为什么会有同学问我不使用数据库自己的时间,而是要通过我们手动在业务层去设置时间的原因。
  • mixed:可以认为是混合模式,一定程度上能解决不一致问题,此外在进行函数使用的时候会按照row的模式进行处理。但是某些情况之下,不一致的情况还是依然存在的。
  • row:行级别,biglog会记录每次操作后每行记录的变化。可以保证绝对的数据一致性,因为他只记录执行后的结果。缺点就是占用空间会大一些,但是磁盘空间是可以用钱解决的,能用钱解决的问题就不是问题。

这个有点类似与redis的存储模式,也就是rdb和aof以及混合模式。面试的时候可能会被问到,大家务必理解。

官方参考

https://github.com/alibaba/canal

Canal功能实现(使用场景):

  • 数据库镜像
  • 数据库实时备份
  • 索引构建和实施维护
  • 业务缓存刷新
  • 带业务逻辑的增量数据处理
  • 数据实时统计,大屏实时展示(数据采集)

数据库的实时备份是很有用的,以前我们客户就提出过这样的要求,而且我自己接的私活也有这样的要求,所以正因为canal,我们可以多赚一点。

Canal监听同步流程

参考如下图:

Canal可以监听数据库的变化,此时我们业务层代码是不需要有变动的,做好数据层面的监听,如果监听到数据变动了,比如增删改了,那么则在缓存中就可以做出对应的修改逻辑就行了。

所以,我们后面就会使用canal来落地实现咱们的0侵入方案,而且这种方案的时效性也比mq异步方案更好一些。

开启MySql的binlog

https://github.com/alibaba/canal/wiki/QuickStart

步骤1 - 开启mysql的biglog模式

并修改配置文件,添加如下内容:

[mysqld]
log-bin=/var/lib/mysql/mysql-bin # 开启 binlog,位置使用我们一开始配置的文件挂载位置
binlog-format=ROW # 选择 ROW 模式
server_id=1 # 配置 MySQL replaction 需要定义,不要和 canal 的 slaveId 重复
# 表示当前数据库开启biglog,不配置,表示所有数据库都开启
binlog-do-db=imooc-hire-dev

重启mysql:

docker restart mysql

查看data目录,会有一些biglog文件:

  • *.index是二进制的索引文件,用于记录所有的二进制文件。
  • *.00000这个就是二进制文件,记录所有的增删改语句事件。

检测binlog是否开启,在navcat中查询:

show variables like '%log_bin%'

创建账号并且授权:

sql 复制代码
CREATE USER canal IDENTIFIED BY 'canal';  
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';
-- GRANT ALL PRIVILEGES ON *.* TO 'canal'@'%' ;
FLUSH PRIVILEGES;

file:文件名

postion:日志的偏移量,主从差异就是根据这个来,可以理解为offset

安装配置Canal

docker pull canal/canal-server:v1.1.6

运行:

sql 复制代码
docker run -p 11111:11111 \
--name canal \
-e canal.destinations=imooc \
-e canal.instance.mysql.slaveId=20231111 \
-e canal.instance.master.address=192.168.1.121:3306 \
-e canal.instance.dbUsername=canal \
-e canal.instance.dbPassword=canal \
-e canal.instance.connectionCharset=UTF-8 \
-e canal.instance.filter.regex=imooc-hire-dev.data_dictionary \
--restart=always \
-d canal/canal-server:v1.1.6
  • 11111:11111:端口映射
  • --name canal:容器名称
  • --restart=always:自动重启
  • canal.destinations:集群名称,给集群取一个名字
  • canal.instance.mysql.slaveId:区别master的id,保证唯一
  • canal.instance.master.address:mysql主数据库地址
  • canal.instance.dbUsername/dbPassword:用户名和密码,也就是之前上一节课所创建的用户以及授权,如果设置root用户其实也行。
  • canal.instance.connectionCharset:字符集
  • canal.instance.filter.regex:监听数据库数据表的表达式,多个可用逗号,连接

以上环境变量如果不设置,也可以,但是需要在canal内部去修改canal-server配置也是OK的。

最后查看是否运行成功日志:

docker logs -f canal

如下命令,可以进入canal容器内部:

docker exec -it canal bash

进入目录并且查看日志:

发现一个错误,这是mysql8的密码加密方式问题:

重新通过navcat更新密码:

sql 复制代码
ALTER USER 'canal'@'%' IDENTIFIED WITH mysql_native_password BY 'canal';
FLUSH PRIVILEGES;

查看结果:

sql 复制代码
docker restart canal

再次查看日志:

随后,随意修改数据字典中的任意记录,再次查看日志:

SpringBoot集成Canal实现监听

官网地址:GitHub - NormanGyllenhaal/canal-client: spring boot canal starter 易用的canal 客户端 canal client

在资源服务中添加依赖:

XML 复制代码
<dependency>
  <groupId>top.javatool</groupId>
  <artifactId>canal-spring-boot-starter</artifactId>
  <version>1.2.1-RELEASE</version>
</dependency>

这个依赖要比官方的依赖好用一些。(最新的在maven仓库中目前下下载不来,只能使用老版本)

配置yml文件:

创建canal监听助手类:

创建一个canal映射对象,字段需要和数据库保持一致,否则监听不到

避免过多的打印日志信息:增删改后的测试

anal实现0侵入缓存同步 - 新增同步

继承通用属性和声明缓存前缀:新增:

Canal实现0侵入缓存同步 - 删除与修改

删除:修改:

前端app查询:

作业:缓存预热 - 刷入数据字典缓存数据

  • 增加一个按钮,叫做"刷新缓存"
  • 点击后,把所有不同类别的数据字典以list形式存入缓存,此功能作为缓存预热。因为目前我们的数据字典数据都是通过sql脚本导入的,不能通过canal监听来同步数据。

删除数据字典表,重新以sql的脚本导入,此刻因为有canal了,也会监听到数据,从而刷入缓存中。

那么如此canal就在咱们这章节全部讲完,本来是要在后俩周讲的,这边前置了也没关系,canal在多级缓存的时候用来同步也是非常不错的。

发起审核创建公司

controller

service

Feign远程查询企业HR数

controller:

获得企业信息的公用方法:

java 复制代码
private CompanySimpleVO getCompany(String companyId) {
    if (StringUtils.isBlank(companyId)) {
        return null;
    }

    String companyJson = redis.get(REDIS_COMPANY_BASE_INFO + ":" + companyId);
    if (StringUtils.isBlank(companyJson)) {
        // 为空,查询数据库
        Company company = companyService.getById(companyId);
        if (company == null) {
            return null;
        }
        CompanySimpleVO companySimpleVO = new CompanySimpleVO();
        BeanUtils.copyProperties(company, companySimpleVO);

        redis.set(REDIS_COMPANY_BASE_INFO + ":" + companyId,
                    new Gson().toJson(companySimpleVO),
                    8 * 60 * 60);

        return companySimpleVO;
    } else {
        // 不为空,转换为对象
        return new Gson().fromJson(companyJson, CompanySimpleVO.class);
    }
}

service:

远程调用

controller:

时间到了,容易造成缓存击穿

service:feign:别忘记在yml中添加redis的配置!!!

上传企业授权书

绑定企业HR信息

feign:

controller:

service:

企业服务调用:

企业表信息:

提交企业审核Seata日期转换采坑

service:

新版本1.5.x版本seata 中mysql8会出现序列化问题, 早起版本使用的是springboot自带的json序列化,而现在内部使用的是fastjson

需要添加依赖

分布式事务测试用seata测试是否成功回滚。出现问题,重新测试时undo_log表可能有脏数据,需要清理。

刷新用户最新信息

为了让用户信息在本地刷新,可以重新刷新一下,或者用户重登陆,在这里我们选择前者。

controller:

HR查询企业审核状态

获得企业信息,主要可以查询到企业的审核状态

企业controller:

提问:为什么这里通过用户id查询而不是企业id查询呢?企业id不是可以直接在用户信息中获得吗?

答曰:因为企业和用户可能会解绑,一旦解绑,但是前端可能也会缓存早期的企业信息,如此一来,这样前端会无法获得正确的企业信息。当然,偷懒的做法就是直接通过企业id去查,这是最简单的。

用户controller:feign:

相关推荐
雷神乐乐1 小时前
Spring学习(一)——Sping-XML
java·学习·spring
小林coding2 小时前
阿里云 Java 后端一面,什么难度?
java·后端·mysql·spring·阿里云
文大。2 小时前
2024年广西职工职业技能大赛-Spring
java·spring·网络安全
yukai080083 小时前
【最后203篇系列】002 - 两个小坑(容器时间错误和kafka模块报错
分布式·kafka
小马爱打代码3 小时前
Spring Boot 中 Map 的最佳实践
java·spring boot·spring
老猿讲编程4 小时前
OMG DDS 规范漫谈:分布式数据交互的演进之路
分布式·dds
C++忠实粉丝4 小时前
服务端高并发分布式结构演进之路
分布式
洛神灬殇5 小时前
彻底认识和理解探索分布式网络编程中的SSL安全通信机制
网络·分布式·ssl
龙哥·三年风水6 小时前
workman服务端开发模式-应用开发-vue-element-admin封装websocket
分布式·websocket·vue
智慧老师8 小时前
Spring基础分析13-Spring Security框架
java·后端·spring