支付系统设计:消息重试组件封装
文章目录
- 前言
- 一、重试场景分析
- 一、如何实现重试
- 1. 扫表
- 2. 基于中间件自身特性
- 3. 基于框架
- 4. 根据公司业务特性自己实现的重试
- 二、重试组件封装
- 1. 需求分析
- 2. 模块设计
- 2.1 持久化模块
- 1. 表定义
- 2. 持久化接口定义
- 3. 持久化配置类
- 2.2 重试模块
- 1.启动
- 2.重试
- 3. 业务端使用
- 1. 引入依赖
- 2. 新增配置
- 3. 使用
- 总结
前言
如何封装一套服务自身业务开箱即用的重试组件?是个值得思考的问题!
在开发支付系统过程中,我们经常会遇到这样的业务场景:调用下游系统、回调上游系统,由于网络原因或者当时对方系统不可用导致调用失败,那么调用失败就失败了么?当然肯定不是,一般都要有重试机制。这种重试机制实现有很多方式,但是万万不可依赖其他系统的重试机制去重试你要重试调用的系统,这个原因下面分析。本篇文章就重试场景给出一个个人觉得还不错的解决方案,也是作者所在用的解决方案,如有更好的解决方案欢迎交流。
一、重试场景分析
在支付系统中我们经常会将一些非核心业务流程做成异步的,在核心主流程中往MQ写入一条相对应的待处理消息,写入成功即认为业务处理成功了,所以我们要证在消费端最大程度的保证处理成功。
在结果通知中也有失败重试策略,我们对接支付渠道如支付宝:如果不返回指定成功的报文信息其将在25小时以内完成8次通知(通知的间隔频率一般是4m,10m,10m,1h,2h,6h,15)。
这里我们分析个场景,流程很简单,如下:
支付渠道通知我们的支付系统,支付系统通知商户系统,之间为同步调用,渠道调用过来,支付系统变更订单状态,变更后调用商户系统,如果调用商户系统失败了,那么支付系统给渠道返回失败,然后过一段时间后渠道发起重试,再次调用支付系统,支付系统再调用商户系统。借助渠道的通知重试策略来完成自身的重试通知。谁要是这么设计,原地刨个坑活埋了他吧,不要觉得没有人用这种方式,事实就是真的有公司这么用。结果可想而知,不出问题只能说明做的系统没交易量,一旦有交易量,支付系统会被商户系统给拖垮掉,原因自行分析。
本篇文章呢我们以支付结果通知为例作为场景展开分析,做一个面对这种场景的统一解决方案,同时是没有使用充值VIP的RabbitMQ作为消息中间件。
既然没钱充值VIP购买其强大的重试功能,只能自己开发了。
一、如何实现重试
1. 扫表
实现重试的方式有很多种,有基于扫描表的,如下:
前置通知失败后,即落入重试表,待定时任务触发扫描表重新发起调用,这种处理方案是很多公司在用的。这种方案虽然不会像上面有拖垮系统的风险,但是问题还是很多的,如定时任务多久触发一次?有些交易对实时性要求比较高,如果第一次因为网络原因导致的失败,紧接着重试一般就能成功了,那么就把定时任务设定1s一次的频率?这种方式不再详细分析了…有点设计能力的人都不会采用这种方式吧。
2. 基于中间件自身特性
RocketMQ中间件本身已经支持重试,下文直接截图了:
3. 基于框架
针对RabbitMQ中间件spring提供的retry:
server:port:8080
spring:rabbitmq:host: xxx.xxx.xxx.xxxport: 5672username: xxxxpassword: xxxpublisher-confirm-type: correlatedlistener:simple:acknowledge-mode: manualretry:enabled: truemax-attempts: 5initial-interval: 5000max-interval: 10000
4. 根据公司业务特性自己实现的重试
如上是自己基于“指数退避策略进行延迟重试”封装的一套重试组件,也是本篇要介绍的方案。
二、重试组件封装
1. 需求分析
如何封装一套服务自身业务开箱即用的重试组件?是个值得思考的问题,但是Spring-boot已经给出了答案。我们在使用Springboot开发项目时候想要集成RabbitMQ只需要加入依赖,然后配置yml就可以使用了,一旦满足约定好的条件,Springboot则帮我们激活
所需要的Bean,那么我们是不是也可以参考其思想自己也装配重试所需的Bean。
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-amqp</artifactId><version>2.4.1</version></dependency>
决定了怎么做,然后分析业务系统特性,自己做的支付系统业务特性是:一个系统会有多个队列的消费者,并且每个队列消息处理失败后的重试次数、间隔时间也各不相同,并且达到最大失败重试次数后要入通知重试表,供后期业务系统恢复后再次发起重试。最终要的是,使用系统只需要简单配置下就可以实现上面需求,就像spring提供的retry机制一样,简单配置下就行了,不需要你知道底层原理。
2. 模块设计
从我们的架构图中可以看到,其主要分为两个模块,重试模块、持久化模块,我们逐个分析这俩模块的设计实现,首先从简单的开始,持久化模块。
2.1 持久化模块
首先没得说需要建表,需要使用starter提供的自动持久化功能就要创建starter持久化所需要的表:
1. 表定义
/*** @author Kkk* @Description: 异常通知恢复表*/
@Entity
@Table(name = "notify_recover")
public class NotifyRecover implements Serializable {/**id*/@Id@Column(name="id",insertable = false)private Long id;/** 唯一标识键 */@Column(name="unique_key")private String uniqueKey ;/** 场景码 */@Column(name="scene_code")private String sceneCode ;/** 调用方系统 */@Column(name="system_id")private String systemId;/** 通知内容 */@Column(name="notify_content")private String notifyContent ;/** 通知方式:http mq */@Column(name="notify_type")private int notifyType ;/** 交换器*/@Column(name="exchange")private String exchange ;/** 异步通知路由键 */@Column(name="notify_key")private String notifyKey ;/** 通知次数 */@Column(name="notify_num")private int notifyNum ;/** 通知状态 */@Column(name="notify_status")private String notifyStatus ;/** 备注 */@Column(name="remark")private String remark ;/** 扩展字段 */@Column(name="extend")private String extend ;/** 创建时间 */@Column(name="create_time",insertable = false)private Date createTime ;/** 修改时间 */@Column(name="update_time",insertable = false)private Date updateTime ;@Column(name="bucket")private String bucket ;// ... ...
}
2. 持久化接口定义
然后入表接口肯定也是需要的:
/*** @author Kkk* @Description: 发送失败处理*/
public interface NotifyRecoverHandler<T> {/*** 处理重发失败入重试表* @param t*/public void handlerSendFail(T t);
}
3. 持久化配置类
创建持久化配置类:
/*** @author Kkk* @Description: 持久化配置类*/
@Configuration
@ConditionalOnProperty(prefix = "spring.rabbitmq.retry",value = "recover",havingValue = "true",matchIfMissing = false)
public class JdbcHelperMqConfiguration {@Bean(name = "jdbcSelectProvider")public JdbcSelectProvider jdbcSelectProviderBean() {return new JdbcSelectProvider();}@Bean(name = "jdbcInsertProvider")public JdbcInsertProvider jdbcInsertProviderBean() {return new JdbcInsertProvider();}@Bean(name = "jdbcUpdateProvider")public JdbcUpdateProvider jdbcUpdateProviderBean() {return new JdbcUpdateProvider();}@Bean(name = "jdbcHelper")public JdbcHelper jdbcHelperBean(@Qualifier("jdbcSelectProvider")JdbcSelectProvider jdbcSelectProvider,@Qualifier("jdbcInsertProvider")JdbcInsertProvider jdbcInsertProvider,@Qualifier("jdbcUpdateProvider")JdbcUpdateProvider jdbcUpdateProvider) {return new JdbcHelperImpl(jdbcSelectProvider,jdbcInsertProvider,jdbcUpdateProvider);}@Bean(name = "notifyRecoverHandler")@ConditionalOnMissingBean(value = NotifyRecoverHandler.class)public NotifyRecoverHandler notifyRecoverHandlerBean(@Qualifier("jdbcHelper")JdbcHelper jdbcHelper) {return new DefaultNotifyRecoverHandlerImpl(jdbcHelper);}
}
此配置类的激活条件时,配置了失败是否需要入重试表配置。同时也可以不使用starter提供的入表策略,如果业务系统有自己的重试表那么就可以将失败的消息入到自定义的表中,此处预留的扩展点。
jdbcSelectProvider
、jdbcInsertProvider
、jdbcUpdateProvider
这个三个类为查询、新增、更新对应的处理类,为底层的JDBC操作。
/*** @author Kkk* @Description: select提供类*/
public class JdbcSelectProvider<T> {private static final Logger logger = LoggerFactory.getLogger(JdbcSelectProvider.class);@Resourceprivate DataSource dataSource;public JdbcSelectProvider() {}public List<T> select(String sql, Class outputClass) {return this.selectExecute(sql,outputClass);}private List<T> selectExecute(String sql, Class outputClass,Object... params) {Connection connection = null;PreparedStatement pst = null;ResultSet res = null;List<T> ts =null;try {connection = DataSourceUtils.getConnection(this.dataSource);pst = connection.prepareStatement(sql);for(int i = 0; i < params.length; ++i) {pst.setObject(i + 1, params[i]);}res = pst.executeQuery();ts = mapRersultSetToObject(res, outputClass);} catch (SQLException var7) {var7.printStackTrace();}finally {try {connection.close();pst.close();} catch (SQLException throwables) {throwables.printStackTrace();}}return ts;}@SuppressWarnings("unchecked")public List<T> mapRersultSetToObject(ResultSet rs, Class outputClass) {List<T> outputList = null;try {if (rs != null) {if (outputClass.isAnnotationPresent(Entity.class)) {ResultSetMetaData rsmd = rs.getMetaData();Field[] fields = outputClass.getDeclaredFields();while (rs.next()) {T bean = (T) outputClass.newInstance();for (int _iterator = 0; _iterator < rsmd.getColumnCount(); _iterator++) {String columnName = rsmd.getColumnName(_iterator + 1);Object columnValue = rs.getObject(_iterator + 1);for (Field field : fields) {if (field.isAnnotationPresent(Column.class)) {Column column = field.getAnnotation(Column.class);if (column.name().equalsIgnoreCase(columnName) && columnValue != null) {BeanUtils.setProperty(bean, field.getName(), columnValue);break;}}}}if (outputList == null) {outputList = new ArrayList<T>();}outputList.add(bean);}} else {logger.error("查询结果集映射失败,映射类需要@Entity注解");}} else {return null;}} catch (Exception e) {logger.error("查询结果集映射失败",e);}return outputList;}
}
jdbcHelper
对如上几个Provider进行了统一包装处理:
/*** @author Kkk* @Description:*/
public class JdbcHelperImpl implements JdbcHelper {private Logger logger = LoggerFactory.getLogger(JdbcHelperImpl.class);String s="'";private JdbcSelectProvider jdbcSelectProvider;private JdbcInsertProvider jdbcInsertProvider;private JdbcUpdateProvider jdbcUpdateProvider;ResultSetMapper<NotifyRecover> resultSetMapper = new ResultSetMapper<NotifyRecover>();public JdbcHelperImpl(JdbcSelectProvider jdbcSelectProvider, JdbcInsertProvider jdbcInsertProvider,JdbcUpdateProvider jdbcUpdateProvider) {this.jdbcSelectProvider = jdbcSelectProvider;this.jdbcInsertProvider = jdbcInsertProvider;this.jdbcUpdateProvider = jdbcUpdateProvider;}public List<NotifyRecover> selectData(String uniqueKey,String sceneCode){StringBuilder stringBuilder = new StringBuilder("SELECT * FROM notify_recover WHERE unique_key='");stringBuilder.append(uniqueKey);stringBuilder.append(s);stringBuilder.append(" AND scene_code='");stringBuilder.append(sceneCode);stringBuilder.append(s);String sql = stringBuilder.toString();List<NotifyRecover> pojoList = this.jdbcSelectProvider.select(sql, NotifyRecover.class);if(null==pojoList || pojoList.size()==0 ){logger.info("根据uniqueKey({}),sceneCode({})查询结果为空!",uniqueKey,sceneCode);return null;}return pojoList;}@Overridepublic void insertData(NotifyRecover notifyRecover) {jdbcInsertProvider.insert(notifyRecover);}@Overridepublic int updateData(NotifyRecover notifyRecover) {StringBuilder stringBuilder = new StringBuilder("UPDATE notify_recover SET notify_status='");stringBuilder.append(notifyRecover.getNotifyStatus());stringBuilder.append("', notify_num=");stringBuilder.append(notifyRecover.getNotifyNum());stringBuilder.append(" WHERE unique_key='");stringBuilder.append(notifyRecover.getUniqueKey());stringBuilder.append(s);stringBuilder.append(" AND scene_code='");stringBuilder.append(notifyRecover.getSceneCode());stringBuilder.append(s);String sql = stringBuilder.toString();int resultSet = this.jdbcUpdateProvider.update(sql);return resultSet;}
}
最后一部分持久化接口默认实现,如果业务方想使用持久化进制,并没有实现持久化接口则采用默认实现:
@Bean(name = "notifyRecoverHandler")@ConditionalOnMissingBean(value = NotifyRecoverHandler.class)public NotifyRecoverHandler notifyRecoverHandlerBean(@Qualifier("jdbcHelper")JdbcHelper jdbcHelper) {return new DefaultNotifyRecoverHandlerImpl(jdbcHelper);}
持久化默认实现:
/*** @author Kkk* @Description: 持久化默认实现*/
public class DefaultNotifyRecoverHandlerImpl implements NotifyRecoverHandler<NotifyRecover> {private Logger logger = LoggerFactory.getLogger(DefaultNotifyRecoverHandlerImpl.class);BasicThreadFactory factory = new BasicThreadFactory.Builder().namingPattern("recover-execute-thread-%d").uncaughtExceptionHandler(new NotifyRecoverThreadUncaughtExceptionHandler()).build();private ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(4,factory);private JdbcHelper jdbcHelperImpl;public DefaultNotifyRecoverHandlerImpl(JdbcHelper jdbcHelperImpl) {this.jdbcHelperImpl = jdbcHelperImpl;}@Overridepublic void handlerSendFail(NotifyRecover notifyRecover) {executor.execute(new Runnable() {@Overridepublic void run() {//采用异步持久化}});}
}
到这里就完成了持久化工作了,但是还有一个很重要的问题,怎么将此类注册为Spring中的Bean呢?方式多种,最简单的是使用@Import
标签,在重试的主配置类上引入此配置类。
@Import(JdbcHelperMqConfiguration.class)
public class RabbitMqRetrySendConfigurationMultiply {
}
2.2 重试模块
下面分析重试模块,首先重试模块我们是基于RabbitMQ死信队列来做的,关于死信、死信队列的概念这里不做解释了,
重试最主要分为启动时、运行时两部分。
1.启动
根据配置自动生成死信队列并通过对应的交换器与原队列进行路由绑定,大概流程见很久之前写的一篇博客[商户交易结果通知设计],当时只是针对支付系统通知功能做的,并没有做什么组件化,后期发现实际
项目中很多场景都需要这种重试机制,所以为了避免重复代码的编写,后期就简单的封装了下作为一个延迟重试组件以供在项目中开发作为一个组件直接引入依赖使用就行了。
要做的是如何将原来的代码片段封装到starter并装配到Spring中。
@Configuration
@EnableConfigurationProperties({RabbitMqRetryMultiplyProperties.class, SystenEnvProperties.class})
@ConditionalOnProperty(prefix = "spring.rabbitmq",value = "isRetry",havingValue = "true")
@ConditionalOnClass({ AmqpAdmin.class, RabbitTemplate.class })
@Import(JdbcHelperMqConfiguration.class)
public class RabbitMqRetrySendConfigurationMultiply {@Autowiredprivate RabbitMqRetryMultiplyProperties rabbitMqRetryMultiplyProperties;@Autowiredprivate SystenEnvProperties systenEnvProperties;@Bean(name = "rabbitMqService")public RabbitMqService rabbitMqServiceBean() {return new RabbitMqServiceImpl();}@Bean(initMethod = "start", destroyMethod = "stop")public PscCommonRetryQueueManager pscCommonRetryQueueManager(@Qualifier("rabbitMqService")RabbitMqService rabbitMqService,@Autowired(required = false) @Qualifier("notifyRecoverHandler")NotifyRecoverHandler notifyRecoverHandler) {return PscCommonRetryQueueManager.builder().configs(rabbitMqRetryMultiplyProperties.getConfigs()).retryCountFlag(SystemConstant.RETRY_COUNT_FLAG).rabbitMqService(rabbitMqService).notifyRecoverHandler(notifyRecoverHandler).applicationName(systenEnvProperties.getName()).build();}
}
即满足如下两个条件即会构建PscCommonRetryQueueManager这个Bean。
@ConditionalOnProperty
@ConditionalOnClass
初始化时候会调用其start方法,在看之前先看下配置类,需要用户配置什么东西。
/*** @author Kkk* @Description: 重试配置类*/
@Data
public class ConfigEntity implements Serializable {//重试次数private Integer retry_count=5;//重试队列名private String retry_queue_name_prefix;//死信消息失效时间计算方式:指数方式 exponentialprivate String message_expiration_type="exponential";//x-dead-letter-exchangeprivate String x_dead_letter_exchange;//x-dead-letter-exchangeprivate String x_dead_letter_routing_key;//延迟时间因子:10s。具体延迟时间计算方式:2^count*10spublic Integer delay_milliseconds=10000;//项目需要消费的队列名称public String consumer_queue_name;//消息丢失处理策略public String notify_recover_handler;
}
接下来看其start方法做了什么,首先看下类继承关系
在接口中定义方法。
/*** @author Kkk* @Description: 重试管理接口*/
public interface RetryQueueManager {/*** 启动*/void start();/*** 停止*/void stop();/*** 发送延迟消息 -可捕获异常入重试表*/boolean sendRetryMessage(Message message);/***发送消息 -可捕获异常入重试表*/boolean sendMessage(String exchange, String routingKey, String jsonString,String uniqueKey,String sceneCode);/*** 发送消息 -可捕获异常入重试表*/boolean sendMessage(String exchange, String routingKey, String jsonString);/*** 发送延迟消息-发送网络异常可以放入重试表*/boolean sendRetryMessage(Message message,String uniqueKey,String sceneCode);
}
抽象层抽取了写公共参数,具体实现由子类实现。
/*** @author Kkk* @Description: 抽象层*/
public abstract class AbstractRetryQueueManager implements RetryQueueManager {private Logger logger = LoggerFactory.getLogger(AbstractRetryQueueManager.class);// 重试处理protected NotifyRecoverHandler notifyRecoverHandler;// 消息处理protected RabbitMqService rabbitMqService;//消息重试次数标识 埋点到消息头中的字段public String retryCountFlag;//应用名称public String applicationName;//重试配置相关信息public List<RetryQueueConfigs> retryQueueConfigs;@Datapublic static final class RetryQueueConfigs {//重试次数public Integer retryCount=10;//重试队列名public String retryQueueNamePrefix;//死信消息失效时间计算方式:指数方式 exponentialpublic String messageExpirationType="exponential";//x-dead-letter-exchangepublic String xDeadLetterExchange="topic";//x-dead-letter-routing-keypublic String xDeadLetterRoutingKey;//延迟时间因子:10s。具体延迟时间计算方式:2^count*10spublic Integer delayMilliseconds;//项目需要消费的队列名称public String consumerQueueName;}@Overridepublic void start() {logger.info("开始创建重试队列!");createRetryQueue();logger.info("创建重试队列完成!");}/*** 应用启动构建重试队列*/protected abstract void createRetryQueue();@Overridepublic void stop() {}// ... ...
}
在子类实现抽象层方法createRetryQueue()
,生成死信交换器和队列并绑定,接着根据配置生成指定个说的死信队列,默认按照指数类型(延迟时间因子:10s。具体延迟时间计算方式:2^count*10s),然后将这些队列绑定到上面生成的交换器上,由于这些生成的死信队列没有消费者,所以消息过期后会再被路由到原队列中,即可又被正常消费处理,以此来达到延迟的效果,原理比较简单。
@Override
protected void createRetryQueue() {for (RetryQueueConfigs config:retryQueueConfigs) {TopicExchange topicExchange = ExchangeBuilder.topicExchange(config.getXDeadLetterExchange()).build();rabbitAdmin.declareExchange(topicExchange);Queue queue1 = QueueBuilder.durable(config.getConsumerQueueName()).build();rabbitAdmin.declareQueue(queue1);Binding binding = BindingBuilder.bind(queue1).to(topicExchange).with(config.getXDeadLetterRoutingKey());rabbitAdmin.declareBinding(binding);if(ExpirationTypeEnum.EXPONENTIAL.getCode().equals(config.getMessageExpirationType())){logger.info("申明“指数型”重试队列开始...");for (int i = 0; i < config.getRetryCount(); i++) {String queueName = null;try {Map<String, Object> args = new HashMap<String, Object>();//指定当成为死信时,重定向到args.put("x-dead-letter-exchange", config.getXDeadLetterExchange());args.put("x-dead-letter-routing-key", config.getXDeadLetterRoutingKey());String expiration = String.valueOf(Double.valueOf(Math.pow(2, i)).intValue()*config.getDelayMilliseconds());queueName = config.getRetryQueueNamePrefix() + "." + expiration;//声明重试队列,将参数带入Queue queue = QueueBuilder.durable(queueName).withArguments(args).build();rabbitAdmin.declareQueue(queue);logger.info("申明“指数型”重试队列成功[queueName:{}]", queueName);}catch (Throwable e){logger.error("申明“指数型”重试队列失败[i:{}, queueName:{}, e.message:{}],异常:", i, queueName, e.getMessage(), e);}}logger.info("申明“指数型”重试队列结束...");}}
}
2.重试
判断重试次数,消费端获取到消息后,根据消息头埋点可以获到重试次数,重试次数超过最大次数则入重试表,待后期分析处理。
/*** 判断是否超过重试次数*/
public RetryEntity isOutOfRetryCount(Message message){int messageRetryCount = getMessageRetryCount(message);RetryQueueConfigs config = getRetryConfigByOriQueue(message);boolean result=messageRetryCount>(null==config?0:config.getRetryCount())?false:true;if(!result){logger.info("超过最大重试次数,入重试表!");//... ...}return new RetryEntity(result,messageRetryCount);
}/*** 获取重试次数*/
public int getMessageRetryCount(Message message){//初始为0int count = 0;Map<String, Object> headers = message.getMessageProperties().getHeaders();if(headers.containsKey(retryCountFlag)){count = NumberUtils.toInt((String) message.getMessageProperties().getHeaders().get(retryCountFlag), 0);}return count;
}
关于重试即消费端处理失败后进行重新投递,根据重试次数计算要投递的队列名称。
@Override
public boolean sendRetryMessage(Message message) {boolean result=true;try {//从消息题中获取到消息来源--队列名称,然后根据队列名称获取到配置中心此队列配置的相关信息RetryQueueConfigs retryConfigByOriQueue = getRetryConfigByOriQueue(message);//从消息头中获取到重试次数int retryCount = getMessageRetryCount(message);//根据配置中心配置的死信消息失效时间计算方式(默认指数方式),和重试次数计算出死信队列名称后缀String expiration = getRetryMessageExpiration(retryCount,retryConfigByOriQueue);logger.info("消息重发开始[expiration:{}, retryCount:{}]", expiration, retryCount);//获取死信队列名称String queueName = getRetryQueueName(expiration,retryConfigByOriQueue);logger.info("消息重发获取重试队列[expiration:{}, retryCount:{}, queueName:{}]", expiration, retryCount, queueName);//发送消息rabbitMqService.sendRetry("", queueName, message, expiration, retryCount,retryCountFlag);logger.info("消息重发结束[expiration:{}, retryCount:{}]", expiration, retryCount);} catch (Exception e) {logger.info("({})发送重试消息失败!", JSON.toJSONString(message),e);result=false;}return result;
}
3. 业务端使用
1. 引入依赖
<dependency><groupId>com.epay</groupId><artifactId>delay-component-spring-boot-stater</artifactId><version>1.0.0-SNAPSHOT</version></dependency>
2. 新增配置
3. 使用
总结
本篇简单的介绍了下在工作中,将RabbitMQ进行简单封装作为延时组件使用,在使用时只需要简单的进行配置下就可以达到延时效果,降低了重复代码的编写,大大缩短了项目开发周期,由于工期紧张封装的starter还是比较粗糙的,还有好多地方需要斟酌打磨。
本篇也只是提供一种思想吧,在工作中可以借鉴下,避免重复劳动,将业务功能组件化,以后不管在什么项目中只要有相同业务场景就可以引入现有组件快速完成业务功能开发。
拙技蒙斧正,不胜雀跃。
相关文章:
支付系统设计:消息重试组件封装
文章目录前言一、重试场景分析一、如何实现重试1. 扫表2. 基于中间件自身特性3. 基于框架4. 根据公司业务特性自己实现的重试二、重试组件封装1. 需求分析2. 模块设计2.1 持久化模块1. 表定义2. 持久化接口定义3. 持久化配置类2.2 重试模块1.启动2.重试3. 业务端使用1. 引入依赖…...
Visual Studio 2022 c#中很实用的VS默认快捷键和原生功能
常常使用VS感觉还是有必要掌握其默认的快捷键,我这个人比较懒,不喜欢动不动就去设置快捷键,系统有就用,记住了就可以到处用,问题是像我们这种有很多个工作场所的人不可能每台电脑都去配置一下快键键。实际上我使用3dma…...
Python的30个编程技巧
1. 原地交换两个数字 Python 提供了一个直观的在一行代码中赋值与交换(变量值)的方法,请参见下面的示例: x,y 10,20 print(x,y) x,y y,x print(x,y) #1 (10, 20) #2 (20, 10) 赋值的右侧形成了一个新的元组,左侧立即解…...
MySQL:JDBC
什么是JDBC? JDBC( Java DataBase Connectivity ) 称为 Java数据库连接 ,它是一种用于数据库访问的应用程序 API ,由一组用Java语言编写的类和接口组成,有了JDBC就可以 用统一的语法对多种关系数据库进行访问,而不用担…...
C++【list容器模拟实现函数解析】
list容器&&模拟实现函数解析 文章目录list容器&&模拟实现函数解析一、list容器使用介绍二、list容器模拟实现及函数解析2.1 list结构体创建2.2 迭代器封装2.21 构造函数:2.22 前置和后置及- -2.23 解引用2.24 判断相等2.25 箭头重载2.26 第二个和第…...
(Java)试题 算法提高 约数个数
一、题目 (1)资源限制 内存限制:512.0MB C/C时间限制:1.0s Java时间限制:3.0s Python时间限制:5.0s (2)输入 输入一个正整数N (3)输出 N有几个约数 &a…...
魔法反射--java反射初入门(基础篇)
👳我亲爱的各位大佬们好😘😘😘 ♨️本篇文章记录的为 java反射初入门 相关内容,适合在学Java的小白,帮助新手快速上手,也适合复习中,面试中的大佬🙉🙉🙉。 ♨️如果文章有…...
概率统计_协方差的传播 Covariance Propagation
1. 方差的传播 误差的传播是指分析在形如的关系中,参量误差(x)对变量误差(y)的影响有多大。误差的传播与函数的微分紧密相关,本质是在利用当Δ x 不大时,。 方差计算公式: X为变量,为总体均值,N为总体例数。求变量X与均值的差的平方再求平均值,即得到方差。方差…...
大学生考研的意义?
当我拿起笔头,准备写这个话题时,心里是非常难受的,因为看到太多的学生在最好的年华,在自由的大学本应该开拓知识,提升认知,动手实践,不断尝试和试错,不断历练自己跳出学生思维圈&…...
【C++笔试强训】第三十一天
🎇C笔试强训 博客主页:一起去看日落吗分享博主的C刷题日常,大家一起学习博主的能力有限,出现错误希望大家不吝赐教分享给大家一句我很喜欢的话:夜色难免微凉,前方必有曙光 🌞。 选择题 &#x…...
toString()、equals()是什么,为啥需要重写,多种方法来重写
https://m.runoob.com/java/java-object-class.html toString() 1.为什么会有toString 子类继承父类就可以使用父类所有非私有的属性的方法。 在Java中所有类都直接或者间接继承Object类,可以说只要是Object类里面定义的非私有的属性和方法,任何类都可…...
家装材料清单中会有哪些装饰材料?
在家居装修中,业主可以根据装修公司出具的材料清单去一一采购,这样不至于有遗漏,就算采用全包的方式,通过材料清单也可以大致了解当时房子装修所用的材料,补充自己的装修知识。下面跟随小编一起了解下房子装修材料中所…...
【C++初阶】6. CC++内存管理
1. C/C内存分布 我们先来看下面的一段代码和相关问题 int globalVar 1; static int staticGlobalVar 1; void Test() {static int staticVar 1;int localVar 1;int num1[10] { 1, 2, 3, 4 };char char2[] "abcd";const char* pChar3 "abcd";int* …...
【数据结构】万字超详解顺序表(比细狗还细)
我这个人走得很慢,但是我从不后退。 ——亚伯拉罕林肯 目录 一.什么是线性表? 二.什么是顺序表? 三.接口函数的实现 1.创建工程 2.构造顺序表 3.初始化顺序表 3.初始化顺序表 4.顺序表的尾插 5.顺序…...
yolov5 剪枝、蒸馏、压缩、量化
文章大纲 剪枝推理优化YOLOv5 剪枝可能出现的问题参考文献与学习路径考察神经网络时期重要的激活函数sigmoid和tanh,它们有一个特点,即输入值较大或者较小的时候,其导数变得很小,而在训练阶段(详见1.2.3节),需要求取多个导数值,并将每层得到的导数值相乘,这样一旦层数…...
如何用python代码,更改照片尺寸,以及更换照片底色
前言 python浅浅替代ps?如何用代码来p证件照并且更换底色? 唉,有个小姐姐给我扔了张照片,叫我帮忙给她搞成证件照的尺寸还得换底色,她说自己忙的很 可惜电脑上没有ps只有pycharm,没得办法只能来试试看代…...
【pygame游戏】Python实现蔡徐坤大战篮球游戏【附源码】
前言 话说在前面,我不是小黑子~😏 本文章纯属技术交流~娱乐 前几天我获得了一个坤坤打篮球的游戏,也给大家分享一下吧~ 好吧,其实并不是这样的游戏,往下慢慢看吧。 准备工作 开发环境 Python版本:3.7.8 …...
通过指针引用字符串详解,以及字符指针变量和字符数组的比较
在平常的案例中已大量地使用了字符串,如在 printf函数中输出一个字符串。这些字符串都是以直接形式 (字面形式) 给出的,在一对双引号中包含若干个合法的字符。在本节中将介绍使用字符串的更加灵活方便的方法:通过指针引用字符串。 目录 一、…...
Vue基本整合(一)
NPM安装npm是node的包管理工具https://nodejs.org/en/脚手架安装npm i -g vue/clihttps://registry.npmjs.org/vue浏览器插件https://devtools.vuejs.org/guide/installation.html#chromehttps://chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhble…...
C++编程之 万能引用
万能引用是一种可以同时接受左值或右值的引用,它的形式是T&&,其中T是一个模板参数。万能引用不是C的一个新特性,而是利用了模板类型推导和引用折叠的规则来实现的功能。 模板类型推导 模板类型推导是指在调用一个模板函数时&#x…...
【JavaScript速成之路】JavaScript内置对象--数组对象
📃个人主页:「小杨」的csdn博客 🔥系列专栏:【JavaScript速成之路】 🐳希望大家多多支持🥰一起进步呀! 文章目录前言数组对象1,数组类型检测2,数组元素增删3,…...
【华为机试真题详解 Python实现】最差产品奖【2023 Q1 | 100分】
文章目录 前言题目描述输入描述输出描述示例 1题目解析参考代码前言 《华为机试真题详解》专栏含牛客网华为专栏、华为面经试题、华为OD机试真题。 如果您在准备华为的面试,期间有想了解的可以私信我,我会尽可能帮您解答,也可以给您一些建议! 本文解法非最优解(即非性能…...
[算法] 二分查找
package com.guigu.search;import java.util.ArrayList; import java.util.Arrays;/*** author: guorui fu* versiion: 1.0* 二分查找 直接适用于已经排序完成的数组*/ public class BinarySearch {public static void main(String[] args) {int arr[] {1,8,8,89,101,1234};Ar…...
HTML面经
1.src与href的区别 src用于替换当前元素,如script标签,img标签等。当html解析到这些标签时,会暂停解析,将指定的资源下载下来,嵌入到所在位置内。href的话则是一个当前页面与引用资源之间的链接,如link标签…...
我的十年编程路 2021年篇
慢慢地,时光走过了8个年头,来到2021年。 站在2021年,回望8年的过往,没有大的起伏和波澜。或许是上天的眷顾,我的事业发展一直都很顺利。当然,弯路也走过一些,而且工作其实挺辗转的,…...
ElasticSearch 8 学习笔记总结(七)
感觉这些东西没必要认真学,了解一下,工作用到再学。 文章目录一、ES8 EQL 介绍二、ES8 EQL基本操作 与 安全检测三、ES SQL操作四、ES SQL与DSL的关系五、ES 常用的SQL操作六、ES datagrip配置ES七、ES8 自然语言处理 NLP八、ES8 性能优化 之 缓存九、ES…...
【云原生】Docker 网络模式详解、容器间网络通信
当项目大规模使用 Docker 时,容器通信的问题也就产生了。要解决容器通信问题,必须先了解很多关于网络的知识。Docker 作为目前最火的轻量级容器技术,有很多令人称道的功能,也有着很多不完善的地方,网络方面就是 Docker…...
Java开发 - 布隆过滤器初体验
目录 前言 布隆过滤器 什么是布隆过滤器 布隆过滤器的作用 布隆过滤器原理 怎么设计布隆过滤器 布隆过滤器使用案例 安装布隆过滤器 添加依赖 添加配置 添加工具类 添加测试代码 简单测试 特别提醒 结语 前言 前面三篇,已经把消息队列…...
【计算机组成原理 - 第一章】计算机系统概论(完结)
本章参考王道考研相关课程: 【2021版】1.2.1_计算机硬件的基本组成_哔哩哔哩_bilibili 【2021版】1.2.2_认识各个硬件部件_哔哩哔哩_bilibili 【2021版】1.2.3_计算机系统的层次结构_哔哩哔哩_bilibili 【2021版】1.3_计算机的性能指标_哔哩哔哩_bilibili 目录 一、…...
C++类与对象(下)【详析】
类与对象(下) 目录类与对象(下)一、再谈构造函数1.构造函数体赋值2.初始化列表定义:注意点:总结:3.explicit关键字引入:explicit:二、 static成员回顾:static…...
网站建设类公/网络推广赚钱
谷歌地图位置偏移You’re meeting a friend downtown in a new city, and he asks you where you are. Be honest: you have no clue. Luckily, Google Maps can help you both out. 您在一个新城市的市区遇到一个朋友,他问您现在在哪里。 老实说:您没有…...
平陆县网站建设/网站权重是怎么提升的
2017-12-1309:13:32更新51论坛上的帖子,大神自己写的库文件,待调试! http://www.51hei.com/bbs/forum.php?modviewthread&tid92967&extrapage%3D7&mobile2 2017-12-1209:33:56更新资料 关于内存不足的问题摘要:http://www.ardui…...
门户网站 页面集成/申请百度收录网址
什么是统计学?统计学是研究数据的收集、整理、分析、解释和表达的一门科学。Statistics is the science dealing with the collections, analysis, interpretation and presentation of masses of numerical data. (Webster国际大词典)统计学简史图1 统计学简史数理…...
网站动画是怎么做的/域名收录查询工具
一、WordPress WordPress 是一种使用 PHP 和 MySQL 开发、功能强大的免费博客系统,目前在全世界使用人数最多!它插件众多,主题丰富,而且安装和使用都非常方便。运用它,您可以轻松地在支持 PHP 和 MySQL 数据库的服务器…...
网站设计与制作教程/安康地seo
命令执行代码是指使用编程语言编写的程序,用于执行特定的系统命令。它可以在不同的平台上使用不同的语言编写,如在Windows上使用C#或Visual Basic,在Linux上使用Python或Bash。...
wordpress 培训插件/网站排名优化方法
1. 编写程序,声明一个method方法,在方法中打印一个108 的型矩形, 在main方法中调用该方法。 2. 修改上一个程序,在method方法中,除打印一个108的型矩形外,再 计算该矩形的面积,并将其作为方法返…...