RabbitMQ可靠性进制

文章目录

    • 1.生产者可靠性
      • 生产者重连
      • 生产者确认
      • 小结
    • 2. MQ的可靠性
      • 数据持久化
      • LazyQueue
      • 小结
    • 3. 消费者的可靠性
      • 消费者确认机制
      • 消费者失败处理方案
      • 业务幂等性
        • 唯一消息ID
        • 业务判断
      • 兜底方案
        • 业务判断
      • 兜底方案


1.生产者可靠性

生产者重连

在某些场景下由于网络波动,可能就会出现客户端连接MQ失败的情况,,此时我们可以在Spring配置文件中开启连接失败后的重连机制:

spring:rabbitmq:connection-timeout: 1s # 设置MQ的连接超时时间template:retry:enabled: true # 开启超时重试机制initial-interval: 1000ms # 失败后的初始等待时间multiplier: 1 # 失败后下次的等待时长倍数,下次等待时长 = initial-interval * multipliermax-attempts: 3 # 最大重试次数

需要注意的是,虽然这个重试机制在网络不稳定的时候可以有效提高消息的发送成功率。而SpringAMQP提供的重试机制是阻塞式的重试,也就是说这个操作并不是异步的会导致主线程被阻塞,从而影响业务性能。

如果对于业务性能有要求,建议禁用重试机制。如果非要使用,一定要合理配置等待时长和重试次数,或者采用异步线程来处理发送消息的代码

生产者确认

RabbitMQ提供了生产者消息确认机制,包括Publisher ConfirmPublisher Return两种,在MQ成功收到消息后会返回确认消息给生产者。而返回的结果又一下几种情况:

  • 消息投递到了MQ,但是路由失败,此时会通过Publisher Return返回路由异常原因,然后返回ACK,并return一个路由失败的消息
    • 路由失败一般不会出现,路由失败可能是这个路由并没有绑定队列,或者说代码写的有问题
    • 所以说路由失败这种情况只要开发配置和代码没有问题,几乎不可能出现
  • 第二种场景就是临时消息投递到了MQ,并且入队成功,返回ACK,告诉发送者消息投递成功
  • 第三种场景就是持久化消息投递到了MQ,并且消息持久化到磁盘后,才返回ACK告诉发送者消息投递成功

除了以上三种场景消息发送成功会返回ACK,其它的情况都会返回NACK,比如说持久化到磁盘失败了,内存满了导致内存丢失。

其中acknack属于Publisher Confirm机制,ack是投递成功;nack是投递失败。而return则属于Publisher Return机制。

通过以上机制基本能保证消息发生成功,只要失败就进行重发

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

代码实现生产者确认机制:

在Spring配置文件中添加对应配置文件:

spring:rabbitmq:publisher-confirm-type: correlated # 开启publisher confirm机制,并设置confirm类型publisher-returns: true # 开启publisher return机制

这里publisher-confirm-type有三种模式可选:

  • none:关闭confirm机制
  • simple:同步阻塞等待MQ的回执
  • correlated:MQ异步回调返回回执

一般我们推荐使用correlated,回调机制。

定义ReturnCallback

每个RabbitTemplate只能配置一个ReturnCallback,因此我们可以在配置类中统一设置。我们在publisher模块定义一个配置类:

@Configuration
@Slf4j
public class MqConfig implements ApplicationContextAware {@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {@Overridepublic void returnedMessage(ReturnedMessage returnedMessage) {log.debug("收到return callback消息:exchange:{},code:{},replyText:{},returnMsg:{}",returnedMessage.getExchange(),returnedMessage.getReplyCode(),returnedMessage.getReplyText(),returnedMessage.getMessage());}});}
}

定义ConfirmCallback

由于每个消息发送时的处理逻辑不一定相同,因此ConfirmCallback需要在每次发消息时定义。具体来说,是在调用RabbitTemplate中的convertAndSend方法时,多传递一个参数:

@Test
void confirmCallback() throws InterruptedException {// 1.创建CorrelationDataCorrelationData correlationData = new CorrelationData();// 2. 给Future添加confirmCallbackcorrelationData.getFuture().addCallback(new ListenableFutureCallback<CorrelationData.Confirm>() {@Overridepublic void onFailure(Throwable ex) {// 该异常是Spring AMQP的异常,和RabbitMQ的无关log.error("future消息发送失败:{}",ex);}@Overridepublic void onSuccess(CorrelationData.Confirm result) {if (result.isAck()){log.debug("消息发送成功,收到确认ACK");} else {log.error("消息发送失败,收到NACK,原因: {}",result.getReason());}}});// 3. 发送消息rabbitTemplate.convertAndSend("test.direct", "red", "hello, confirm callback", correlationData);
}

这里的CorrelationData中包含两个核心的东西:

  • id:消息的唯一标示,MQ对不同的消息的回执以此做判断,避免混淆(默认构造方法使用了UUID)

    public CorrelationData() {this.id = UUID.randomUUID().toString();
    }
    
  • SettableListenableFuture:回执结果的Future对象,利用回调函数异步处理确认消息

注意事项

开启生产者确认也是比价消耗MQ的性能的,一般也是不建议开启的,生产者确认消息主要有以下几种场景:

  • 一种是路由失败,一般是RoutingKey失败导致的,还有就是交换机名称错误,这也就是编码问题
  • MQ内部故障,这种场景出现概率比价低除非对消息可靠性要求较高的业务才需要开启,并且只要开启ConfirmCallback处理NACK就可以了

因为生产者确认机制需要额外的网络何系统资源开销,所以如果非要使用生产者确认机制,只需要开启Publisher Confirm机制,并且只是处理NACK消息,进行指定次数的重试,如果依然失败记录异常消息即可。

Publisher Return这种一般由于配置或者编码出现问题的根本不需要关心,因为路由出现问题一般是业务代码的问题

小结

如何保证生产发送消息的可靠性?

  1. 首先我们可以在配置文件中开启生产者重连机制,避免网络波动场景下客户端连接MQ失败
  2. 如果是其他原因导致的失败,RabbitMQ还支持生产者确认机制,接着开启生产者确认机制,根据返回的ACK和NAC来确认是否重发消息
  3. 通过以上手段基本可以保证生产者发送消息的可靠性
  4. 但是因为生产者重连和确认机制会增加网络和系统资源的开销,所以在大多数场景下无需开启确认和重连机制,除非对消息可靠性要求较高

2. MQ的可靠性

生产者在发送的消息到达MQ之后,如果MQ不能及时保存,也会导致消息丢失,所以MQ的可靠性我们也要非常关注。

  • 一旦MQ宕机,内存中的消息会丢失
  • 内存空间有限,当消费者故障或处理过慢时,会导致消息积压,引发MQ阻塞

如果采用的是非持久化的消息(纯内存),当不断往MQ中发送消息,MQ的队列存储不下的时候就会发生PageOut,将存放不下的一部老消息存放到磁盘中,在PageOut的那一瞬间是无法处理消息的,也就是阻塞状态。可能就会导致消息堆积,影响性能。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

数据持久化

为了提升MQ的性能,默认情况MQ的数据都是在内存中临时存储的,只要一重启就会丢失,为了保证数据的可靠性就必须去配置数据持久化,持久化又包括:

  • 交换机的持久化
  • 队列的持久化
  • 消息的持久化

如下图在创建队列的时候就能指定创建持久化队列

设置为Durable就是持久化模式,Transient就是临时模式。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在发送消息的时候也需要指定消息为持久化的消息,而SpringAMQP提供的发送消息的方法默认就是发送的持久化的消息。

使用持久化的消息,将每次发送的消息备份到磁盘中,过一段时间清空内存中已经持久化的消息,保证消息的一个安全性,也就说某一时刻内存和磁盘都会存在同一份消息,只是后续内存会做一个清空,在清空内存的时候会有一定的性能下降,但并不会出现像存内存那样的PageOut直接阻塞无法处理消息

LazyQueue

从RabbitMQ的3.6.0版本开始,就增加了Lazy Queues的模式,也就是惰性队列。惰性队列的特征如下:

  • 接收到消息后直接存入磁盘而非内存(内存中指保留最近的一部分消息)
  • 消费者要消费消息时才会从磁盘中读取并加载到内存(也就是懒加载)
  • 支持数百万条的消息存储

在3.12版本后,所有队列都是Lazy Queue模式,无法更改

