SpringBoot与RabbitMQ 集成以及死信队列,TTL,延迟队列

news2025/1/30 16:01:07
  • 简单示例
    • 项目结构
    • 依赖
    • 配置
    • 生产者
    • 消费者
  • 消息的可靠投递示例
    • confirm
    • return
  • 消费者确认机制
  • 消费端限流
  • TTL
    • 单条消息
    • 整个队列设置 TTL
  • 死信队列
    • 死信队列的实现步骤
  • 延迟队列
  • 消息幂等设计

简单示例

项目结构

依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>RELEASE</version>
        <scope>compile</scope>
    </dependency>
</dependencies>

配置

spring:
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: dc
    password: Aa111111
    virtual-host: /dc0407
    # 让发布者可以收到消息是否投递成功的回调
    # 默认是 false
    publisher-confirms: true

    listener:
      simple:
        # 消费者开启手动确认模式,默认是 none 
        acknowledge-mode: manual
        # 每次消费一个
        prefetch: 1
package io.dc.rabbitmqinspringboot.config;

import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitMQConfig {
    public static final String EXCHANGE_NAME = "boot_topic_exchange";
    public static final String QUEUE_NAME = "boot_queue";

    // 1. 声明交换机
    @Bean("bootTopicExchange")
    public Exchange bootExchange(){
        return ExchangeBuilder.topicExchange(EXCHANGE_NAME).durable(false).build();
    }

    // 2. 声明队列
    @Bean("bootQueue")
    public Queue bootQueue(){
        return QueueBuilder.durable(QUEUE_NAME).build();
    }

    // 3. 将队列与交换器进行绑定
    @Bean
    public Binding bindQueueExchange(@Qualifier("bootQueue") Queue queue, @Qualifier("bootTopicExchange") Exchange exchange){
        return BindingBuilder.bind(queue).to(exchange).with("boot.#").noargs();
    }

    // 自定义连接工厂,自由程度高,本次演示没用到
    @Bean("customConnectionFactory")
    public ConnectionFactory connectionFactory(
            @Value("${spring.rabbitmq.host}") String host,
            @Value("${spring.rabbitmq.port}") int port,
            @Value("${spring.rabbitmq.username}") String username,
            @Value("${spring.rabbitmq.password}") String password,
            @Value("${spring.rabbitmq.virtual-host}") String vhost
    ){
        CachingConnectionFactory factory = new CachingConnectionFactory();
        factory.setHost(host);
        factory.setPort(port);
        factory.setUsername(username);
        factory.setPassword(password);
        factory.setVirtualHost(vhost);

        return factory;
    }

    // 自定义消息监听器工厂,自由程度高
    @Bean(name = "customListenFactory")
    public SimpleRabbitListenerContainerFactory listenerContainerFactory(@Qualifier("customConnectionFactory") ConnectionFactory connectionFactory){
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        // 设置手动签收
        factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
        // 设置预处理数量,同时没有确认的消息不能超过 N 个,起到消费者限流的作用,详细解释见下
        factory.setPrefetchCount(5);
        return factory;
    }
}

生产者

在测试类中发消息


package io.dc.rabbitmqinspringboot;

import io.dc.rabbitmqinspringboot.config.RabbitMQConfig;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@SpringBootTest
@RunWith(SpringJUnit4ClassRunner.class)
public class RabbitMqInSpringBootApplicationTests {

    @Autowired
    RabbitTemplate rabbitTemplate;

   // 发一次消息
    @Test
    public void sendMsg() {
        rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME,"boot.dc","你好 rabbitMQ");
    }
    // 批量发消息
    @Test
    public void sendBatchMsg() {
        for (int i = 0; i < 10; i++) {
            rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME,"boot.dc","你好 rabbitMQ");
        }
    }
}

消费者

简单版,自动 ack

package io.dc.rabbitmqinspringboot.consumer;

import io.dc.rabbitmqinspringboot.config.RabbitMQConfig;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import org.springframework.amqp.core.Message;

@Component
public class Consumer1 {
    @RabbitListener(queues = RabbitMQConfig.QUEUE_NAME)
    public void listenerQueue(Message message){
        System.out.println("消费者 1 接收到消息:"+ message);
    }
}

手动 ack:
需要在配置文件中额外设置为手动确认:manual

package io.dc.rabbitmqinspringboot.consumer;

import com.rabbitmq.client.Channel;
import io.dc.rabbitmqinspringboot.config.RabbitMQConfig;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;

