基于Spring的发布订阅模式

在我们使用spring开发应用时,经常会碰到要去解耦合一些依赖调用,比如我们在做代码的发布流程中,需要去通知相关的测试,开发人员关注发布中的错误信息。而且通知这个操作又不希望强耦合在主业务流程中,这个时候我们很容易就想到了观察者设计模式,而spring恰好提供了事件-监听机制,让我们看一下他们是具体怎么实现的吧。

事件-监听机制:

  1. 首先是一种对象间的一对多的关系;最简单的如交通信号灯,信号灯是目标(一方),行人注视着信号灯(多方);
  2. 当目标发送改变(发布),观察者(订阅者)就可以接收到改变;
  3. 观察者如何处理(如行人如何走,是快走/慢走/不走,目标不会管的),目标无需干涉;所以就松散耦合了它们之间的关系。

Spring提供的事件驱动模型/观察者抽象

其实整个模型就有三个角色,事件,目标(发布者),监听者,我们看一下spring中如何实现这三者

事件:

具体代表者是:ApplicationEvent:

1、 我们可以看到spring中ApplicationEvent该抽象类继承自JDK的EventObject。JDK要求所有事件将继承它,并通过source得到事件源。

package org.springframework.context;

import java.util.EventObject;

/**
* Class to be extended by all application events. Abstract as it
* doesn't make sense for generic events to be published directly.
*
* @author Rod Johnson
* @author Juergen Hoeller
*/
public abstract class ApplicationEvent extends EventObject { /** use serialVersionUID from Spring 1.2 for interoperability */
private static final long serialVersionUID = 7099057708183571937L; /** System time when the event happened */
private final long timestamp; /**
* Create a new ApplicationEvent.
* @param source the object on which the event initially occurred (never {@code null})
*/
public ApplicationEvent(Object source) {
super(source);
this.timestamp = System.currentTimeMillis();
} /**
* Return the system time in milliseconds when the event happened.
*/
public final long getTimestamp() {
return this.timestamp;
} }

  

目标(发布者)

具体代表者是具体代表者是:ApplicationEventPublisher及ApplicationEventMulticaster。ApplicationContext该接口继承了ApplicationEventPublisher,并在AbstractApplicationContext实现了具体代码,实际执行是委托给ApplicationEventMulticaster(可以认为是多播):

ApplicationContext继承自ApplicationEventPublisher

public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory,
MessageSource, ApplicationEventPublisher, ResourcePatternResolver {
}

ApplicationEventPublisher定义了publishEvent方法

public interface ApplicationEventPublisher {

    /**
* Notify all <strong>matching</strong> listeners registered with this
* application of an application event. Events may be framework events
* (such as RequestHandledEvent) or application-specific events.
* @param event the event to publish
* @see org.springframework.web.context.support.RequestHandledEvent
*/
void publishEvent(ApplicationEvent event); /**
* Notify all <strong>matching</strong> listeners registered with this
* application of an event.
* <p>If the specified {@code event} is not an {@link ApplicationEvent},
* it is wrapped in a {@link PayloadApplicationEvent}.
* @param event the event to publish
* @since 4.2
* @see PayloadApplicationEvent
*/
void publishEvent(Object event); }

在AbstractApplicationContext实现了具体代码,实际执行是委托给ApplicationEventMulticaster(可以认为是多播)

protected void publishEvent(Object event, ResolvableType eventType) {
Assert.notNull(event, "Event must not be null");
if (logger.isTraceEnabled()) {
logger.trace("Publishing event in " + getDisplayName() + ": " + event);
} // Decorate event as an ApplicationEvent if necessary
ApplicationEvent applicationEvent;
if (event instanceof ApplicationEvent) {
applicationEvent = (ApplicationEvent) event;
}
else {
applicationEvent = new PayloadApplicationEvent<Object>(this, event);
if (eventType == null) {
eventType = ((PayloadApplicationEvent)applicationEvent).getResolvableType();
}
} // Multicast right now if possible - or lazily once the multicaster is initialized
if (this.earlyApplicationEvents != null) {
this.earlyApplicationEvents.add(applicationEvent);
}
else {
getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);
} // Publish event via parent context as well...
if (this.parent != null) {
if (this.parent instanceof AbstractApplicationContext) {
((AbstractApplicationContext) this.parent).publishEvent(event, eventType);
}
else {
this.parent.publishEvent(event);
}
}
}

