Cassandra 自定义重试策略

39 阅读4分钟

为什么要自定义重试?

执行过程中会有如下异常

  • 读超时异常
  • 写入超时异常
  • 节点不可用异常
  • 请求中止异常
  • 响应错误异常

异常具体报错

org.springframework.data.cassandra.CassandraUncategorizedException: Query; CQL [com.datastax.oss.driver.internal.core.cql.DefaultSimpleStatement@e3ca556c]; null; nested exception is com.datastax.oss.driver.api.core.connection.HeartbeatException
Cassandra timeout during SIMPLE write query at consistency LOCAL_ONE (1 replica were required but only 0 acknowledged the write); nested exception is com.datastax.oss.driver.api.core.servererrors.WriteTimeoutException: Cassandra timeout during SIMPLE write query at consistency LOCAL_ONE (1 replica were required but only 0 acknowledged the write)

自定义重试策略

package cn.yizhoucp.octopus.config.configuration.custom.retry;

import com.datastax.oss.driver.api.core.ConsistencyLevel;
import com.datastax.oss.driver.api.core.connection.ClosedConnectionException;
import com.datastax.oss.driver.api.core.connection.HeartbeatException;
import com.datastax.oss.driver.api.core.context.DriverContext;
import com.datastax.oss.driver.api.core.retry.RetryDecision;
import com.datastax.oss.driver.api.core.retry.RetryPolicy;
import com.datastax.oss.driver.api.core.servererrors.CoordinatorException;
import com.datastax.oss.driver.api.core.servererrors.ReadFailureException;
import com.datastax.oss.driver.api.core.servererrors.WriteFailureException;
import com.datastax.oss.driver.api.core.servererrors.WriteType;
import com.datastax.oss.driver.api.core.session.Request;
import edu.umd.cs.findbugs.annotations.NonNull;
import lombok.extern.slf4j.Slf4j;

/**
 * 自定义重试策略
 *
 * @author tingfeng
 */
@Slf4j
public class DatastaxCustomRetry implements RetryPolicy {

    /**
     * 读重试次数
     */
    private final int readAttempts = 3;

    /**
     * 写重试次数
     */
    private final int writeAttempts = 3;

    /**
     * 不可用重试次数
     */
    private final int unavailableAttempts = 2;

    private final DriverContext driverContext;

    private final String cl;

    /**
     * 必须显示声明包含这两个参数的构造器
     *
     * @param driverContext 驱动上下文
     * @param cl            一致性级别
     */
    public DatastaxCustomRetry(DriverContext driverContext, String cl) {
        this.driverContext = driverContext;
        this.cl = cl;
        // 在这里进行其他初始化操作,如果需要的话
        log.info("DatastaxCustomRetry-init readAttempts : {} writeAttempts : {} unavailableAttempts : {} cl : {}"
                , readAttempts, writeAttempts, unavailableAttempts, cl);
    }

    /**
     * 读超时重试
     *
     * @param request     传递的请求对象,其中包含了触发读取超时的请求信息。
     * @param cl          一致性级别,表示读操作的一致性要求。
     * @param blockFor    所需的响应数量,即需要的副本数量
     * @param received    实际收到的响应数量
     * @param dataPresent 表示是否成功检索到数据
     * @param retryCount  当前的重试次数
     * @return
     */
    @Override
    public RetryDecision onReadTimeout(@NonNull Request request, @NonNull ConsistencyLevel cl
            , int blockFor, int received, boolean dataPresent, int retryCount) {

        /**
         * 如果当前的重试次数小于指定的最大重试次数(readAttempts),并且已经收到的响应数量大于或等于所需的响应数量(blockFor)
         * ,并且没有成功检索到数据(!dataPresent),则决定进行相同节点重试,否则选择不进行重试,将异常传播给调用方
         */
        RetryDecision decision = (retryCount < readAttempts && received >= blockFor && !dataPresent)
                ? RetryDecision.RETRY_SAME
                : RetryDecision.RETHROW;

        if (decision == RetryDecision.RETRY_SAME) {
            log.info("casRetry-读取超时在相同节点上重试 一致性: {},所需响应: {},收到的响应: {},是否检索到数据: {},重试次数: {}"
                    , cl, blockFor, received, false, retryCount);
        }

        return decision;
    }

