详解消息队列

序言
众所周知,互联网系统具有“三高”的特性(高可用 H-Availability、高扩展 H-Expansion、高性能 H-Performance),而消息队列在提升系统“三高”特性方面,是必不可少的利器。
首先,消息队列以其多副本设置宕机选主策略,实现自身的高可用;
其次,消息队列以其分区策略,实现消费端的高扩展;
最后,消息队列天然适合异步处理负载均衡的场景,异步处理实现调用方与被调用方的解耦,缩短了调用方的等待时间;负载均衡指的是消费者组的概念,单个生产者的消息可以向多个消费者分摊消息,可极大提升整个系统的分发与处理性能。例如博主亲身经历的某智能对话系统,在单笔交易60秒的前提下,单机压测,QPS破百、并发通路数破千,是可以达到的目标。

关联文章

SpringBoot工程搭建

详解缓存Redis

详解数仓ElasticSearch

详解消息中间件Kafka

作为springBoot框架使用的第四篇,本章详解消息队列的使用,现在读者已经是一个成熟的读者了,这章我们将使用consul作为配置中心,稍微贴近企业级应用的开发。

本文目录

  1. 消息队列的作用
  2. Kafka 的基本概念
  3. Kafka 消息设计(源码解析)
  4. Kafka 生产者、消费者设计(源码解析)
  5. Kafka 消息积压,定位及解决方案
  6. Kafka 事务不生效,定位与解决方案
  7. 经验总结

1、消息队列的作用

概念解释
异步处理降低服务端响应时延,反之,若使用Http等同步请求,客户端需阻塞等待服务端处理完成
负载均衡实现多实例对消息分发处理,通过主题、分区、消费者组设置,实现实例间均衡消费的“发布-订阅”模式

可能读者不太理解负载均衡能带来好处,作者曾有过深刻体会:在多实例场景下,采用分布式锁可做简单的消息分发,即竞争到锁的实例处理当前消息(例如某个批次任务的处理),但是在消息量一旦加大后,系统在分发效率方面、公平性两方面大打折扣,使用消息队列做分发可解决这个难题,在这里推荐Kafka的“发布-订阅”模式。
值得一提的是,消息队列除了Kafka,还有很多优秀的组件,例如RocketMQ,这些中间件之间的差别对比,暂时不在本次讨论范围。
作者选择Kafka呢?其实是考虑到系统要求极高的吞吐性能(上百QPS),但对于SLA、事务方面没有太高的要求(SLA两个9,事务可选项),读者可按需求选择符合自身业务需求的消息中间件。

2、Kafka 的基本概念

概念定义应用场景
Topic消息主题,同类消息的约定名称消息的分发与消费都是以主题为记号
Broker数部署kafka服务的实例数,由于单个实例可以部署多个Kafka服务,准确定义是Kafka服务的端口数是集群实现的物理保证
副本数每份消息的副本数量用于灾备,一般设置小于等于broker数,因为宕机是以broker为单位,副本数间接影响ISR数量
Partition数Topic对应的消息被切分的份数提升消息消费能力,设置为大于等于消费者数量,该值与broker数无关

Kafka的详细参数及设计理念备查

来一个直观的感受,topic名称叫做HC_People_Info,3个分区,1个副本
kafka信息

3、Kafka 消息设计

名称设计原则
Topic以服务模块名称作为主题名,不要滥用,如果单个服务模块确实主题很多,可以下设二级目录
Key一般不设置,若消息间需要保序可设置,功能是避免消息的先发后到。例如,客户端先点击任务启动,再暂停,经过消息队列传导后,服务端不能接受到信息为先暂停、后启动;此时设置taskId为key,相同任务的消息就被Hash到同一个partition,在Kafka层面,可保证被顺序消费
Value被封装一个类,例如kafkaMsg,注意在传输前,需序列化成字符串

Value包含以下关键信息

  • 事件ID:方便消息的定位、同一主题可容纳多个事件
  • 事件名:方便消息定位
  • 唯一序号:方便消费端做幂等
  • 时间戳:定位消息事件发生时间(到微妙)
  • 具体的业务数据

下面,我们以传递“人的信息”为例子,实现上面的消息设计,假定人的信息只包含年龄、名称

  • 配置信息
  • 配置类,承接配置信息
  • Value类

配置信息

