整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

我自定义的拦截器为什么会靠后执行?

项目中自定义了拦截器Filter,项目中使用了spring security,它也有对应的拦截器,我想让我自定义的Filter在spring security的拦截器前执行。

因为我自定义的拦截器,需要提前做一些逻辑处理;然后spring security的拦截器需要用到这部分的处理结果;所以我必须要想办法让我自定义的拦截器靠前执行。

那就一起来看看spring security设置的拦截器的默认优先级等级是多少吧。

模拟场景

自定义拦截器如下:

@Slf4j
public class MyOncePerRequestFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        log.info("======== MyOncePerRequestFilter ========");
        filterChain.doFilter(request, response);
    }
}

@Configuration
public class Config {
    @Bean
    public FilterRegistrationBean<MyOncePerRequestFilter> i18nFilterRegistrationBean() {
        FilterRegistrationBean<MyOncePerRequestFilter> registrationBean = new FilterRegistrationBean();
        MyOncePerRequestFilter myOncePerRequestFilter = new MyOncePerRequestFilter();
        registrationBean.setFilter(myOncePerRequestFilter);
        registrationBean.addUrlPatterns("/*");
        registrationBean.setOrder(-1);
        return registrationBean;
    }
}

spring security的简单配置如下:

@Slf4j
public class MyTokenStore implements TokenStore {
    @Override
    public OAuth2AccessToken readAccessToken(String tokenValue) {
        log.info("======== readAccessToken ========");
        return new DefaultOAuth2AccessToken(tokenValue);
    }

    @Override
    public OAuth2Authentication readAuthentication(OAuth2AccessToken token) {
        Authentication authentication = new AbstractAuthenticationToken(Sets.newHashSet()) {
            {
                super.setAuthenticated(true);
            }
            @Override
            public Object getCredentials() {
                return null;
            }
            @Override
            public Object getPrincipal() {
                return StringUtils.EMPTY;
            }
        };
        OAuth2Request request =
                new OAuth2Request(null, null, null, true,
                        Sets.newHashSet(), Sets.newHashSet(), null, null, null);
        return new OAuth2Authentication(request, authentication);
    }

    @Override public OAuth2Authentication readAuthentication(String token) {
        return null;
    }

    @Override
    public void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
    }

    @Override public void removeAccessToken(OAuth2AccessToken token) {

    }

    @Override public void storeRefreshToken(OAuth2RefreshToken refreshToken, OAuth2Authentication authentication) {
    }

    @Override public OAuth2RefreshToken readRefreshToken(String tokenValue) {
        return null;
    }

    @Override public OAuth2Authentication readAuthenticationForRefreshToken(OAuth2RefreshToken token) {
        return null;
    }

    @Override public void removeRefreshToken(OAuth2RefreshToken token) {
    }

    @Override public void removeAccessTokenUsingRefreshToken(OAuth2RefreshToken refreshToken) {
    }

    @Override public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) {
        return null;
    }

    @Override public Collection<OAuth2AccessToken> findTokensByClientIdAndUserName(String clientId, String userName) {
        return null;
    }

    @Override public Collection<OAuth2AccessToken> findTokensByClientId(String clientId) {
        return null;
    }
}

@Configuration
@EnableResourceServer
@EnableWebSecurity
public class MyResourceServerConfigurerAdapter extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        MyTokenStore tokenStore = new MyTokenStore();
        resources.tokenStore(tokenStore);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests().anyRequest().authenticated()
                .and().anonymous().key("anonymousUser")
                .and().httpBasic();
    }
}

启动类如下:

@RestController
public class MyController {
    @GetMapping("/hello")
    public String hello() {
        return "hello,world!";
    }
}

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class Starter {
    public static void main(String[] args) {
        SpringApplication.run(Starter.class, args);
    }
}

启动后,访问 http://127.0.0.1:8080/hello?access_token=123

日志打印如下:

102149 [http-nio-8080-exec-1] INFO  c.e.l.s.mvc.security.MyTokenStore - ======== readAccessToken ======== 
102149 [http-nio-8080-exec-1] INFO  c.e.l.s.m.s.MyOncePerRequestFilter - ======== MyOncePerRequestFilter ======== 