ApplicationContext自动到本地容器里找一个ApplicationEventMulticaster实现,如果没有自己new一个SimpleApplicationEventMulticaster。

public void multicastEvent(final ApplicationEvent event, ResolvableType eventType) {
ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
for (final ApplicationListener<?> listener : getApplicationListeners(event, type)) {
Executor executor = getTaskExecutor();
if (executor != null) {
executor.execute(new Runnable() {
@Override
public void run() {
invokeListener(listener, event);
}
});
}
else {
invokeListener(listener, event);
}
}
}

大家可以看到如果给它一个executor(java.util.concurrent.Executor),它就可以异步支持发布事件了。

所以我们发送事件只需要通过ApplicationContext.publishEvent即可

监听器

具体代表者是:ApplicationListener

1、其继承自JDK的EventListener

package org.springframework.context;

import java.util.EventListener;

/**
* Interface to be implemented by application event listeners.
* Based on the standard {@code java.util.EventListener} interface
* for the Observer design pattern.
*
* <p>As of Spring 3.0, an ApplicationListener can generically declare the event type
* that it is interested in. When registered with a Spring ApplicationContext, events
* will be filtered accordingly, with the listener getting invoked for matching event
* objects only.
*
* @author Rod Johnson
* @author Juergen Hoeller
* @param <E> the specific ApplicationEvent subclass to listen to
* @see org.springframework.context.event.ApplicationEventMulticaster
*/
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener { /**
* Handle an application event.
* @param event the event to respond to
*/
void onApplicationEvent(E event); }

  

我们在spring中自己实现的demo:

1.定义事件

@Data
public class OrderCreateEvent extends ApplicationEvent { private String tradeOrderNo;
private Long userId; public OrderCreateEvent(Object source, String tradeOrderNo, Long userId) {
super(source);
this.tradeOrderNo = tradeOrderNo;
this.userId = userId;
} }

  

2.实现目标(发布者)

public interface OrderPublisher {

    default void publish(ApplicationEvent event) {
ApplicationContext context = SpringBeanUtil.getContext();
context.publishEvent(event);
} }

  