spring:
  kafka:
    # kafka服务所在机器,需要填写broker的IP+port
    bootstrap-servers: xxx.xxx.xxx.xxx:xxxx
    producer:
      # 保证leader和follower都收到后,给生产者返回ack,持久性最好,时间延迟最大
      acks: -1
      # 发送不成功,重试三次
      retries: 3
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.apache.kafka.common.serialization.StringSerializer
      max.in.flight.requests.per.connection: 1
    consumer:
      # 消费者群默认名称;消息只会被消费者组下的一个实例消费;可以在consumerListener上单独定制消费者组
      group-id: localGroup
      enable-auto-commit: false
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      # auto-commit-interval: 100 # ms
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
# kafka的主题汇总,关键信息!!!
kafkaTopic:
  # 模块名
  demo:
    # 主题名
    topic: HC_People_Info
    # 事件id及事件名称
    ageRequest:   
      id: 3451662860023003
      name: Evt_people_age_Request
    nameRequest:
      id: 3451662860023004
      name: Evt_people_name_Request

配置类

@Service
@Data
public class DemoConfig {
    @Value("${kafkaTopic.demo.topic}")
    private String topic;

    // 事件ID
    @Value("${kafkaTopic.demo.ageRequest.id}")
    private Long ageRequestId;

    // 事件名称
    @Value("${kafkaTopic.demo.ageRequest.name}")
    private String ageRequestName;

    // 事件ID
    @Value("${kafkaTopic.demo.nameRequest.id}")
    private Long nameRequestId;

    // 事件名称
    @Value("${kafkaTopic.demo.nameRequest.name}")
    private String nameRequestName;

}

Value类

@Data
@Slf4j
public class Msg {
    // 事件ID
    private Long eventId;

    // 事件名称
    private String eventName;

    // 时间戳
    private Long timestamp;

    // 序列号
    private String sequenceNum;

    // 业务数据
    private Object object;

    public Msg(){}

    public Msg(Long eventId, String eventName, String sequenceNum, Object object) {
        this.eventId = eventId;
        this.eventName = eventName;
        this.timestamp = new Date().getTime();
        this.sequenceNum = sequenceNum;
        this.object = object;
    }
}

4、Kafka 生产者、消费者设计(源码解析)

我们在上一节已设计好了主题,那如何写一个demo去调用Kafka呢?分为以下步骤,为方便演示,我们把生产者和消费者设置为同一个应用模块,通过浏览器调用写好的controller,演示数据流从浏览器、controller、producer、Kafka、consumer的流转过程。

  • 引入相关POM
  • 消费者、消费者服务
  • 生产者 、生产者服务
  • 序列化服务
  • controller编写
  • 验证数据流程

引入相关POM