@Component
public class Consumer2 {
    // 也可以采用自定义监听工厂
    // @RabbitListener(queues = RabbitMQConfig.QUEUE_NAME, containerFactory = "customListenFactory")
    @RabbitListener(queues = RabbitMQConfig.QUEUE_NAME)
    public void onMessage(Message message, Channel channel) throws Exception {
        System.out.println("消费者 2 收到消息:" + new String(message.getBody()));
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        System.out.println("消息 Id: " + deliveryTag);
        // 进行消息签收,如果不签收,将只会收到 ${listener.simple.prefetch} 个消息,因为设置了手动签收模式
        channel.basicAck(deliveryTag, true);
        
        // 拒绝签收
        // 最后一个参数: 是否重回队列
//        channel.basicNack(deliveryTag,false, false);
    }
}

消息的可靠投递示例

在 RabbitMQ 的基本概念和五种模式使用示例 中已经介绍了两种实现可靠投递的机制,这里仅作为一个完整的补充:

confirm

@Test
public void testConfirm(){
    rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
        @Override
        public void confirm(CorrelationData correlationData, boolean b, String s) {
            System.out.println("执行了 confirm 方法。..");
            if(b){
                System.out.println("发送成功");
            }else{
                System.out.println("发送失败:" + s);
            }
        }
    });

    // 正常
    rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME,"boot.dc","你好 rabbitMQ");
    // 错误的交换器。会打印发送失败
    rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME + "111","boot.dc","你好 rabbitMQ");
    try {
        Thread.sleep(5000);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
}

return

@Test
public void testReturn(){
    // 只有设置 true,消息无法到达队列时,才会退回给生产者
    rabbitTemplate.setMandatory(true);
    rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
        /**
        *
        * @param message the returned message.
        * @param replyCode the reply code.
        * @param replyText the reply text.
        * @param exchange the exchange.
        * @param routingKey the routing key.
        */
        @Override
        public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
            System.out.println("消息退回了~");
            System.out.println("message: " + message.toString());
            System.out.println("replyCode: " + replyCode);
            System.out.println("replyText: " + replyText);
            System.out.println("exchange: " + exchange);
            System.out.println("routingKey: " + routingKey);
        }
    });
    // 正常
    rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME,"boot.dc","你好 rabbitMQ");
    // 错误的路由 key。消息会被退回
    rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME,"1111_boot.dc","你好 rabbitMQ");
    try {
        Thread.sleep(5000);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
}

消费者确认机制

Ack 指的是 ack 指 Acknowledge,确认。 表示消费端收到消息后的确认方式。

有三种确认方式:

  • 自动确认 acknowledge = none
  • 手动确认 acknowledge = manual
  • 根据异常情况确认 acknowledge = auto 这种情况使用麻烦,一般不用

其中自动确认是指,当消息一旦被 Consumer 接收到,则自动确认收到,并将相应 message 从 RabbitMQ 的消息缓存中移除。但是在实际业务处理中,很可能消息接收到,业务处理出现异常,那么该消息就会丢失。如果设置了手动确认方式,则需要在业务处理成功后,调用 channel.basicAck(),手动签收,如果出现异常,则调用 channel.basicNack() 方法,让其自动重新发送消息。

消费者调用 basicAck 或者 basicNack 签收或拒绝

消息可靠性总结:

  1. 持久化: exchange 要持久化,queue 要持久化, message 要持久化
  2. 生产者要 confirm
  3. 消费者要 ack
  4. Broker 要高可用

消费端限流

假设有一个场景:服务端挤压了大量的消息消息,此时启动消费者客户端,大量的消息会瞬间流入该客户端,可能会让客户端宕机。

当数据量特别大的时候,对生产者限制肯定是不科学的,这是用户的行为,我们应该对消费端限流。

RabbitMQ 提供了一种 qos(服务质量保证)功能,在非自动确认消息的前提下,如果一定数目的消息未被确认前,不消费新得消息

设置方法:

/**
    * Request specific "quality of service" settings.
    *
    * These settings impose limits on the amount of data the server
    * will deliver to consumers before requiring acknowledgements.
    * Thus they provide a means of consumer-initiated flow control.
    * @see com.rabbitmq.client.AMQP.Basic.Qos
    * @param prefetchSize maximum amount of content (measured in
    * octets) that the server will deliver, 0 if unlimited
    * @param prefetchCount maximum number of messages that the server
    * will deliver, 0 if unlimited
    * @param global true if the settings should be applied to the
    * entire channel rather than each consumer
    * @throws java.io.IOException if an error is encountered
    */