    /**
     * 写入超时时的重试策略。
     *
     * @param request    请求对象,包含触发写入超时的请求信息。
     * @param cl         一致性级别,表示写入操作的一致性要求。
     * @param writeType  写入类型,表示发生写入超时的写入操作类型。
     * @param blockFor   所需的响应数量,即需要的副本数量。
     * @param received   实际收到的响应数量。
     * @param retryCount 当前的重试次数。
     * @return RetryDecision 决策,指示是否进行重试。
     */
    @Override
    public RetryDecision onWriteTimeout(@NonNull Request request, @NonNull ConsistencyLevel cl
            , @NonNull WriteType writeType, int blockFor, int received, int retryCount) {
        RetryDecision decision = (retryCount < writeAttempts)
                ? RetryDecision.RETRY_SAME
                : RetryDecision.RETHROW;

        if (decision == RetryDecision.RETRY_SAME) {
            log.info("casRetry-写超时在相同节点上重试 一致性: {},所需响应: {},收到的响应: {},是否检索到数据: {},重试次数: {}"
                    , cl, blockFor, received, false, retryCount);
        }
        return decision;
    }

    /**
     * 处理在节点不可用情况下的重试策略。
     *
     * @param request    请求对象,包含触发不可用情况的请求信息。
     * @param cl         一致性级别,表示操作的一致性要求。
     * @param required   所需的副本数量。
     * @param alive      实际可用的副本数量。
     * @param retryCount 当前的重试次数。
     * @return RetryDecision 决策,指示是否进行重试。
     */
    @Override
    public RetryDecision onUnavailable(@NonNull Request request, @NonNull ConsistencyLevel cl
            , int required, int alive, int retryCount) {
        RetryDecision decision =
                (retryCount < unavailableAttempts) ? RetryDecision.RETRY_NEXT : RetryDecision.RETHROW;

        if (decision == RetryDecision.RETRY_NEXT) {
            log.info("casRetry-节点不可用重试 一致性: {},所需的副本数量: {},实际可用的副本数量: {},重试次数: {}"
                    , cl, required, alive, retryCount);
        }

        return decision;
    }

    /**
     * 处理在请求中止时的重试策略。
     *
     * @param request    请求对象,包含触发请求中止的请求信息。
     * @param error      异常对象,表示引发请求中止的错误。
     * @param retryCount 当前的重试次数。
     * @return RetryDecision 决策,指示是否进行重试。
     */
    @Override
    public RetryDecision onRequestAborted(@NonNull Request request, @NonNull Throwable error, int retryCount) {
        RetryDecision decision =
                ((error instanceof ClosedConnectionException || error instanceof HeartbeatException) && (retryCount < unavailableAttempts))
                        ? RetryDecision.RETRY_NEXT
                        : RetryDecision.RETHROW;

        if (decision == RetryDecision.RETRY_NEXT) {
            log.info("casRetry-请求中止时的重试策略 重试次数: {}", retryCount);
        }

        return decision;
    }

    /**
     * 处理在错误响应时的重试策略。
     *
     * @param request    请求对象,包含触发错误响应的请求信息。
     * @param error      CoordinatorException,表示引发错误响应的异常。
     * @param retryCount 当前的重试次数。
     * @return RetryDecision 决策,指示是否进行重试。
     */
    @Override
    public RetryDecision onErrorResponse(@NonNull Request request, @NonNull CoordinatorException error, int retryCount) {
        RetryDecision decision =
                ((error instanceof ReadFailureException || error instanceof WriteFailureException) && (retryCount < unavailableAttempts))
                        ? RetryDecision.RETRY_NEXT
                        : RetryDecision.RETHROW;

        if (decision == RetryDecision.RETRY_NEXT) {
            log.info("casRetry-错误响应时的重试策略 重试次数: {}", retryCount);
        }

        return decision;
    }