从结果可以看出,spring security的拦截器是比我们自定义的拦截器先执行的,而我们自定义的拦截器的优先级是registrationBean.setOrder(-1)

我猜应该是这个值决定了执行顺序,那就带着这个猜想往下看一下吧。

是不是因为order的值

在之前的配置中,我们将自定义的拦截器顺序置为-1

我们先在MyOncePerRequestFilter.doFilterInternal打个断点,看一下执行链的顺序:

从这条链中,我们猜测springSecurityFilterChain的order是-100,我们自定义的拦截器是在它后面的

那我们直接把我们的拦截器设置成-101,registrationBean.setOrder(-101);,再来尝试一下:

从断点结果可以看出,我们的设置是有效的,并且起到了作用,而且打印日志也说明了结果,如下:

11956 [http-nio-8080-exec-1] INFO  c.e.l.s.m.s.MyOncePerRequestFilter - ======== MyOncePerRequestFilter ======== 
98419 [http-nio-8080-exec-1] INFO  c.e.l.s.mvc.security.MyTokenStore - ======== readAccessToken ======== 

找出在哪里赋予的order值

这个过程是极其枯燥的,所以就先给结果了,如下:

spring security的拦截器链是在下面这部分创建的:

@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(SecurityProperties.class)
@ConditionalOnClass({ AbstractSecurityWebApplicationInitializer.class, SessionCreationPolicy.class })
@AutoConfigureAfter(SecurityAutoConfiguration.class)
public class SecurityFilterAutoConfiguration {
	private static final String DEFAULT_FILTER_NAME = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME;
	@Bean
	@ConditionalOnBean(name = DEFAULT_FILTER_NAME)
	public DelegatingFilterProxyRegistrationBean securityFilterChainRegistration(
			SecurityProperties securityProperties) {
		DelegatingFilterProxyRegistrationBean registration = new DelegatingFilterProxyRegistrationBean(
				DEFAULT_FILTER_NAME);
		registration.setOrder(securityProperties.getFilter().getOrder()); // 这里
		registration.setDispatcherTypes(getDispatcherTypes(securityProperties));
		return registration;
	}
}

public abstract class AbstractSecurityWebApplicationInitializer implements WebApplicationInitializer {
    public static final String DEFAULT_FILTER_NAME = "springSecurityFilterChain";
}

@ConfigurationProperties(prefix = "spring.security")
public class SecurityProperties {
	public static final int DEFAULT_FILTER_ORDER = OrderedFilter.REQUEST_WRAPPER_FILTER_MAX_ORDER - 100; // 这里

	private final Filter filter = new Filter();
	public Filter getFilter() {
		return this.filter;
	}
	public static class Filter {
		private int order = DEFAULT_FILTER_ORDER; // 这里
		public int getOrder() {
			return this.order;
		}
		public void setOrder(int order) {
			this.order = order;
		}
	}
}

public interface OrderedFilter extends Filter, Ordered {
	int REQUEST_WRAPPER_FILTER_MAX_ORDER = 0; // 这里
}

从上面的代码可以看出,默认值是-100,同样也可以使用spring.security.filter.order来自定义值。

下面是寻找此过程的历程:

继续从这里开始,ApplicationFilterChain.internalDoFilter如下:

可以看出所有的拦截器都是在filters中,我们可以看这个值是怎么来的,通过调试,是在ApplicationFilterChain.addFilter这个地方,如下:

它是被ApplicationFilterFactory.createFilterChain调用的,如下:

所以filters是根据filterMaps来添加的,我们再来看一下filterMaps是怎么来的,一共涉及到两个地方,如下:

StandardContext.addFilterMap和StandardContext.addFilterMapBefore如下:

看一下调用链:

原来是被ServletWebServerApplicationContext.selfInitialize调用的,如下:

ServletWebServerApplicationContext.getServletContextInitializerBeans如下:

ServletContextInitializerBeans构造函数如下:

所有的拦截器都是通过addServletContextInitializerBeans(beanFactory);和addAdaptableBeans(beanFactory);来把bean加进来的

经过一番调试,终于找到spring security这个拦截器定义顺序的位置,SecurityFilterAutoConfiguration.securityFilterChainRegistration如下:

可以看到SecurityProperties securityProperties是注入进来的,找到这个类看一下,securityProperties.filter.order如下:

@ConfigurationProperties(prefix = "spring.security")
public class SecurityProperties {
	public static final int DEFAULT_FILTER_ORDER = OrderedFilter.REQUEST_WRAPPER_FILTER_MAX_ORDER - 100;
	private final Filter filter = new Filter();
	private final User user = new User();
	public static class Filter {
		/**
		 * Security filter chain order.
		 */
		private int order = DEFAULT_FILTER_ORDER;
		public int getOrder() {
			return this.order;
		}
		public void setOrder(int order) {
			this.order = order;
		}
	}
}
public interface OrderedFilter extends Filter, Ordered {
	/**
	 * Filters that wrap the servlet request should be ordered less than or equal to this.
	 */
	int REQUEST_WRAPPER_FILTER_MAX_ORDER = 0;
}

到此我们也找到了这个默认值,是根据spring.security.filter.order来决定的,默认值是-100

解决办法

第一种就是修改自己的顺序:

@Configuration
public class Config {
    @Bean
    public FilterRegistrationBean<MyOncePerRequestFilter> i18nFilterRegistrationBean() {
        FilterRegistrationBean<MyOncePerRequestFilter> registrationBean = new FilterRegistrationBean();
        MyOncePerRequestFilter myOncePerRequestFilter = new MyOncePerRequestFilter();

        registrationBean.setFilter(myOncePerRequestFilter);
        registrationBean.addUrlPatterns("/*");
        registrationBean.setOrder(-101); // 这里
        return registrationBean;
    }
}

第二种就是修改spring security拦截器的顺序:

spring:
  security:
    filter:
      order: 0

大家可以自己跑跑试试看,完结撒花~~~~~~

原文地址:https://www.cnblogs.com/eaglelihh/p/15009562.html

述:本文将讨论如何用最简单的术语在网站上运行 C# 代码。半技术讲座我使用了 wasm-tools-net7,这是一个基于 wasm-tools 的工作负载,没有包含任何额外的包。我的重点是简单性和主要主题。彻底了解该主题可提供完成所有其他任务所需的信息。如何工作?WebAssembly 工作原理:序列图创建演示创建项目我用的是net7,但这取决于你。Dotnet new console -o WASM_Demo cd WASM_Demo Dotnet workload install wasm-tools-net7此时,需要对 csproj 文件进行修改。Project Sdk=Mi

本文将讨论如何用最简单的术语在网站上运行 C# 代码。

半技术讲座

我使用了 wasm-tools-net7,这是一个基于 wasm-tools 的工作负载,没有包含任何额外的包。我的重点是简单性和主要主题。彻底了解该主题可提供完成所有其他任务所需的信息。

如何工作?

WebAssembly 工作原理:序列图

创建演示

创建项目

  • 我用的是net7,但这取决于你。
Dotnet new console -o WASM_Demo  
  
cd WASM_Demo  
  
Dotnet workload install wasm-tools-net7

此时,需要对 csproj 文件进行修改。

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net7.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>

        <RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
        <WasmMainJSPath>main.js</WasmMainJSPath>
        <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
    </PropertyGroup>

    <ItemGroup>
        <WasmExtraFilesToDeploy Include="index.html" />
        <WasmExtraFilesToDeploy Include="main.js" />
    </ItemGroup>

</Project>

我们添加了什么:

  • RuntimeIdentifier (wasm-tools 需要)
  • WasmMainJSPath (wasm-tools 需要)
  • AllowUnsafeBlocks(JSExportAttribute 需要不安全的代码)
  • ItemGroup (Include as resource)导入 index.html导入main.js