在添加队列的时候,添加x-queue-mod=lazy参数即可设置队列为Lazy模式:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

代码配置Lazy模式

@Bean
public Queue lazyQueue(){return QueueBuilder.durable("lazy.queue").lazy() // 开启Lazy模式.build();
}

使用声明式注解

@RabbitListener(queuesToDeclare = @Queue(name = "lazy.queue",durable = "true",arguments = @Argument(name = "x-queue-mode", value = "lazy")
))
public void listenLazyQueue(String msg){log.info("接收到 lazy.queue的消息:{}", msg);
}

小结

RabbitMQ如何保证消息的可靠性

  1. 首先通过配置可以让交换机、队列、以及发送的消息都持久化。这样队列中的消息会持久化到磁盘,MQ重启消息依然存在。
  2. RabbitMQ在3.6版本引入了LazyQueue,并且在3.12版本后会称为队列的默认模式。LazyQueue会将所有消息都持久化。
  3. 开启持久化和生产者确认时, RabbitMQ只有在消息持久化完成后才会给生产者返回ACK回执

3. 消费者的可靠性

当RabbitMQ向消费者投递消息以后,需要知道消费者处理消息的状态如何,因为不能保证消息投递给消费者并不代表就一定被正确消费了,因为消费者也会出现很多的异常情况,比如说:

  • 消息在投递过程中出现了网络波动
  • 消费者收到了消息,但是消费者机器突然宕机了
  • 消费者接受到消息后,没有正确处理消息导致异常

除了上诉还有其他的异常情况,从而导致消息丢失,因此RabbitMQ还需要知道消费者的处理状态,消费者处理失败就可以进行重新投递消息

消费者确认机制

为了确认消费者是否成功处理消息,RabbitMQ提供了消费者确认机制(Consumer Acknowledgement)。即:当消费者处理消息结束后,应该向RabbitMQ发送一个回执,告知RabbitMQ自己消息处理状态。回执有三种可选值:

  • ack:成功处理消息,RabbitMQ从队列中删除该消息
  • nack:消息处理失败,RabbitMQ需要再次投递消息
  • reject:消息处理失败并拒绝该消息,RabbitMQ从队列中删除该消息

一般使用reject这种方式情况比较少,消息处理失败比如说消息格式有问题啥的(消息转换异常),而这种情况一般使用try/chtch机制捕获,消息处理成功返回ACK,失败就返回nack。

由于消息回执的处理代码比较统一,因此SpringAMQP帮我们实现了消息确认。并允许我们通过配置文件设置ACK处理方式,有三种模式:

  • none:不处理。即消息投递给消费者后立刻ack,消息会立刻从MQ删除。非常不安全,不建议使用
  • manual:手动模式。需要自己在业务代码中调用api,发送ackreject,存在业务入侵,但更灵活
  • auto:自动模式。SpringAMQP利用AOP对我们的消息处理逻辑做了环绕增强,当业务正常执行时则自动返回ack. 当业务出现异常时,根据异常判断返回不同结果:
    • 如果是业务异常,会自动返回nack
    • 如果是消息处理或校验异常,自动返回reject;

返回Reject的常见异常有:

Starting with version 1.3.2, the default ErrorHandler is now a ConditionalRejectingErrorHandler that rejects (and does not requeue) messages that fail with an irrecoverable error. Specifically, it rejects messages that fail with the following errors:

  • o.s.amqp…MessageConversionException: Can be thrown when converting the incoming message payload using a MessageConverter.
  • o.s.messaging…MessageConversionException: Can be thrown by the conversion service if additional conversion is required when mapping to a @RabbitListener method.
  • o.s.messaging…MethodArgumentNotValidException: Can be thrown if validation (for example, @Valid) is used in the listener and the validation fails.
  • o.s.messaging…MethodArgumentTypeMismatchException: Can be thrown if the inbound message was converted to a type that is not correct for the target method. For example, the parameter is declared as Message but Message is received.
  • java.lang.NoSuchMethodException: Added in version 1.6.3.
  • java.lang.ClassCastException: Added in version 1.6.3.

通过Spring的配置文件,可以配置SpringAMQP的ACK处理方式()

spring:rabbitmq:listener:simple:acknowledge-mode: auto # 自动ack

消费者失败处理方案

