1.异步简单使用

 1.异步概述

 1.1 什么是异步?

  异步是一种编程模式,其中函数或方法的调用不需要等待操作完成即可返回控制权给调用者。这意味着调用者可以在等待操作完成的同时继续执行其他任务。当操作完成后,通常会通过回调、事件通知、Future 或其他机制来告知调用者结果

1.2 相对于同步的优缺点

 优点

  1. 提高性能:异步可以提高程序的并发性和响应能力,特别是在涉及 I/O 操作(如文件读写网络通信)时,可以让 CPU 在等待 I/O 操作完成时去做其他有用的工作
  2. 改善用户体验:在 Web 开发中,异步可以使得应用程序在执行耗时操作时仍然保持界面响应,从而提升用户体验
  3. 资源利用率高:异步模型减少了线程的阻塞时间,从而可以更好地利用系统资源

 缺点

  1. 代码复杂度增加:异步编程通常需要处理回调、事件或使用更复杂的编程模型,这可能会使代码变得难以理解和维护。
  2. 调试难度增加:由于异步代码的执行路径不是线性的,调试起来比同步代码更困难。
  3. 错误处理复杂:异步编程中的错误处理通常更加复杂,因为错误需要通过特殊的机制来捕获和处理。

应用场景

  1. Web 开发:处理 HTTP 请求,尤其是涉及到数据库查询、远程服务调用等耗时操作
  2. 网络编程:网络通信中,读取和发送数据通常是非阻塞的
  3. GUI 应用程序:图形用户界面中的后台任务处理,例如文件上传/下载、数据同步等
  4. 大数据处理批处理任务、数据流处理等场景下,异步处理可以提高处理速度。
  5. 微服务架构服务间通信时,为了减少响应时间,通常采用异步调用

2. 异步实现增加用户

2.1 使用Spring的@Async实现异步

实现流程:

1.配置异步支持;

  在Spring中定义一个TaskExecutor 的Bean异步支持。

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
    @Bean
    public ThreadPoolTaskExecutor taskExecutor(){
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setCorePoolSize(2);
        taskExecutor.setMaxPoolSize(5);
        taskExecutor.setQueueCapacity(10);
        taskExecutor.initialize();
        return taskExecutor;
    }
}
异步配置

2.定义异步方法;

  在服务层定义一个带有 @Async 注解的方法,该方法将负责异步增加用户的功能。

@Async
    public void asyncAddUser(User user) {
        User newUser = new User();
        //根据雪花算法生成分布式ID(定义一个工厂,使用ConcurrentMap做缓存,提高分布式ID生成后查询效率)
        long userId = SnowFlakeFactory.getSnowFlakeFromCache().nextId();
        newUser.setId(userId);
        newUser.setUsername(user.getUsername());
        //利用shiro将username作为盐值加密的Hash的MD5加密算法加密
        String md5Pwd = CommonUtils.encryptPassword(newUser.getUsername(), user.getPassword());
        newUser.setPassword(md5Pwd);
        int i = userMapper.addUser(newUser);
        if (i == 0) throw new TestException("插入用户数据失败!");
        logger.info("插入"+user.getUsername()+"成功!");
    }
异步实现增加用户

 3.结果

数据库结果

2.2 使用ApplicationEvent和ApplicationListener实现异步

实现流程:

1.定义事件

  定义一个事件,继承ApplicationEvent添加任何你想要传递给监听器的数据

public class AddUserEvent extends ApplicationEvent {

    private String username;

    private String password;


    public AddUserEvent(Object source, String username, String password) {
        super(source);
        this.username = username;
        this.password = password;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}
添加用户事件

2.创建一个监听器

  创建一个监听器来监听AddUserEvent事件。当事件被发布时,监听器将执行相应的操作

@Slf4j
public class AddUserEventListener implements ApplicationListener<AddUserEvent> {

    @Autowired(required = false)
    private UserMapper userMapper;

    @Override
    public void onApplicationEvent(AddUserEvent addUserEvent) {
        log.info("接收事件,开始添加用户...");
        if (Objects.isNull(addUserEvent)){
            log.error(TestConstants.UserEvent.EVENT_PARAM_NULL_CODE, TestConstants.UserEvent.EVENT_PARAM_NULL_MSG);
        }
        User newUser = new User();
        String username = addUserEvent.getUsername();
        String password = addUserEvent.getPassword();
        long userId = SnowFlakeFactory.getSnowFlakeFromCache().nextId();
        newUser.setId(userId);
        newUser.setUsername(username);
        //利用shiro将username作为盐值加密的Hash的MD5加密算法加密
        String md5Pwd = CommonUtils.encryptPassword(newUser.getUsername(), password);
        newUser.setPassword(md5Pwd);
        int i = userMapper.addUser(newUser);
        if (i == 0) throw new TestException("插入用户数据失败!");
        log.info("插入"+username+"成功!");
    }
}
新增用户监听器

3.发布事件

  在你的服务层或者控制器中,当一个新用户被创建后,你可以通过ApplicationEventPublisher接口来发布事件

@Service
public class UserServiceImpl implements UserService {

    private static final Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);

    @Resource
    private UserMapper userMapper;