<properties>
        <java.version>1.8</java.version>
        <lombok.version>1.18.12</lombok.version>
        <junit.version>4.12</junit.version>
        <spring-boot.version>2.0.7.RELEASE</spring-boot.version>
        <consul.version>2.1.2.RELEASE</consul.version>
        <elasticsearch.version>7.6.1</elasticsearch.version>
        <elasticsearch-data.version>4.0.0.RELEASE</elasticsearch-data.version>
 </properties>
    
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--kafka-->
        <dependency>
            <groupId>org.springframework.kafka</groupId>
            <artifactId>spring-kafka</artifactId>
        </dependency>

        // 如果想使用Kafka的事务功能,需要引入mysql相关包,因为在注入开启事务的注解时,需要指定数据源,数据源的实例化依赖于mysql的相关jar
        <!--mysql-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <!--数据库驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <!--配置中心相关-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-config</artifactId>
            <version>${consul.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-discovery</artifactId>
            <version>${consul.version}</version>
            <exclusions>
                <exclusion>
                    <artifactId>commons-collections</artifactId>
                    <groupId>commons-collections</groupId>
                </exclusion>
            </exclusions>
        </dependency>
  </dependencies>

消费者、消费者服务

@Component
@Slf4j
public class KafkaCustomer {


    @Autowired
    private NameServiceImpl nameService;

    @Autowired
    private DemoConfig demoConfig;

    /**
     * 处理kafka消息并分发
     *
     * @param msg
     */
    @KafkaListener(topics = "${kafkaTopic.demo.topic}")
    public void processDemoInfo(String msg) {
        try {
            log.info("demo msg  is [{}]", msg);
            Msg kafkaMsg = JsonUtils.fromJson(Msg.class, msg);
            log.info("kafkaMsg is [{}]", kafkaMsg);
            if (kafkaMsg.getEventName().equals(demoConfig.getNameRequestName())) {
                nameService.handle(kafkaMsg.getSequenceNum(), kafkaMsg.getObject());
            }
        } catch (Exception e) {
            log.error("process info error, msg:[{}]", msg, e);
        }
    }

}

// vo设置
@Data
public class NameRequest {
    private String name;
}

// nameServiceImp就是接受信息后打印
@Service
@Slf4j
public class NameServiceImpl implements ReceiverService {

    @Override
    public void handle(String seqId, Object data) {
        try {
            NameRequest request = JsonUtils.fromJson(NameRequest.class, JsonUtils.toJson(data));
            log.info("received:{}", request);
        } catch (Exception e) { }
    }
}

生产者、生产者服务

@Slf4j
public class KafkaProducer {

    public static KafkaTemplate<String, String> kafkaTemplate;

    static {
        kafkaTemplate = (KafkaTemplate<String, String>) SpringContext.getAppObject("kafkaTemplate");
    }

    /**
     * 事务提交后发送信息(不包含key值)
     */
    public static void sendMsgAfterTrans(String topic, Object msg) {
        if (TransactionSynchronizationManager.isActualTransactionActive()) { // 在事务中
            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
                @Override
                public void afterCommit() {
                    // 事务commit之后执行该方法
                    log.info("MsgSender sendMsgAfterTrans params, msg={}", msg);
                    try {
                        ListenableFuture<SendResult<String, String>> send =
                                kafkaTemplate.send(topic, JsonUtils.toJson(msg));
                        send.addCallback(new KafkaCallBack(topic, msg));
                    } catch (Exception e) {
                        log.error("MsgSender sendMsgAfterTrans error, msg={}", msg, e);
                    }
                    super.afterCommit();
                }
            });
        }
    }

    /**
     * 事务提交后发送信息(包含key值)
     */
    public static void sendMsgAfterTrans(String topic, String key, Object msg) {
        if (TransactionSynchronizationManager.isActualTransactionActive()) { // 在事务中
            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
                @Override
                public void afterCommit() {
                    // 事务commit之后执行该方法
                    log.info("MsgSender sendMsgAfterTrans params, msg={}", msg);
                    try {
                        ListenableFuture<SendResult<String, String>> send =
                                kafkaTemplate.send(topic, key, JsonUtils.toJson(msg));
                        // 发送成功或者失败会回调相应的方法(已被重写),打出日志
                        send.addCallback(new KafkaCallBack(topic, msg));
                    } catch (Exception e) {
                        log.error("MsgSender sendMsgAfterTrans error, msg={}", msg, e);
                    }
                    super.afterCommit();
                }
            });

        }
    }

    /**
     * 不开启事务的情况下,发送消息
     *
     * @param topic
     * @param key
     * @param msg
     */
    public static void sendMsgWithNoTrans(String topic, String key, Object msg) {
        log.info("MsgSender sendMsgWithNoTrans params, msg={}", msg);
        try {
            ListenableFuture<SendResult<String, String>> send =
                    kafkaTemplate.send(topic, key, JsonUtils.toJson(msg));
            send.addCallback(new KafkaCallBack(topic, msg));
        } catch (Exception e) {
            log.error("MsgSender sendMsgWithNoTrans error, msg={}", msg, e);
        }
    }
}

// 回调函数,成功或者失败通知
@Slf4j
public class KafkaCallBack implements ListenableFutureCallback<SendResult<String, String>> {

    private String topic;
    private String key;
    private Object msg;

    public KafkaCallBack(String topic, Object msg) {
        this.topic = topic;
        this.msg = msg;
    }

    public KafkaCallBack(String topic, String key, Object msg) {
        this.topic = topic;
        this.key = key;
        this.msg = msg;
    }

    @Override
    public void onFailure(Throwable throwable) {
        log.error("KafkaCallBack fail, Topic={}, Key={}, Msg={}", topic, key, msg);
    }

    @Override
    public void onSuccess(SendResult<String, String> stringStringSendResult) {
        log.info("KafkaCallBack success, Topic={}, Key={}, Msg={}", topic, key, msg);
    }
}

// 生产者服务
public interface SenderService {

    public void requestName(NameRequest request);

    public void requestNameTrans(NameRequest request);
}

@Service
public class SenderServiceImpl implements SenderService {

    @Autowired
    private DemoConfig demoConfig;