根据上诉的消费者处理机制,消费者出现异常后消息会不断重新进入到队列会哦只能,再次重新尝试发送给消费者,那么此时消费者再次消费消息又报错,消息又会重新入队,知道消息处理完成。

假设极端情况消费者就是无法处理该消息,那么消息就会无限循环,导致mq的消息处理表现造成不小的系统开销,影响系统性能。

不过出现这种极端情况的概率是非常低的,针对上诉情况Spring又为开发者提供了消费者失败重试机制,在消费者出现异常的时候利用本地重试,而不是不断的将消息发送到mq的队列中,

修改Spring的配置文件

spring:rabbitmq:listener:simple:retry:enabled: true # 开启消费者失败重试initial-interval: 1000ms # 初识的失败等待时长为1秒multiplier: 1 # 失败的等待时长倍数,下次等待时长 = multiplier * last-intervalmax-attempts: 3 # 最大重试次数stateless: true # true无状态;false有状态。如果业务中包含事务,这里改为false
  • 消费者在失败后消息没有重新回到MQ无限重新投递,而是在本地重试了3次
  • 本地重试3次以后,抛出了AmqpRejectAndDontRequeueException异常。查看RabbitMQ控制台,发现消息被删除了,说明最后SpringAMQP返回的是reject

结论:

  • 开启本地重试时,消息处理过程中抛出异常,不会requeue到队列,而是在消费者本地重试
  • 重试达到最大次数后,Spring会返回reject,消息会被丢弃

业务幂等性


在程序开发中业务幂等性指的是同一个业务,执行一次或者执行多次对业务状态的影响是一致的,也就执行一次或者多次是没有什么区别的。

比如:

  • 使用id来删除一条数据
  • 使用id来查询一条数据

数据的删除和查询操作通常都是幂等的,但是数据的更新操作往往不是幂等的,如果重复执行就可能出现不可预期的结果。比如说:

  • 取消订单:取消订单在恢复库存的时候,如果用户点击多次就可能出现库存重复增加的情况
  • 退款业务:重复的退款操作会对商家操作不小的损失。

所以在编写业务代码的时候要尽可能的避免业务被重复执行,然后实际业务中有很多业务场景都会被重复提交,比如说:

  • 用户网络卡顿频繁提交表单
  • 多个服务之间的调用重试
  • MQ的将同一条消息重复投递

因此,我们必须想办法保证消息处理的幂等性。这里给出两种方案:

  • 唯一消息ID
  • 业务状态判断
唯一消息ID

唯一消息ID其实就是个每一条消息一个UUID,然后将消息投递给消费者。

  1. 消费者接收到消息将消息处理后,将消息ID保存到数据库
  2. 如果下次又处理相同的消息,去数据库中查询是否存在即可

SpringAMQP的MessageConverter自带了MessageID的功能,我们只要开启这个功能即可。

@Bean
public MessageConverter messageConverter(){// 1.定义消息转换器Jackson2JsonMessageConverter jjmc = new Jackson2JsonMessageConverter();// 2.配置自动创建消息id,用于识别不同消息,也可以在业务中基于ID判断是否是重复消息jjmc.setCreateMessageIds(true);return jjmc;
}

该方案需要增加一个查和一个写数据库的操作对性能上有一定影响,还有一个需要保存消息ID也增加了耦合性,对业务有一定侵入

业务判断

我们还可以通过业务逻辑的判断来保证业务的幂等性。假设有一个支付场景,有已支付和未支付状态,在修改前判断是否已经支付,支付了就放弃本次操作。

通过MySQL的行锁即可实现,只有当订单为未支付的时候SQL才能执行修改成已支付状态

UPDATE `order` SET status = ? WHERE id = ? AND status = 1

兜底方案

MQ能保证99%的可能,但并不是百分之百,所以在某些时候我们可以做一些兜底方案,保证多个服务之间的订单状态一致。

比如说支付服务支付完成后MQ消息通知失败,就可以通过定时任务主动去查询支付状态来去更新订单状态


案需要增加一个查和一个写数据库的操作对性能上有一定影响,还有一个需要保存消息ID也增加了耦合性,对业务有一定侵入

业务判断