返回到program.cs文件,需要考虑某些规则。

  • 类必须是公共的和部分的。
  • 函数必须是公共的和静态的,并且必须使用 [JSExport] 进行属性化。

让我们举个例子。

using System.Runtime.InteropServices.JavaScript;

namespace WASM_Demo;

public partial class Program
{
    static void Main(string[] args) { }

    [JSExport]
    public static string Response()
    {
        return """
               <h1>
                   Hello World
               </h1>
               """;
    }
}

没关系,但是我们如何在浏览器中运行此代码?

运行这个程序的代码是dotnet.js的,它自带了wasm-tools,所以没有必要担心它。要使用此dotnet.js,我们只需使用一个名为 main.js 的文件。

import { dotnet } from './dotnet.js'

const is_browser = typeof window != "undefined";
if (!is_browser) throw new Error(`Expected to be running in a browser`);

const { setModuleImports, getAssemblyExports, getConfig, runMainAndExit } = await dotnet
    .withDiagnosticTracing(false)
    .withApplicationArgumentsFromQuery()
    .create();

const config = getConfig();
const exports = await getAssemblyExports(config.mainAssemblyName);

const html = 
    exports
        .WASM_Demo    // Namespace
        .Program      // Class Name
        .Response();  // Function Name

// Regular javascript code
document.getElementById("app").innerHTML = `${html}`;

await runMainAndExit(config.mainAssemblyName, [] /* Console App Args */);

index.html页面的模板已经准备完毕。

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>WASM Demo</title>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <link rel="modulepreload" href="./dotnet.js" />
    </head>
    
    <body>
        <main id="app"></main>
        <script type="module" src="./main.js"></script>
    </body>
</html>

现在,让我们再看一遍这个过程,

  • HTTP 请求传入
  • WASM-Tools 处理此问题并发送index.html文件。
  • index.html文件请求dotnet.js和main.js文件,dotnet.js由 WASM-Tools 发送,main.js是我们的自定义代码。
  • 通过在 main.js 中使用 dotnet.js,我们可以进入 C# 代码中的 Program 类,调用 Response 函数并在 main.js 中进行我们想要的任何客户端更改。

我们还有一件事要做,你需要打开一个名为 runtimeconfig.template.json 的文件,并将以下 JSON 数据放入其中。

{
  "wasmHostProperties": {
    "perHostConfig": [
      {
        "name": "browser",
        "html-path": "index.html",
        "Host": "browser"
      }
    ]
  }
}

我们已经到了尽头,程序现在可以运行了。唯一需要的命令是:

Dotnet run -c Release

常见问题

我可以托管所有文件而不是 wasm-tools 吗?又是如何做到的呢?

当然,但它可能会变得有点复杂,你用 wasm-tools 制作的项目不能用于任何其他目的,即控制台应用程序不起作用,wasm-tools 可以工作。因为我们选择 browser-wasm 作为 RuntimeIdentifier,并且多个 RuntimeIdentifiers 在 .NET 中不可用。作为替代方法,您可以打开两个项目,将第一个项目设置为 WASM 项目,然后在第二个项目中将其设置为控制台应用程序,然后生成第一个项目并托管输出文件夹,所有 DLL 和文件都将在那里。

这个演示只是索引文件,我可以做多页吗?又是如何做到的呢?

当然,但这比你想象的要难得多,因为这样做的方法是一种叫做SPA(单页应用程序)的方法,用户总是在同一页面上,只是内容发生了变化。有多种方法可以做到这一点。所以它可以用你的创造力来完成。

我可以像计数器一样做动态代码吗?又是如何做到的呢?

_是的,我也这样做了,你可以一遍又一遍地调用 C# 函数,如果你只是将导出绑定到 window 对象,你可以从每个 JavaScript 代码中调用它。

Q简介

  • 什么是MQ
  • 跨进程的消息队列,主要角色包括生产者与消费者。
  • 生产者只负责生产信息,无法感知消费者是谁,消息怎么处理,处理结果是什么。
  • 消费者负责接收及处理消息,无法感知生产者是谁,怎么产生的。
  • Mq能做什么?
  • MQ 特性一般有异步,吞吐量大 ,延时低;
  • 适合做:
  1. 投递异步通知。
  2. 限流,削峰谷。
  3. 可靠事件,处理数据一致性。
  4. 利用一些特性,可以做定时任务。
  5. 等….