3.实现监听器
方式一
@Component
@Slf4j
public class OrderEventHandler { private static final Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create(); @Async
@EventListener
public void createHandle(OrderCreateEvent event) {
RedisKey redisKey = new RedisKey("order", "create", event.getTradeOrderNo());
if (redisTemplate.hasKey(redisKey.toString())) {
return;
}
Order order = (Order) event.getSource();
//调用 HSF 接口创建订单落 ES
try {
Boolean syncResult = orderSyncEsServiceProxy.createOrder(order);
if (!syncResult) {
log.error("同步订单 tradeOrderNo:[{}],创建订单失败", order.getTradeOrderNo());
}
} catch (Throwable e) {
log.error("同步订单 tradeOrderNo:[{}],创建订单异常:", order.getTradeOrderNo(), e);
} String messageId = mqProductConfig.sendMessage(
mqPropertiesConfig.getGroupNameMap().get(MqGroupConstant.GroupType.GID_CREATE_ORDER),
MqGroupConstant.Tag.CREATE_ORDER_TAG,
event.getTradeOrderNo(), 60 * 30L); redisTemplate.opsForValue().set(redisKey.toString(), gson.toJson(event), 60 * 60 * 24, TimeUnit.SECONDS); // 锁定用户权益
benefitServiceProxy.lockCouponBenefit(order); // 用户信息
UserInfoDTO userInfoDTO =
userServiceProxy.queryUserInfo(order.getUserId(), order.getSubOrders().get(0).getAddressId(),
order.getTradeChannel(), null); // 创建订单埋点
youShuTrackService.track(TrackFactory.createOrderFactory(order, userInfoDTO), YouShuApiEnum.ORDER_ADD);
log.error("[{}] OrderEventHandler.createHandle.event , messageId [{}] ", event.getTradeOrderNo(), messageId);
} @Async
@EventListener
public void paidHandle(OrderPaidEvent event) {
RedisKey redisKey = new RedisKey("order", "paid", event.getTradeOrderNo());
if (redisTemplate.hasKey(redisKey.toString())) {
return;
}
redisTemplate.opsForValue().set(redisKey.toString(), gson.toJson(event), 60 * 60 * 24, TimeUnit.SECONDS); // 拼团付款成功后 开团或者加团接口
Order order = (Order) event.getSource();
if (BusinessTypeEnum.AID_GROUP_ORDER.getCode().equals(order.getBizType()) ||
BusinessTypeEnum.COMMUNITY_GROUP_ORDER.getCode().equals(order.getBizType())) {
aidGroupServiceProxy.createAndJoinGroup(order);
} // 使用用户权益
benefitServiceProxy.useCouponBenefit(order);
} @Async
@EventListener
public void confirmHandle(OrderConfirmEvent event) {
RedisKey redisKey = new RedisKey("order", "confirm", event.getTradeOrderNo());
if (redisTemplate.hasKey(redisKey.toString())) {
return;
} String messageId = mqProductConfig.sendMessage(
mqPropertiesConfig.getGroupNameMap().get(MqGroupConstant.GroupType.GID_CONFIRM_ORDER),
MqGroupConstant.Tag.CONFIRM_ORDER_TAG + evn,
gson.toJson(event.getSource()), 0L); Map<String, String> eventMap = Maps.newHashMap();
eventMap.put("event", gson.toJson(event));
eventMap.put("source", gson.toJson(event.getSource()));
eventMap.put("messageId", messageId);
redisTemplate.opsForHash().putAll(redisKey.toString(), eventMap);
redisTemplate.expire(redisKey.toString(), 60 * 60 * 24, TimeUnit.SECONDS); Order source = (Order) event.getSource();
source.getSubOrders().forEach(subOrder -> {
itemLimitHelper.addItemLimitNum(subOrder.getUserId(), subOrder.getBizItemId(),
subOrder.getBuyAmount().intValue(), subOrder.getTradeChannel(), source.getBizType());
}); // 订单 - 确单,埋点
youShuTrackService.track(TrackFactory.updateOrderFactory(event.getTradeOrderNo(), TrackOrderStatusEnum.UN_TRANSPORT), YouShuApiEnum.ORDER_UPDATE);
log.error("[{}]OrderEventHandler.confirmHandle success , messageId=[{}]", event.getTradeOrderNo(), messageId);
} @Async
@EventListener
public void refundHandle(OrderRefundEvent event) { RefundOrderDTO refundOrderDTO = event.getRefundOrderDTO();
log.info("[{}] OrderEventHandler.refundHandle", event.getTradeOrderNo());
log.info("refundOrderDTO is{}", JSON.toJSONString(refundOrderDTO));
RedisKey redisKey = new RedisKey("order", "refund", event.getTradeOrderNo());
if (refundOrderDTO != null) {
redisKey = new RedisKey("order", "refund", event.getTradeOrderNo(),
CollectionUtils.isEmpty(refundOrderDTO.getTradeSubOrderIdList()) ? "" :
Joiner.on(",").join(refundOrderDTO.getTradeSubOrderIdList()));
} if (redisTemplate.hasKey(redisKey.toString())) {
return;
} RefundMqDTO refundMqDTO = new RefundMqDTO();
refundMqDTO.setTradeOrderNo(event.getTradeOrderNo());
if (refundOrderDTO != null) {
refundMqDTO.setRefundFee(refundOrderDTO.getRefundFee());
refundMqDTO.setSubOrderId(refundOrderDTO.getTradeSubOrderId());
refundMqDTO.setSubOrderIdList(refundOrderDTO.getTradeSubOrderIdList());
} String messageId = mqProductConfig.sendMessage(
mqPropertiesConfig.getGroupNameMap().get(MqGroupConstant.GroupType.GID_REFUND_PAYMENT),
MqGroupConstant.Tag.REFUND_PAYMENT,
GsonUtils.gson().toJson(refundMqDTO), 0L); redisTemplate.opsForValue().set(redisKey.toString(), messageId, 3, TimeUnit.DAYS); Order order = (Order) event.getSource(); //退款根据
benefitServiceProxy.returnCouponBenefit(order); if (Objects.nonNull(refundOrderDTO) && Objects.nonNull(refundOrderDTO.getBenefitRefundFee())
&& refundOrderDTO.getBenefitRefundFee() > 0) {
benefitServiceProxy.returnBenefitDiscountFee(order.getUserId(), order.getTradeOrderNo(),
refundOrderDTO.getTradeSubOrderIdList(), refundOrderDTO.getBenefitRefundFee());
}
//库存回退
if (TradeChannelEnum.WECHAT_SUBSCRIPTION.getCode().equals(order.getTradeChannel())) {
Date startDeliveryDate = order.getSubOrders().get(0).getSubOrderLine().getStartDeliveryDate();
saleStockOptProxy.returnStock(SaleStockOptProxy.groupBizItemByOrder(order), startDeliveryDate);
} //全部退款进行调用es更新兼容拼团退款
if (Objects.isNull(refundOrderDTO) || CollectionUtils.isEmpty(refundOrderDTO.getTradeSubOrderIdList())) {
orderSyncEsServiceProxy.changeOrderStatus(order.getTradeOrderNo(), OrderPayStatusEnum.CLOSED);
}
// 退款埋点
youShuTrackService.track(TrackFactory.returnOrderFactory(order), YouShuApiEnum.ORDER_RETURN);
log.error("[{}]OrderEventHandler.refundHandle success , messageId=[{}]", event.getTradeOrderNo(), messageId);
} @Async
@EventListener
public void cancelHandle(OrderCancelEvent event) {
log.info("[{}] OrderEventHandler.cancelHandle", event.getTradeOrderNo());
RedisKey redisKey = new RedisKey("order", "cancel", event.getTradeOrderNo());
if (redisTemplate.hasKey(redisKey.toString())) {
return;
} redisTemplate.opsForValue().set(redisKey.toString(), System.currentTimeMillis() + "", 3, TimeUnit.DAYS); Order order = (Order) event.getSource(); //权益解锁
benefitServiceProxy.unLockCouponBenefit(order); //库存回退
if (TradeChannelEnum.WECHAT_SUBSCRIPTION.getCode().equals(order.getTradeChannel())) {
log.info("startReturnStock : {}", JSON.toJSONString(SaleStockOptProxy.groupBizItemByOrder(order)));
Date startDeliveryDate = order.getSubOrders().get(0).getSubOrderLine().getStartDeliveryDate();
saleStockOptProxy.returnStock(SaleStockOptProxy.groupBizItemByOrder(order), startDeliveryDate);
} String freezerNum = AttributesUtils.getAttrByKey(order.getAttributes(), "appoint_freezer");
String freezerBoxNum = AttributesUtils.getAttrByKey(order.getAttributes(), "appoint_freezer_box"); freezerOperationProxy.unlockBox(freezerNum, freezerBoxNum); // 接龙活动套餐库存
if (BusinessTypeEnum.JIE_LONG_ORDER.getCode().equals(order.getBizType())) {
Long storeId = order.getSubOrders().get(0).getStoreId();
String comboId = AttributesUtils.getAttrByKey(order.getAttributes(), "combo_id");
if (comboId == null) {
log.error("Order content error {}", JSON.toJSONString(order));
throw new BusinessException(OrderErrorCode.ITEM_NOT_EXISTS);
} comboStoreServiceProxy.returnStock(Long.parseLong(comboId), storeId);
} orderSyncEsServiceProxy.changeOrderStatus(order.getTradeOrderNo(), OrderPayStatusEnum.PAID_OUTTIME); // 订单 - 取消,埋点
youShuTrackService.track(TrackFactory.updateOrderFactory(event.getTradeOrderNo(), TrackOrderStatusEnum.CANCEL_UN_PAY), YouShuApiEnum.ORDER_UPDATE);
log.info("[{}]OrderEventHandler.cancelHandle success", event.getTradeOrderNo());
} }