    @Override
    public void close() {
        // Nothing to do
    }
}

驱动器加载自定义重试类

package cn.yizhoucp.octopus.config.configuration;

import cn.yizhoucp.octopus.config.configuration.custom.retry.DatastaxCustomRetry;
import com.datastax.oss.driver.api.core.CqlSessionBuilder;
import com.datastax.oss.driver.api.core.config.DefaultDriverOption;
import com.datastax.oss.driver.api.core.config.DriverConfigLoader;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.cassandra.config.AbstractCassandraConfiguration;
import org.springframework.data.cassandra.config.CqlSessionFactoryBean;
import org.springframework.data.cassandra.config.SessionBuilderConfigurer;

import java.time.Duration;

@Slf4j
@Configuration
public class CassandraConfig extends AbstractCassandraConfiguration {

    //空间名称
    @Value("${spring.data.cassandra.keyspace-name}")
    String keyspaceName;

    //节点IP(连接的集群节点IP)
    @Value("${spring.data.cassandra.contact-points}")
    String contactPoints;

    @Value("${spring.data.cassandra.username}")
    String username;

    @Value("${spring.data.cassandra.password}")
    String password;

    @Value("${spring.data.cassandra.session-name}")
    String sessionName;

    @Value("${spring.data.cassandra.pool-size}")
    Integer poolSize;

    @Override
    public String getKeyspaceName() {
        return keyspaceName;
    }

    @Override
    public String getContactPoints() {
        return contactPoints;
    }

    @Override
    public String getSessionName() {
        return sessionName;
    }

    @Override
    public String getLocalDataCenter() {
        return "datacenter1";
    }

    @Bean
    @Override
    public CqlSessionFactoryBean cassandraSession() {
        CqlSessionFactoryBean cqlSessionFactoryBean = super.cassandraSession();
        cqlSessionFactoryBean.setPassword(password);
        cqlSessionFactoryBean.setUsername(username);
        return cqlSessionFactoryBean;
    }

    /**
     * PT2S 异常优化 & 配置文件修改 request.timeout 不生效
     *
     * @return
     */
    @Override
    protected SessionBuilderConfigurer getSessionBuilderConfigurer() {
        log.info("getSessionBuilderConfigurer-start poolSize : {}", poolSize);
        return new SessionBuilderConfigurer() {
            @Override
            public CqlSessionBuilder configure(CqlSessionBuilder cqlSessionBuilder) {
                CqlSessionBuilder cqlSessionBuilder1 = cqlSessionBuilder
                        .withConfigLoader(DriverConfigLoader.programmaticBuilder()
                                .withDuration(DefaultDriverOption.REQUEST_TIMEOUT, Duration.ofMillis(6000))
                                .withDuration(DefaultDriverOption.HEARTBEAT_TIMEOUT, Duration.ofMillis(3000))
                                .withDuration(DefaultDriverOption.HEARTBEAT_INTERVAL, Duration.ofSeconds(10))
                                .withDuration(DefaultDriverOption.CONNECTION_INIT_QUERY_TIMEOUT, Duration.ofSeconds(3))
                                .withInt(DefaultDriverOption.CONNECTION_POOL_LOCAL_SIZE, poolSize)
                                .withInt(DefaultDriverOption.CONNECTION_POOL_REMOTE_SIZE, poolSize)
                                // 添加自定义重试策略
                                .withClass(DefaultDriverOption.RETRY_POLICY_CLASS, DatastaxCustomRetry.class)
                                .build());
                return cqlSessionBuilder1;
            }
        };
    }
}