    public void requestName(NameRequest request) {
        Msg value = new Msg(demoConfig.getNameRequestId(), demoConfig.getNameRequestName(),
                UUID.randomUUID().toString(), request);
        KafkaProducer.sendMsgWithNoTrans(demoConfig.getTopic(),null, value);
    }

    // 支持事务,业务逻辑都完成后提交;否则回滚
    @Transactional
    public void requestNameTrans(NameRequest request) {
        Msg value = new Msg(demoConfig.getNameRequestId(), demoConfig.getNameRequestName(),
                UUID.randomUUID().toString(), request);
        KafkaProducer.sendMsgAfterTrans(demoConfig.getTopic(), value);
    }
}

序列化服务

// 序列化util
public class JsonUtils {

    /**
     * 使用ThreadLocal创建对象,防止出现线程安全问题
     */
    private static final ThreadLocal<ObjectMapper> OBJECT_MAPPER = new ThreadLocal<ObjectMapper>() {
        @Override
        protected ObjectMapper initialValue() {
            ObjectMapper objectMapper = new ObjectMapper();
            objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); // 忽略不存在的字段
            objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
            return objectMapper;
        }
    };

    /**
     * 禁止调用无参构造
     *
     * @throws IllegalAccessException
     */
    private JsonUtils() throws IllegalAccessException {
        throw new IllegalAccessException("Can't create an instance!");
    }

    /**
     * 将对象转化成json
     *
     * @param t
     *
     * @return
     *
     * @throws JsonProcessingException
     */
    public static <T> String toJson(T t) throws JsonProcessingException {
        return OBJECT_MAPPER.get().writeValueAsString(t);
    }

    /**
     * 将json转化成bean
     *
     * @param json
     * @param valueType
     *
     * @return
     *
     * @throws JsonParseException
     * @throws JsonMappingException
     * @throws IOException
     */
    public static <T> T fromJson(Class<T> valueType, String json)
            throws JsonParseException, JsonMappingException, IOException {
        return OBJECT_MAPPER.get().readValue(json, valueType);
    }

    /**
     * 将json转化成List
     *
     * @param json
     * @param collectionClass
     * @param elementClass
     *
     * @return
     *
     * @throws JsonParseException
     * @throws JsonMappingException
     * @throws IOException
     */
    public static <T> List<T> toList(String json, Class<? extends List> collectionClass, Class<T> elementClass)
            throws JsonParseException, JsonMappingException, IOException {
        JavaType javaType = OBJECT_MAPPER.get().getTypeFactory().constructCollectionType(collectionClass, elementClass);
        return OBJECT_MAPPER.get().readValue(json, javaType);
    }

    /**
     * 将json转化成Map
     *
     * @param json
     * @param mapClass
     * @param keyClass
     * @param valueClass
     *
     * @return
     *
     * @throws JsonParseException
     * @throws JsonMappingException
     * @throws IOException
     */
    public static <K, V> Map<K, V> toMap(String json, Class<? extends Map> mapClass, Class<K> keyClass,
                                         Class<V> valueClass)
            throws JsonParseException, JsonMappingException, IOException {
        JavaType javaType = OBJECT_MAPPER.get().getTypeFactory().constructMapType(mapClass, keyClass, valueClass);
        return OBJECT_MAPPER.get().readValue(json, javaType);
    }

    /**
     * 将POJO 对象转json
     * @param <K>
     * @param <V>
     * @param object
     * @return
     */
    public static <K, V> Map<? extends String, ?> toMap(Object object)
            throws JsonParseException, JsonMappingException, IOException {
        return OBJECT_MAPPER.get().convertValue(object, new TypeReference<Map<String, Object>>() { });
    }

}
// spring容器直接获取对象封装 
@Component
public class SpringContext implements ApplicationContextAware {
    private static ApplicationContext CONTEXT;

    public SpringContext() {
    }

    public void setApplicationContext(ApplicationContext context) throws BeansException {
        CONTEXT = context;
    }

    public static ApplicationContext getContext() {
        return CONTEXT;
    }

    public static boolean containsBean(String name) {
        return CONTEXT.containsBean(name);
    }

    public static Object getAppObject(String name) {
        return CONTEXT.getBean(name);
    }

    public static <T> T getAppObject(String name, Class<T> requiredType) {
        return CONTEXT.getBean(name, requiredType);
    }

    public static String getActiveProfile() {
        return CONTEXT.getEnvironment().getActiveProfiles()[0];
    }
}

controller

@Controller
public class AgentController {