好了,通过这3步,我们就可以在spring中愉快的实现事件-监听机制了。在我们需要发送事件时,只要调用 EventService的发送方法即可

@Slf4j
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order extends BaseDO implements OrderPublisher {
private Long orderId;
private String bizType;
private String tradeOrderNo;
private Long totalFee;
private Long actualPaidFee;
private Long discountFee;
private Long refundFee;
private Date payTime;
private Date refundTime;
private Integer status;
private Long userId;
private String tradeChannel;
private String attributes;
private Long deliveryFee;
private List<SubOrder> subOrders;
private List<Promotion> promotions; public void create() {
this.publish(new OrderCreateEvent(this, this.tradeOrderNo, this.userId));
} public void confirm() {
this.publish(new OrderConfirmEvent(this, this.tradeOrderNo, this.userId));
} public void paid() {
this.publish(new OrderPaidEvent(this, this.tradeOrderNo, this.userId));
} public void refund(RefundOrderDTO subOrderId) {
this.publish(new OrderRefundEvent(this, this.tradeOrderNo, this.userId,subOrderId));
} public void canceled() {
this.publish(new OrderCancelEvent(this, this.tradeOrderNo, this.userId));
} public void finishPaid() {
if (this.status == OrderPayStatusEnum.PAID_DONE.getValue() || this.status == OrderPayStatusEnum.PAID_DONE_SHIPPED.getValue()) {
return;
}
if (this.status != OrderPayStatusEnum.UN_PAID.getValue()) {
throw new BusinessException(OrderErrorCode.ORDER_STATUS_IS_ERROR);
}
this.status = OrderPayStatusEnum.PAID_DONE.getValue();
this.payTime = new Date();
} public void confirmOrder() {
if (this.status == OrderPayStatusEnum.PAID_DONE_SHIPPED.getValue()) {
return;
}
if (this.status == OrderPayStatusEnum.CLOSED.getValue()) {
return;
}
if (this.status != OrderPayStatusEnum.PAID_DONE.getValue()) {
throw new BusinessException(OrderErrorCode.ORDER_STATUS_IS_ERROR);
}
this.status = OrderPayStatusEnum.PAID_DONE_SHIPPED.getValue();
subOrders.forEach(subOrder -> {
if (subOrder.getStatus() != SubOrderStatusEnum.INIT.getValue()) {
throw new BusinessException(OrderErrorCode.ORDER_STATUS_IS_ERROR);
}
subOrder.setStatus(SubOrderStatusEnum.WAIT.getValue());
});
} public void cancel() {
if (this.status != OrderPayStatusEnum.UN_PAID.getValue()) {
return;
}
if (this.status == OrderPayStatusEnum.PAID_OUTTIME.getValue()) {
return;
}
this.status = OrderPayStatusEnum.PAID_OUTTIME.getValue();
} public void complete(Long subOrderId) {
subOrders.stream().filter(e -> e.getSubOrderId().equals(subOrderId)).forEach(subOrder -> {
subOrder.setStatus(SubOrderStatusEnum.END.getValue());
}); if (subOrders.stream().allMatch(subOrder ->
SubOrderStatusEnum.END.getValue().equals(subOrder.getStatus()) ||
SubOrderStatusEnum.TXP_CLOSE.getValue().equals(subOrder.getStatus())
)) {
this.status = OrderPayStatusEnum.SUCCESS.getValue();
}
} public void refundOrder(Long refundFee, Date refundDate) {
if (this.status == OrderPayStatusEnum.UN_PAID.getValue()) {
throw new BusinessException(OrderErrorCode.ORDER_STATUS_IS_ERROR);
}
if (this.status == OrderPayStatusEnum.CLOSED.getValue()) {
throw new BusinessException(RefundErrorCode.ORDER_IS_REFUND);
}
refundOrder(refundFee, refundDate, null, null);
} }

  