我们还可以通过业务逻辑的判断来保证业务的幂等性。假设有一个支付场景,有已支付和未支付状态,在修改前判断是否已经支付,支付了就放弃本次操作。

通过MySQL的行锁即可实现,只有当订单为未支付的时候SQL才能执行修改成已支付状态

UPDATE `order` SET status = ? WHERE id = ? AND status = 1

兜底方案

MQ能保证99%的可能,但并不是百分之百,所以在某些时候我们可以做一些兜底方案,保证多个服务之间的订单状态一致。

比如说支付服务支付完成后MQ消息通知失败,就可以通过定时任务主动去查询支付状态来去更新订单状态


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/34554.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【专项测试】限流测试

简介 限流的目的是防止恶意请求、恶意攻击&#xff0c;或者防止流量超出系统峰值时保护系统免受灭顶之灾。 限流的具体做法是是通过对并发访问/请求进行限速或者对一个时间窗口的请求进行限速在保护系统&#xff0c;一旦达到限制速率则可以拒绝服务&#xff08;定向到错误页&a…

Qt-D指针与Q指针的设计哲学

文章目录 前言PIMLP与二进制兼容性D指针Q指针优化d指针继承Q_D和Q_Q 前言 在探索Qt源码的过程中会看到类的成员有一个d指针&#xff0c;d指针类型是一个private的类&#xff0c;这种设计模式称为PIMPL&#xff08;pointer to implementation&#xff09;&#xff0c;本文根据Q…

ctf web入门知识合集

文章目录 01做题思路02信息泄露及利用robots.txt.git文件泄露dirsearch ctfshow做题记录信息搜集web1web2web3web4web5web6web7web8SVN泄露与 Git泄露的区别web9web10 php的基础概念php的基础语法1. PHP 基本语法结构2. PHP 变量3.输出数据4.数组5.超全局变量6.文件操作 php的命…

LangChain 工作流编排

文章目录 LCEL流式调用案例invoke的异步调用异步流中的事件 LCEL LangChain Expression Language&#xff0c;是一种强大的工作流编排工具&#xff0c;可以从基本组件构建复杂的任务链&#xff08;Chain&#xff09;&#xff0c;有如下亮点&#xff1a; 流式支持&#xff1b;…

PyTorch 深度学习实战(14):Deep Deterministic Policy Gradient (DDPG) 算法

在上一篇文章中&#xff0c;我们介绍了 Proximal Policy Optimization (PPO) 算法&#xff0c;并使用它解决了 CartPole 问题。本文将深入探讨 Deep Deterministic Policy Gradient (DDPG) 算法&#xff0c;这是一种用于连续动作空间的强化学习算法。我们将使用 PyTorch 实现 D…

3.14-1列表

列表 一.列表的介绍和定义 1 .列表 类型: <class list> 2.符号:[] 3.定义列表: 方式1:[] 通过[] 来定义 list[1,2,3,4,6] print(type(list)) #<class list> 方式2: 通过list 转换 str2"12345" print(type(str2)) #<class str> list2lis…

Java集合 - HashMap

HashMap 是 Java 集合框架中的一个重要类&#xff0c;位于 java.util 包中。它实现了 Map 接口&#xff0c;基于哈希表的数据结构来存储键值对&#xff08;key-value pairs&#xff09;。HashMap 允许使用 null 作为键和值&#xff0c;并且是非同步的&#xff08;非线程安全的&…

有效的山脉数组 力扣941

一、题目 给定一个整数数组 arr&#xff0c;如果它是有效的山脉数组就返回 true&#xff0c;否则返回 false。 让我们回顾一下&#xff0c;如果 arr 满足下述条件&#xff0c;那么它是一个山脉数组&#xff1a; arr.length > 3在 0 < i < arr.length - 1 条件下&am…

本地部署Spark集群

部署Spark集群大体上分为两种模式&#xff1a;单机模式与集群模式 大多数分布式框架都支持单机模式&#xff0c;方便开发者调试框架的运行环境。但是在生产环境中&#xff0c;并不会使用单机模式。 下面详细列举了Spark目前支持的部署模式。 &#xff08;1&#xff09;Local…

前端---初识HTML(前端三剑客)