    @Autowired
    private SenderServiceImpl senderService;

    @RequestMapping("/sendmsg")
    public String sendMsg() {
        NameRequest request=new NameRequest();
        request.setName("zhangsan");
        senderService.requestNameTrans(request);
        return "hello";
    }
}

// springBoot启动类
// 注意启动类上加上@EnableAsync,具体方法上加上@Async
@SpringBootApplication
@ServletComponentScan
@EnableAsync
// 开启事务
@EnableTransactionManagement
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

验证数据流程
完成上述配置后,终于等到了验证过程,我们来尝试run一下项目吧

成功启动springboot项目,并显示消息主题已创建,消息被当前服务订阅
成功启动springboot

观察topic的情况,分区情况、消费者组情况,LAG代表消费的延迟情况,若消息有积压,则LAG值会有体现。
Topic
调用controller后,看日志打印
生产者日志如下:
生产者日志
消费者日志如下
消费者日志
Kafka的Topic显示有一条消息,已经被消费
Kafka消息

5、Kafka 消息积压,定位及解决方案

经过上述四步,我们已经完成了简单的demo演示,但是距离实战还有一定的距离。下面5和6小节,我将分享两个使用过程中极容易遇到的问题与解决办法。
当压测期间,消息量达到一定数量级时候,我们会看到当生产者生产消息速度大于消费者消费速度时候,消息在Kafka中积压
作者将分以下步骤进行解析

  • 模拟消息积压
  • 定位消息积压
  • 解决消费积压

模拟消息积压
controller改动
消息发送
consumer服务改动,每次消费一条数据需要10秒
在这里插入图片描述

定位消息积压
可以看到三个分区的消息积压一共9条
在这里插入图片描述
解决消费积压
那我们如何让消息尽快被处理呢?第一,扩容消费者机器的实例;第二,增加Kafka单主题的partition数量;第三,增加单实例的消费能力
第一点和第二点都是运维的同事帮研发同学干了,但是对系统影响范围大,影响所有的主题;第三点是改动最小的,研发同学撸起袖子就直接上了,那我们来看看怎么增加单实例的消费能力——线程池及阻塞队列的使用

如果还不明白线程池使用的同学,可以移步至博主的多线程专题:

线程池单例的使用

话不多说,使用线程池的方式,直接上代码

#消费者线程池配置
kafkaconsumer.executor:
  # 核心线程数等于最大线程数,保证不会线程数过多导致OOM
  corePoolSize: 10
  maximumPoolSize: 10
  keepAliveTime: 5
  # 阻塞队列的最大长度
  capacity: 10
  waitTime: 500

// 消费者线程池代码
@Service
@Slf4j
public class KafkaConsumerThreadPool {
    @Value("${kafkaconsumer.executor.corePoolSize}")
    private Integer corePoolSize;
    @Value("${kafkaconsumer.executor.maximumPoolSize}")
    private Integer maximumPoolSize;
    @Value("${kafkaconsumer.executor.keepAliveTime}")
    private Long keepAliveTime;
    @Value("${kafkaconsumer.executor.capacity}")
    private Integer capacity;
    @Value("${kafkaconsumer.executor.waitTime}")
    private Long waitTime;
    private volatile ThreadPoolExecutor executor = null;

    public KafkaConsumerThreadPool() {
    }

    // 采取DCL方式进行线程池初始化
    public KafkaConsumerThreadPool getInstance() {
        if (this.executor == null) {
            Class var1 = KafkaConsumerThreadPool.class;
            synchronized(KafkaConsumerThreadPool.class) {
                if (this.executor == null) {
                    BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue(this.capacity);
                    RejectedExecutionHandler handler = new RejectedExecutionHandler() {
                        @Override
                        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                            log.error("task " + r.toString() + " reject from " + executor.toString());
                        }
                    };
                    this.executor = new ThreadPoolExecutor(this.corePoolSize, this.maximumPoolSize, this.keepAliveTime,
                            TimeUnit.SECONDS, workQueue, handler);
                }
            }
        }

        return this;
    }

    public void executor(Runnable runnable) {
        while(true) {
            try {
                log.info("consumer try to get thread");
                this.executor.execute(runnable);
                return;
            } catch (RejectedExecutionException var5) {
                log.info("consumer info get thread failed, wait for ");

                try {
                    Thread.sleep(500L);
                } catch (InterruptedException var4) {
                    var4.printStackTrace();
                }
            }
        }
    }
}
// 消费者服务代码