使用@EventListener还支持SPEL表达式

@EventListener(condition = "#jkRecordConfirmEvent.jkRecordConfirmEventDTO.cwConfirmAndReturnFlag")
@Async("asyncListenerExecutor")

  

我们可以看到在监听器中,实现的方法使用了@Async注解。在spring3提供了@Aync注解来完成异步调用。我们可以使用这个新特性来完成异步调用。在实际项目中,我们一般也是把这两者结合来使用的,特别是监听事件是一件耗时过程时,这种方式降低了代码的耦合性,非常好用。

基于Spring的发布订阅模式 EventListener的更多相关文章

  1. Spring源码之七registerListeners()及发布订阅模式

    Spring源码之七registerListeners()及发布订阅模式 大家好,我是程序员田同学. 今天带大家解读refresh()方法中的registerListeners()方法,也就是我们经常 ...

  2. SpringBoot事件监听机制及观察者模式/发布订阅模式

    目录 本篇要点 什么是观察者模式? 发布订阅模式是什么? Spring事件监听机制概述 SpringBoot事件监听 定义注册事件 注解方式 @EventListener定义监听器 实现Applica ...

  3. SpringBoot Redis 发布订阅模式 Pub/Sub

    SpringBoot Redis 发布订阅模式 Pub/Sub 注意:redis的发布订阅模式不可以将消息进行持久化,订阅者发生网络断开.宕机等可能导致错过消息. Redis命令行下使用发布订阅 pu ...

  4. RabbitMQ/JAVA (发布/订阅模式)

    发布/订阅模式即生产者将消息发送给多个消费者. 下面介绍几个在发布/订阅模式中的关键概念-- 1. Exchanges (转发器) 可能原来我们都是基于一个队列发送和接收消息.现在介绍一下完整的消息传 ...

  5. redis的发布订阅模式

    概要 redis的每个server实例都维护着一个保存服务器状态的redisServer结构 struct redisServer {     /* Pubsub */     // 字典,键为频道, ...

  6. javascript设计模式——发布订阅模式

    前面的话 发布—订阅模式又叫观察者模式,它定义对象间的一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知.在javascript开发中,一般用事件模型来替代传统的发布—订阅模 ...

  7. 《JavaScript设计模式与开发实践》笔记第八章 发布-订阅模式

    第八章 发布-订阅模式 发布-订阅模式描述 发布-订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知. 发布-订阅模式可以广泛应用于 ...

  8. 观察者模式 vs 发布-订阅模式

    我曾经在面试中被问道,_“观察者模式和发布订阅模式的有什么区别?” _我迅速回忆起“Head First设计模式”那本书: 发布 + 订阅 = 观察者模式 “我知道了,我知道了,别想骗我” 我微笑着回 ...

  9. redis实现消息队列&发布/订阅模式使用

    在项目中用到了redis作为缓存,再学习了ActiveMq之后想着用redis实现简单的消息队列,下面做记录.   Redis的列表类型键可以用来实现队列,并且支持阻塞式读取,可以很容易的实现一个高性 ...

随机推荐

  1. 【python】一些python用法规律笔记

    作为本科用了多年MATLAB的工科生,学起来python有些似曾相识但也有些不习惯的地方. 在这里总结一下,慢慢整理,希望能巩固python语法 一.前闭后开 这个是和MATLAB很大不同.不论是ra ...

  2. KingbaseESV8R6 垃圾回收原理以及如何预防膨胀

    背景 KingbaseESV8R6支持snapshot too old 那么实际工作中,经常看到表又膨胀了,那么我们讨论一下导致对象膨胀的常见原因有哪些呢? 未开启autovacuum 对于未开启au ...

  3. KingbaseES 如何查看应用执行的SQL的执行计划

    通过explain ,我们可以获取特定SQL 的执行计划.但对于同一条SQL,不同的变量.不同的系统负荷,其执行计划可能不同.我们要如何取得SQL执行时间点的执行计划?KingbaseES 提供了 a ...

  4. 如何修改SAO用户密码

    KingbaseES SAO 用户是专门用于审计管理的用户,用户配置审计策略需要使用该用户.在initdb 完成后,SAO  用户的默认密码保存在参数 sysaudit.audit_table_pas ...

  5. 日志:Redo Log 和 Undo Log

    本篇文章主要介绍 Redo Log 和 Undo Log: 利用 Redo Log 和 Undo Log 实现本地事务的原子性.持久性 Redo Log 的写回策略 Redo Log Buffer 的 ...

  6. 【2022-09-09】Django框架(九)

    Django框架(九) cookie与session简介 网址的发展史: 1.起初网站都没有保存用户功能的需求,所有用户访问返回的结果都是一样的. 比如:新闻网页,博客网页,小说... (这些网页是不 ...

  7. ClangFormat配置备份

    { # 语言 Language: Cpp, # 水平对齐表达式的操作数 AlignOperands: true, # 不对包含头文件进行排序 SortIncludes: false, # 对齐注释 A ...

  8. liunx标准输入与输出

    一.Linux提供了三种输入/输出通道给程序在linux中,每个进程都会有三个文件,并且这三个文件会进行重定向处理:1. 标准输入(STDIN) - 缺省为键盘2. 标准输出(STDOUT) - 默认 ...

  9. 大家都在用MySQL count(*)统计总数,到底有什么问题?

    在日常开发工作中,我经常会遇到需要统计总数的场景,比如:统计订单总数.统计用户总数等.一般我们会使用MySQL 的count函数进行统计,但是随着数据量逐渐增大,统计耗时也越来越长,最后竟然出现慢查询 ...

  10. MinIO Docker 快速入门

    官方文档地址:http://docs.minio.org.cn/docs/master/minio-docker-quickstart-guide 在Docker中运行MinIO单点模式 MinIO ...