void basicQos(int prefetchSize, int prefetchCount, boolean global) throws IOException;
  • prefetchSize: 每条消息得大小。0: 不限制 注意: RabbitMQ 没有实现这个功能
  • prefetchCount: 一次性消费消息得数量,会告诉 RabbitMQ 不要同时个同一个客户端推送对于 N 个消息,也就是一旦超过 N 个消息美欧被 ack,则该客户端就会阻塞,知道有消息 ack
  • global: true / false 是否将上面得设置应用到 channel,简单得说上面得限制是 channel 级别还是某个消费者客户端级别。设置 false 的时候生效,因为 rabbitmq 没有实现 channel 级别的控制

在 sprinBoot 中,对客户端限流只需配置 prefetch 即可。和调用 basicQos(0, N, false) 效果一样

TTL

TTL 全称是 Time To Live (存活时间), 当消息到达存活时间后还没有被消费就会自动清除,这与 redis 中的过期时间概念类似,我们应该合理应用 TTL,可以有效的处理过期的垃圾信息,从而降低服务器的负载。

RabbitMQ 既可以对单条消息设置存活时间,也可以对整个队列设置

单条消息

@Test
public void sendMsgWithTtl() {

    MessageProperties properties1 = new MessageProperties();
    properties1.setExpiration("6000");
    Message message = new Message("你好".getBytes(),properties1);

    rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME,"boot.dc",message);
}

整个队列设置 TTL

可以在后管设置:

也可以在代码中设置:

// 2. 声明队列
@Bean("bootQueueTTL")
public Queue bootQueue(){
    Map<String, Object> arg = new HashMap<>();
    arg.put("x-message-ttl",10000);
    return QueueBuilder.durable("bootQueueTTL").withArguments(arg).build();
}

后管查看结果:

如果两者都设置了 TTL 以短的时间为准

死信队列

没有被及时消费的消息将被投放到一个特殊队列,被称为死信队列

没有被及时消费的原因:

  1. 消息被拒绝(basic.reject, basic.nack), 并且不再重新投递,(requeue = false)
  2. 消息超时未被消费
  3. 队列长度达到最大

死信队列的实现步骤

声明队列Q的时候,在附加参数 x-dead-letter-exchange 指定交换器E的名称,只是声明,并非绑定。 它的含义是,当在队列Q上产生死信时,该消息会通过交换器 E 发走。
就这么简单,至于 E 会把消息发送发送到哪里,就看交换器E绑定了哪些队列。

代码示例:


// 带有死信的队列
public static final String QUEUE_NAME_WITH_DEAD = "boot_queue_with_dead";

// 死信消息从正常队列中移除,通过该交换机进入死信队列
// 和正常交换机没有差别,只不过被带有死信的队列指定了
public static final String deadExchange = "dead_exchange";
// 死信队列,接收死信交换机过来的消息
public static String deadQueue = "dead_queue";


// 声明死信交换器,和普通交换器一样
@Bean("bootDeadExchange")
public Exchange bootDeadExchange() {
    return ExchangeBuilder.topicExchange(deadExchange).durable(false).build();
}

// 声明接收死信的队列
@Bean("bootDeadQueue")
public Queue bootDeadQueue(){
    return QueueBuilder.durable(deadQueue).build();
}
// 把接收死信的队列与死信交换器绑定
@Bean
public Binding bindWithDeadQueueExchange(@Qualifier("bootDeadQueue") Queue queue, @Qualifier("bootDeadExchange") Exchange exchange){

    return BindingBuilder.bind(queue).to(exchange).with("boot.#").noargs();
}

//********* 这才是死信队列的重要步骤 ***************
// 声明带有死信的队列
@Bean("bootWithDeadQueue")
public Queue bootWithDeadQueue(){
    Map<String, Object> arg = new HashMap<>();
    // 声明接收死信消息的交换器
    // *****这一步很重要,可以让死信通过 deadExchange 发走****
    arg.put("x-dead-letter-exchange",deadExchange);
    return QueueBuilder.durable(QUEUE_NAME_WITH_DEAD).withArguments(arg).build();
}

// 将带有死信的队列绑定到交换机上
@Bean
public Binding bindDeadQueueExchange(@Qualifier("bootWithDeadQueue") Queue queue, @Qualifier("bootTopicExchange") Exchange exchange){
    return BindingBuilder.bind(queue).to(exchange).with("boot.#").noargs();
}


// 发消息
@Test
public void sendDeadMsg() {

    MessageProperties properties = new MessageProperties();
    properties.setExpiration("6000");
    Message message = new Message("你好,6秒后我将要从列中移除,并进入死信队列".getBytes(),properties);

    rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME,"boot.hei",message);
}