由于MQ是异步处理消息的,所以MQ不适合做同步处理操作,如果需要及时的返回处理结果请不要用MQ;

  • MQ 个系统带来了什么?
  • 缺点:增加了系统的复杂性,除了代码组件接入以外还需要考虑,高可用,集群,消息的可靠性等问题!
  • 生产者:消息发送怎么保证可靠性,怎么保证不重复!
  • 消费者:怎么保证幂等性,接收到重复消息怎么处理!
  • 还有会带来的处理延时等问题!

优点: 解耦,利用MQ我们可以很好的给我们系统解耦,特别是分布式/微服系统!

原来的同步操作,可以用异步处理,也可以带来更快的响应速度;

  • 哪些场景可以使用MQ

场景 (1)

系统解耦,用户系统或者其他系统需要发送短信可以通过 MQ 执行;很好的将 用户系统 和 短信系统进行解耦;

场景(2)

顺序执行的任务场景,假设 A B C 三个任务,B需要等待 A完成才去执行,C需要等待B完成才去执行;

我见过一些同学的做法是 ,用 三个定时器 错开时间去执行的,假设 A定时器 9 点执行, B 定时器 10 点执行 , C 11 点执行 , 类似这样子;

这样做其实是 不安全的, 因为 后一个任务 无法知道 前一个任务是否 真的执行了! 假设 A 宕机了, 到 10 点 B 定时去 执行,这时候 数据就会产生异常!

当我们 引入 MQ 后 可以这么做, A执行完了 发送 消息给 B ,B收到消息后 执行,C 类似,收到 B消息后执行;

场景(3)

支付网关的通知,我们的系统常常需要接入支付功能,微信或者支付宝通常会以回调的形式通知我们系统支付结果。

我们可以将我们的支付网关独立出来,通过MQ通知我们业务系统进行处理,这样处理有利于系统的解耦和扩展!

假设我们还有一个积分系统,用户支付成功,给用户添加积分。只需要积分系统监听这个消息,并处理积分就好,无需去修改再去修改网关层代码!

如果没有使用MQ ,我是不是还得去修改网关系统的代码,远程调用增加积分的接口?

这就是使用了MQ的好处,解耦和扩展!

当然我们的转发规则也要保证每个感兴趣的队列能获取到消息!



场景(4)

微服/分布式系统,分布式事务 - 最终一致性 处理方案!

详情: 分布式事务处理方案,微服事务处理方案

场景(5)

  • 消息延时队列,可做些定时任务,不固定时间执行的定时任务。
  • 例如:用户下单后如果24小时未支付订单取消;
  • 确认收货后2天后没有评价自动好评;
  • 等...

我们以前的做法是 通常启用一个定时器,每分钟或者每小时,去跑一次取出需要处理的订单或其他数据进行处理。

这种做法一个是 效率比较低,如果数据量大的话,每次都要扫库,非常要命!

再者时效性不是很高,最差的时候可能需要等待一轮时长!

还有可能出现重复执行的结果,时效和轮询的频率难以平衡!

利用MQ(Rabbitmq),DLX (Dead Letter Exchanges)和 消息的 TTL (Time-To-Live Extensions)特性。我们可以高效的完成这个任务场景!不需要扫库,时效性更好!

DLX:http://www.rabbitmq.com/dlx.html,

TTL:http://www.rabbitmq.com/ttl.html#per-message-ttl

原理:

发送到队列的消息,可以设置一个存活时间 TTL,在存活时间内没有被消费,可以设置这个消息转发到其他队列里面去;然后我们从这个其他队列里面消费执行我们的任务,这样就可以达到一个消息延时的效果!



设置过期时间:

过期时间可以统一设置到消息队列里面,也可以单独设置到某个消息!

PS 如果消息设置了过期时间,发生到了设置有过期时间的队列,已队列设置的过期时间为准!