1.HTML 先为大家介绍几个学习前端的网站&#xff1a;菜鸟教程&#xff0c;w3school&#xff0c;CSS HTML&#xff1a;超文本标记语言 超⽂本: ⽐⽂本要强⼤. 通过链接和交互式⽅式来组织和呈现信息的⽂本形式. 不仅仅有⽂本, 还可能包含图⽚, ⾳频, 或者⾃已经审阅过它的学者…

AcWing 4905. 面包店 二分

类似还有一个题是二分&#xff0c;用区间来判断是否有解 这个爆long long 有点坑了 const int N 1e2 10;LL n,tc,Tm; LL a[N],b[N],c[N];bool check(LL mid) {LL minx max(0LL,mid 1 - Tm),maxx min(tc - 1LL,mid);//将y转为x的函数,此时判断x是否有解//枚举所有客户的需…

SpringBoot 第一课(Ⅲ) 配置类注解

目录 一、PropertySource 二、ImportResource ①SpringConfig &#xff08;Spring框架全注解&#xff09; ②ImportResource注解实现 三、Bean 四、多配置文件 多Profile文件的使用 文件命名约定&#xff1a; 激活Profile&#xff1a; YAML文件支持多文档块&#xff…

2025年西安交通大学少年班招生考试初试数学试题(初中组)

1、已知正整数 x 、 y 、 z x、y、z x、y、z 满足 x y z 2025 xyz2025 xyz2025 &#xff0c; x 2 y y 2 z z 2 x x y 2 y z 2 z x 2 x^2yy^2zz^2xxy^2yz^2zx^2 x2yy2zz2xxy2yz2zx2&#xff0c;则 x 、 y 、 z x、y、z x、y、z 共有 ___ 组解。 2、在数 1 、 2 、 …

开发、科研、日常办公工具汇总(自用,持续更新)

主要记录汇总一下自己平常会用到的网站工具&#xff0c;方便查阅。 update&#xff1a;2025/2/11&#xff08;开发网站补一下&#xff09; update&#xff1a;2025/2/21&#xff08;补充一些AI工具&#xff0c;刚好在做AI视频相关工作&#xff09; update&#xff1a;2025/3/7&…

软件架构设计习题及复习

软件系统需求分析 系统需求模型转换为架构模型 软件架构设计 架构风格领域 难点 单选 平衡点是敏感点的一种&#xff0c;如果达到了平衡点一定要选平衡点&#xff0c;不能选敏感点添加层次不能提高系统性能&#xff0c;任何时候直接沟通性能最高效

ccf3501密码

//密码 #include<iostream> #include<cstring> using namespace std; int panduan(char a[]){int lstrlen(a);int s0;int zm0,sz0,t0;int b[26]{0},c[26]{0},d[10]{0},e0,f0;while(s<l&&l>6){if(a[s]<Z&&a[s]>A){b[a[s]-A];zm;}if(a[s…

【JavaEE进阶】Spring事务

目录 &#x1f343;前言 &#x1f334;事务简介 &#x1f6a9; 什么是事务? &#x1f6a9;为什么需要事务? &#x1f6a9;事务的操作 &#x1f340;Spring 中事务的实现 &#x1f6a9;Spring 编程式事务 &#x1f6a9;Spring声明式事务Transactional &#x1f6a9;T…

MySQL索引特性——会涉及索引的底层B+树

1 没有索引&#xff0c;可能会有什么问题 索引&#xff1a;提高数据库的性能&#xff0c;索引是物美价廉的东西了。不用加内存&#xff0c;不用改程序&#xff0c;不用调sql&#xff0c;只要执行正确的 create index &#xff0c;查询速度就可能提高成百上千倍。但是天下没有免…

给单片机生成字库的方案

Python 这段代码用来将txt文件中储存的字符串转变成二进制的像素数据 from PIL import Image, ImageFont, ImageDraw import osdef find_microsoft_yahei():"""Windows系统定位微软雅黑字体"""font_paths ["C:/Windows/Fonts/msyh.ttc&q…

01Spring Security框架

Spring Security是什么&#xff1f; Spring Security是⼀个提供身份验证、授权和针对常见攻击的保护的框架。 Spring Security做什么&#xff1f; 作为开发⼈员&#xff0c;在⽇常开发过程中需要⽤到Spring Security的场景⾮常多。事实上&#xff0c;对Web应⽤程序⽽⾔&#xf…