效果如下:

六秒后:

延迟队列

RabbitMQ 没有自己实现延迟队列,但是我们可以借助TTL 和死信队列完成延迟队列的功能。

  1. 把需要延时处理的消息设置TTL,并发送到带有死信的队列中。
  2. 消费者监听死信队列

消息幂等设计

幂等设计是一种设计思想,一次或多次请求同一个资源,对资源本身应该有同样的结果,也就是说任意执行多次对资源本身产生的影响与执行一次的影响相同

在MQ中,消费多条相同的消息应该与只消费一次带来的效果相同

可以采用乐观锁的方式实现消息幂等:

以sql 更新语句为例:

-- version 某时刻等于 1
update account set price = price - 100, version = version + 1 where id = 1 and version  = 1;
-- 把版本号作为更新语句的条件,同时版本号自增

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Linux 裸机篇(一)】ARM Cortex-A 架构基础、汇编基础

目录一、ARM Cortex-A 架构基础1. Cortex-A 处理器运行模型2. Cortex-A 寄存器组2.1 通用寄存器2.1.1 未备份寄存器2.1.2 备份寄存器2.1.3 程序计数器 R152.2 程序状态寄存器二、ARM 汇编基础1. GNU 汇编语法1.1 语句1.2 段1.3 伪操作1.4 函数2. 常用汇编指令2.1 处理器内部数据…

你的订婚|结婚纪念日是质数吗?进来测算看看……

今年开年以来&#xff0c;随着ChatGPT的爆火&#xff0c;原本一直平静的三六零安全科技股份有限公司&#xff08;下称360&#xff09;股价仅2月以来涨幅就达到近200%。然而4月4日晚间&#xff0c;360发布公告称&#xff0c;公司董事长周鸿祎与妻子胡欢离婚。有意思的是&#xf…

【Java版oj】day25星际密码、数根

目录 一、洗牌 &#xff08;1&#xff09;原题再现 &#xff08;2&#xff09;问题分析 &#xff08;3&#xff09;完整代码 二、数根 &#xff08;1&#xff09;原题再现 &#xff08;2&#xff09;问题分析 &#xff08;3&#xff09;完整代码 一、洗牌 &#xff08;1&…

过度的焦虑 到底有多糟

你们知道过度的焦虑到底有多糟糕吗&#xff1f; 现在生活节奏越来越快&#xff0c;不管是生活、工作还是学习&#xff0c;很多方面都给我们带来了很多的压力问题&#xff0c;我们所承受的负担越来越重&#xff0c;很多人时常处于一种非常疲劳、过度的焦虑的状态。 你们知道过度…

什么是Node.js

文章目录什么是Node.js简介常用命令Node内置模块Node.js和JavaScript的区别什么是Node.js 简介 Node.js是一个基于Chrome V8引擎的JavaScript运行环境。它允许开发者使用JavaScript编写服务器端代码&#xff0c;而不仅仅是浏览器端的代码。Node.js的出现使得JavaScript可以在…

Vue环境下安装Less|Sass|Stylus(详细指南)

Vue_Cli环境下如何使用less、sass、stylus&#xff1f;报错如何解决&#xff1f; 安装Less 依次使用以下npm执行命令即可完后less的安装 npm install lessnpm install less-loader在vue组件style中使用less <style lang"less"></style>安装Sass(三者之…

如何抓住ChatGPT的热潮,打造小红书爆款

如何抓住ChatGPT的热潮&#xff0c;打造小红书爆款 前两周我看到一个小红书才申请了没多久就已经有好几万的粉丝&#xff0c;于是我让我老婆也赶紧注册一个&#xff0c;毕竟小红书也有着不错的用户群体 那么我们如何通过GPT辅助我们快速创作呢&#xff1f;先来看下ChatGPT的回答…

《离散数学导学》精炼——第6,7章(类型集合论,谓词逻辑)

引言 笔者一直觉得在计算机这一学科的学习中&#xff0c;离散数学是极为重要的知识基础。离散化的思想体现在计算机学科的方方面面。举例来说&#xff0c;“像素”这一概念是我们日常生活中耳熟能详的&#xff0c;将一个图片拆分成一个个极微小的像素&#xff0c;就是利用了离…

[1] 顺序表实现

一、引入顺序表 提出问题&#xff1a; 顺序表底层是一个数组&#xff0c;为什么不是直接操作数组就好了&#xff0c;还要单独写一个类&#xff0c;说底层是数组呢&#xff1f;&#xff1f; 因为顺序表可以有更多的操作&#xff1a; 比如一个数组&#xff0c;我们没有办法知…