已 SpringBoot 为例:

配置转发队列和被转发队列:

@Component
@Configuration
public class RabbitMqConfig {
 @Bean
 public Queue curQueue() {
 Map<String, Object> args = new HashMap<String, Object>();
 //超时后的转发器 过期转发到 delay_queue_exchange
 args.put("x-dead-letter-exchange", "delay_queue_exchange");
 //routingKey 转发规则
 args.put("x-dead-letter-routing-key", "user.#");
 //过期时间 20 秒
 args.put("x-message-ttl", 20000);
 return new Queue("cur_queue", false, false, false, args);
 }
 @Bean
 public Queue delayQueue() {
 return new Queue("delay_queue");
 }
 @Bean
 TopicExchange exchange() {
 //当前队列
 return new TopicExchange("cur_queue_exchange");
 }
 @Bean
 TopicExchange exchange2() {
 //被转发的队列
 return new TopicExchange("delay_queue_exchange");
 }
 @Bean
 Binding bindingHelloQueue(Queue curQueue, TopicExchange exchange) {
 //绑定队列到转发器
 return BindingBuilder.bind(curQueue).to(exchange).with("user.#");
 }
 @Bean
 Binding bindingHelloQueue2(Queue delayQueue, TopicExchange exchange2) {
 return BindingBuilder.bind(delayQueue).to(exchange2).with("user.#");
 }
}

发生消息:

@Component
public class MqEventSender {
 Logger logger = LoggerFactory.getLogger(MqEventSender.class);
 @Autowired
 private RabbitTemplate rabbitTemplate;
 /**
 * 消息没有设置 时间
 * 发生到队列 cur_queue_exchange
 * @param msg
 */
 public void sendMsg(String msg) {
 logger.info("发送消息: " + msg);
 rabbitTemplate.convertAndSend("cur_queue_exchange", "user.ss", msg);
 }
 /**
 * 消息设置时间
 * 发生到队列 cur_queue_exchange
 * @param msg
 */
 public void sendMsgWithTime(String msg) {
 logger.info("发送消息: " + msg);
 MessageProperties messageProperties = new MessageProperties();
 //过期时间设置 10 秒
 messageProperties.setExpiration("10000");
 Message message = rabbitTemplate.getMessageConverter().toMessage(msg, messageProperties);
 rabbitTemplate.convertAndSend("cur_queue_exchange", "user.ss", message);
 }
}

消息监听:

监听 的队列是 delay_queue 而不是 cur_queue;

PS cur_queue 不应该有监听者,否则消息被消费达不到想要的延时消息效果!

/**
 * Created by linli on 2017/8/21.
 * 监听 被丢到 超时队列内容
 */
@Component
@RabbitListener(queues = "delay_queue")
public class DelayQueueListener {
 public static Logger logger = LoggerFactory.getLogger(AddCommentsEventListener.class);
 @RabbitHandler
 public void process(@Payload String msg) {
 logger.info("收到消息 "+msg);
 }
}

测试:

/**
 * Created by linli on 2017/8/21.
 */
@RestController
@RequestMapping("/test")
public class TestContorller {
 @Autowired
 MqEventSender sender;
 @RequestMapping("/mq/delay")
 public String test() {
 sender.sendMsg("队列延时消息!");
 sender.sendMsgWithTime("消息延时消息!");
 return "";
 }
}

结果:



观察结果发现:发送时间 和 收到时间 间隔 20秒 ;

我们给消息设置的 10 秒 TTL 时间没有生效!验证了 : 如果消息设置了过期时间,发生到了设置有过期时间的队列,已队列设置的过期时间为准!

如果希望每个消息都要自己的存活时间,发送到队列 不要设置

args.put(“x-message-ttl”, 20000);

消息的过期时间 设置在队列还是消息,根据自己的业务场景去定!

  • 总结

MQ 是一个跨进程的消息队列,我们可以很好的利用他进行系统的解耦;

引入MQ会给系统带来一定的复杂度,需要评估!

MQ 适合做异步任务,不适合做同步任务!