    /**
     * 在服务层或者控制器中,当一个新用户被创建后,通过ApplicationEventPublisher接口来发布事件。
     * @param user
     */
    @Override
    public void asyncAddUser1(User user) {
        //发布添加用户事件
        /**
         * UserService 是触发事件的对象,因此我们将其作为 AddUserEvent 的 source 参数。
         * 优点:任何监听 AddUserEvent 的组件都可以访问到 source,并通过调用 getSource() 方法来获取 UserService 的引用
         */
        AddUserEvent addUserEvent = new AddUserEvent(this, user.getUsername(), user.getPassword());
        publisher.publishEvent(addUserEvent);
        logger.info("添加用户的任务已发布!");
    }
}
发布事件

4.配置一个监听器

  确保监听器被Spring容器管理并且能够接收到事件。可以通过配置类或注解的方式进行注册

@Configuration
public class AsyncConfig implements AsyncConfigurer {
  
    //注册监听器到Spring中并接收事件
    @Bean
    public AddUserEventListener addUserEventListener(){
        return new AddUserEventListener();
    }
}
异步事件配置

 5.结果

 2.3 通过RabbitMQ实现异步用户新增

  queue_nameexchange_namerouting_key之间的关系

  它们共同决定了消息如何从生产者传递到消费者。下面是对这三个概念的具体解释及其相互关系:

1. Queue (队列)

  • 定义: 队列是消息的容器,它是消息最终被发送到的地方。每个队列都有一个唯一的名称。
  • 作用: 存储消息直到消费者(worker)将其取出并处理。
  • 特性:
    • 每个队列只能由一个消费者消费。
    • 队列可以被声明为持久化或非持久化。
    • 队列可以被声明为自动删除(auto-delete)。
  • 示例: my_queue 是一个队列名称。

2. Exchange (交换机)

  • 定义: 交换机是接收来自生产者的消息并将这些消息路由到队列的组件
  • 作用: 根据特定的规则将消息发送到零个或多个队列。
  • 类型:
    • Direct: 根据具体的路由键将消息路由到队列。
    • Fanout: 将所有消息广播到绑定的所有队列。
    • Topic: 使用模式匹配的方式根据路由键将消息路由到队列。
    • Headers: 不使用路由键,而是基于消息头部的信息来路由。
  • 示例: my_exchange 是一个交换机名称。

3. Routing Key (路由键)

  • 定义: 路由键是在消息发布时使用的键值,用于决定消息应该被发送到哪个队列
  • 作用: 结合交换机的类型来确定消息的路由路径
  • 特性:
    • 路由键可以为空(默认为空字符串)。
    • 路由键可以包含点分隔符来表达层级结构。
  • 示例: my_routing_key 是一个路由键名称。

4. 实现流程

1.依赖以及配置

依赖
<!--RabbitMQ 依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>


配置文件
# rabbitmq配置
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
依赖以及配置文件

2.RabbitMq配置类

  通过RabbitMq实现异步操作需要的配置注意点:

  1. 必须配置不可变且不重复交换机名称、队列名称以及绑定队列到交换机的bingdingBean;
  2. 有几个队列则需要几个绑定队列到交换即的Bean;
  3. 在交换机和队列中的构造函数中都包含名称、是否持久化、是否自动删除等属性;
  4. 交换机类型可以分为四种一种direct(一对一)、fanout(广播)、topic(一对一或一对多)、headers(消息headers匹配)
  5. 队列除了交换机的集中属性外,还具有排他属性(只允许一中消息一个消费者消费)、map属性(消息什么是否被删除)
@Configuration
public class RabbitMqConfig {

    public static final String QUEUE_NAME = "user_queue";
    public static final String EXCHANGE_NAME = "user_exchange";
    public static final String ROUTE_KEY_NAME = "user_route_key";

    @Bean
    public Queue userQueue(){
        return new Queue(QUEUE_NAME);
    }

    @Bean
    public DirectExchange userExchange() {
        return new DirectExchange(EXCHANGE_NAME);
    }

    @Bean
    public Binding userBinding(Queue userQueue, DirectExchange userExchange) {
        return BindingBuilder.bind(userQueue).to(userExchange).with(ROUTE_KEY_NAME);
    }
}
RabbitMq配置类

3.消息发布者

   创建一个消费者类,用于监听RabbitMQ的消息队列并处理用户数据。

public class UserConsumer {
    @Autowired
    private UserService userService;

    @RabbitListener(queues = RabbitMqConfig.QUEUE_NAME)
    public void processUser(User user) {
        log.info("Received user: " + user);
        // 在这里执行保存用户到数据库的操作
        userService.addUser(user);
        log.info("新增用户成功!");
    }
}
消息消费者

4.消息生产者

  创建一个生产者类,用于发送用户数据到RabbitMQ的消息队列。

@Slf4j
@Component
public class UserProducer {
    private final RabbitTemplate rabbitTemplate;

    @Autowired
    public UserProducer(RabbitTemplate rabbitTemplate){
        this.rabbitTemplate = rabbitTemplate;
    }

    public void sendUser(User user){
        rabbitTemplate.convertAndSend(RabbitMqConfig.EXCHANGE_NAME, RabbitMqConfig.ROUTE_KEY_NAME, user);
        log.info("send User:"+user);
    }
}
生产者

5.结果

参考链接

这8种java异步实现方式,性能炸裂!-CSDN博客

posted @ 2024-08-04 18:34  求知律己  阅读(2)  评论(0编辑  收藏  举报