Android 11.0 原生SystemUI下拉通知栏UI背景设置为圆角背景的定制(二)

1.前言 在11.0的系统rom定制化开发中,在原生系统SystemUI下拉状态栏的下拉通知栏的背景默认是白色四角的背景, 由于在产品设计中,在对下拉通知栏通知的背景需要把四角背景默认改成圆角背景,所以就需要分析系统原生下拉通知栏的每条通知的默认背景, 然后通过systemui的通知…

MobTech 秒验|极速验证,拉新无忧

一、运营拓展新用户的难题 运营拓展新用户是每个应用都需要面对的问题&#xff0c;但是在实际操作中&#xff0c;往往会遇到一些困难。其中一个主要的难题就是注册和登录的繁琐性。用户在使用一个新的应用时&#xff0c;通常需要填写手机号、获取验证码、输入验证码等步骤&…

Java-红黑树的实现

目录一、概述二、红黑树的操作1. 变色2. 左旋与右旋3. 插入节点4. 删除节点三、手写代码1. 通用方法2. 中序遍历3. 左旋4. 右旋5. 添加节点6. 删除节点四、完整代码五、测试1. 红黑树打印类2. 测试代码3. 测试结果一、概述 关于红黑树的学习&#xff0c;先推荐给大家一个网址&…

Centos7安装部署Jenkins

Jenkins简介&#xff1a; Jenkins只是一个平台&#xff0c;真正运作的都是插件。这就是jenkins流行的原因&#xff0c;因为jenkins什么插件都有 Hudson是Jenkins的前身&#xff0c;是基于Java开发的一种持续集成工具&#xff0c;用于监控程序重复的工作&#xff0c;Hudson后来被…

文章自动生成器 -原创文章生成器在线版

怎么将ChatGPT生成文章保存 在使用ChatGPT生成文章后&#xff0c;您可以使用以下几种方法将其保存起来&#xff1a; 复制粘贴&#xff1a;最简单的方法是将生成的文章文本复制并粘贴到文本编辑器或其他文本处理软件中&#xff0c;如Word文档或Google Docs&#xff0c;以保存文…

I2C通信

一、理论上了解I2C时序 I2C写数据时序如图&#xff1a; 通过解析器解析I2C通信如上图&#xff08;SCL和SDA反了&#xff09;。 1---起始信号 2、3---应答信号ACK 5---停止信号 起始信号&#xff1a;SCL线是高电平时&#xff0c;SDA线从高电平向低电平切换。 停…

一个大二学生送给大一学弟学妹的建议

博主简介&#xff1a;先简单的介绍一下我吧&#xff0c;本人是一名大二学生&#xff0c;来自四川。目前所学专业是人工智能&#xff0c;致力于在CSDN平台分享自己的学习内容。 我为什么要写这篇文章&#xff1f; 我来到CSDN也已经一年了&#xff0c;在这一年里面&#xff0c;我…

go binary包

binary包使用与详解 最近在看一个第三方包的库源码&#xff0c;bigcache&#xff0c;发现其中用到了binary 里面的函数&#xff0c;所以准备研究一下。 可以看到binary 包位于encoding/binary&#xff0c;也就是表示这个包的作用是编辑码作用的&#xff0c;看到文档给出的解释…

加密的本质:数学的不对称性

文章目录 引言I 预备知识1.1 加密和授权1.2 非对称的特性II 椭圆曲线加密的方法2.1 椭圆曲线2.2 椭圆曲线的性质引言 不对称有时却自有其妙处与美感,比如黄金分割就是不对称的。 可以通过加密和授权,兼顾保护信息不外泄,而且某些得到授权的人还能使用信息。 I 预备知识 …

2022年人民满意手机银行发展洞察

易观&#xff1a;商业银行积极践行“金融为民”&#xff0c;坚持“以用户为中心”的发展理念&#xff0c;从全客群、全服务、全渠道推动金融服务触达广大人民群众。其中&#xff0c;手机银行作为服务及经营主阵地&#xff0c;是人民群众获取金融服务的超级入口及服务平台。 “以…

键盘摄影:今天老李是一名动物摄影师

键摄&#xff0c;全称键盘摄影师。原本是一个贬义词&#xff0c;是指那些没有相机&#xff0c;没有实拍经验&#xff0c;仅凭一副鼠标键盘&#xff0c;在家里打字&#xff0c;在网上头头是道地分享摄影技巧&#xff0c;同时对别人的作品指指点点&#xff0c;然后又无法秀出自己…