@Component
@Slf4j
public class KafkaCustomer {


    @Autowired
    private NameServiceImpl nameService;

    @Autowired
    private DemoConfig demoConfig;

    @Autowired
    private KafkaConsumerThreadPool kafkaConsumerThreadPool;

    /**
     * 处理kafka消息并分发
     *
     * @param msg
     */
    @KafkaListener(topics = "${kafkaTopic.demo.topic}")
    public void processDemoInfo(String msg) {
        try {
            log.info("demo msg  is [{}]", msg);
            Msg kafkaMsg = JsonUtils.fromJson(Msg.class, msg);
            log.info("kafkaMsg is [{}]", kafkaMsg);
            if (kafkaMsg.getEventName().equals(demoConfig.getNameRequestName())) {
                kafkaConsumerThreadPool.getInstance().executor(()->{
                    nameService.handle(kafkaMsg.getSequenceNum(), kafkaMsg.getObject());
                });
            }
        } catch (Exception e) {
            log.error("process info error, msg:[{}]", msg, e);
        }
    }

}

让我们来再次调用controller,可以看到LAG为0,且消息被迅速消费
消费者日志
在这里插入图片描述

6、Kafka 事务不生效,定位与解决方案

原因分析
最明显的问题表象是,生产者服务中开启了事务,但是消息一直没有投递到Kafka,可以从两方面来分析原因:

  • 注解位置不对,若方法A调用方法B,只有方法B上有@transaction,则B上的事务无法生效
  • 配置问题,springboot的启动类需要相关配置@EnableTransactionManagement,其次也是最隐蔽的问题,事务实现需要有相关数据源、jar的引用

解决方案
第一种问题的解决方案,对于注解位置问题,则从方法A开始,就开启事务(可以理解为从service层就开启注解服务)
第二种问题,需要配置数据源

spring:
  # 数据库
  datasource:
    url: jdbc:mysql://xxx.xxx.xxx.xxx:8888/idsaas_new_press?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=CTT
    username: root
    password: qatest
    driver-class-name: com.mysql.jdbc.Driver
    # 连接池
    type: com.zaxxer.hikari.HikariDataSource
    hikari:
      minimum-idle: 5
      maximum-pool-size: 15
      auto-commit: true
      idle-timeout: 30000
      pool-name: DatebookHikariCP
      max-lifetime: 540000
      connection-timeout: 30000
      connection-test-query: SELECT 1


// 注意启动类上加上@EnableAsync,具体方法上加上@Async
@SpringBootApplication
@ServletComponentScan
@EnableAsync
// 启动类开启事务
@EnableTransactionManagement
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

// service
@Service
public class SenderServiceImpl implements SenderService {

    @Autowired
    private DemoConfig demoConfig;

    public void requestName(NameRequest request) {
        Msg value = new Msg(demoConfig.getNameRequestId(), demoConfig.getNameRequestName(),
                UUID.randomUUID().toString(), request);
        KafkaProducer.sendMsgWithNoTrans(demoConfig.getTopic(),null, value);
    }
     // service层就开启事务
    @Transactional
    public void requestNameTrans(NameRequest request) {
        Msg value = new Msg(demoConfig.getNameRequestId(), demoConfig.getNameRequestName(),
                UUID.randomUUID().toString(), request);
        KafkaProducer.sendMsgAfterTrans(demoConfig.getTopic(), value);
    }
}

可以看到数据源开启成功,Kafka事务可以正常使用
数据源开启成功

7、经验总结

文章末尾,博主想谈谈对以下话题的看法:

  • 学习的目的
  • 谁是最好的老师

首先,学习是为了得到奖励。不仅是物质上的奖励,真正的奖励是你学到东西的愉悦感,你掌握了从无到有的过程,是对新事物的支配带来的愉悦感。

其次,谁是最好的老师?是你自身的自驱力怀疑力
第一,怀疑是智慧的开始,带着问题去阅读;
第二,为什么自驱力重要?这是Idea触发的重要因素
有人问比尔盖茨,你认为微软公司面临的最大竞争对手来自于哪里?是Google、亚马逊、FaceBook么?比尔盖茨回答都不是,他说,这些伟大的公司我都很敬畏,但我最大的竞争对手是那些,每天在自己车库里,不分白天黑夜捣腾新事务、实践新想法的年轻人。
Last but not least,推荐《如何阅读一本书》,相信你一定会受益匪浅。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值