ShardingProxy源码阅读(二)业务处理

829 阅读14分钟

前言

本章学习sharding-proxy业务处理流程:

  • PacketCodec:针对数据库协议的编解码实现。
  • FrontendChannelInboundHandler:针对Netty的ChannelInboundHandler实现。
  • ShardingProxy实际的业务执行。

一、PacketCodec编解码

PacketCodec编解码Handler,委托DatabasePacketCodecEngine编解码引擎实现具体的编解码逻辑。这里主要关注MySQL的解码实现类MySQLPacketCodecEngine

public final class PacketCodec extends ByteToMessageCodec<DatabasePacket> {
    
    private final DatabasePacketCodecEngine databasePacketCodecEngine;
    
    @Override
    protected void decode(final ChannelHandlerContext context, final ByteBuf in, final List<Object> out) {
        int readableBytes = in.readableBytes();
        if (!databasePacketCodecEngine.isValidHeader(readableBytes)) {
            return;
        }
        databasePacketCodecEngine.decode(context, in, out, readableBytes);
    }
    
    @Override
    protected void encode(final ChannelHandlerContext context, final DatabasePacket message, final ByteBuf out) {
        databasePacketCodecEngine.encode(context, message, out);
    }
}

1、解码

对于MySQL来说,通过自定义协议解决粘包拆包,报文的4个字节属于协议头。

解码之前,首先需要校验报文可读字节数大于4(协议头长度),否则不处理。

@Override
public boolean isValidHeader(final int readableBytes) {
    // 可读字节数 > 3(Payload长度) + 1(sequenceID)
    return readableBytes > MySQLPacket.PAYLOAD_LENGTH + MySQLPacket.SEQUENCE_LENGTH;
}

接着进入MySQLPacketCodecEngine的decode方法解码。

public final class MySQLPacketCodecEngine implements DatabasePacketCodecEngine<MySQLPacket> {
  @Override
  public void decode(final ChannelHandlerContext context, final ByteBuf in, final List<Object> out, final int readableBytes) {
      // markReaderIndex标记读index,readMediumLE读出3个字节
      int payloadLength = in.markReaderIndex().readMediumLE();
      // 计算实际报文总长度 = payload内容长度 + 协议头长度
      int realPacketLength = payloadLength + MySQLPacket.PAYLOAD_LENGTH + MySQLPacket.SEQUENCE_LENGTH;
      // 如果可读字节不足,表示遇到半包,重置读index返回
      if (readableBytes < realPacketLength) {
          in.resetReaderIndex();
          return;
      }
      // 读payload和sequenceId到一个新的ByteBuf
      ByteBuf byteBuf = in.readRetainedSlice(payloadLength + MySQLPacket.SEQUENCE_LENGTH);
      out.add(byteBuf);
  }
}

可以看到解码逻辑很简单,主要是处理粘包拆包问题,保证读入的ByteBuf和实际业务一致。

2、编码

PacketPayload

PacketPayload:ByteBuf的包装类,提供不同数据库的读写ByteBuf的方式,辅助业务数据写入ByteBuf。

public interface PacketPayload extends AutoCloseable {
    ByteBuf getByteBuf();
}

比如writeInt2写入2字节长度的int到ByteBuf中,MySQLPacketPayload的实现如下:

@RequiredArgsConstructor
@Getter
public final class MySQLPacketPayload implements PacketPayload {
    private final ByteBuf byteBuf;
    public void writeInt2(final int value) {
        byteBuf.writeShortLE(value);
    }
}

PostgreSQLPacketPayload的实现如下:

@RequiredArgsConstructor
@Getter
public final class PostgreSQLPacketPayload implements PacketPayload {
    private final ByteBuf byteBuf;
    public void writeInt2(final int value) {
    	byteBuf.writeShort(value);
	}
}

DatabasePacket

DatabasePacket:负责写入PacketPayload,一般本身持有业务相关数据。

public interface DatabasePacket<T extends PacketPayload> {
    void write(T payload);
}

MySQLPacket:负责写入MySQLPacketPayload,同时需要子类实现获取SequenceId方法。针对于不同类型的SQL语句会有不同的MySQLPacket实现。

public interface MySQLPacket extends DatabasePacket<MySQLPacketPayload> {
    // payload长度
    int PAYLOAD_LENGTH = 3;
    // sequenceId长度
    int SEQUENCE_LENGTH = 1;
    // 获取sequenceId
    int getSequenceId();
}

编码

回到MySQLPacketCodecEngine的encode方法,按照Payload长度->SequenceId->Payload的顺序写入ByteBuf。其中Payload数据是通过MySQLPacket写入MySQLPacketPayload持有的ByteBuf,然后再最终写入out的。

public void encode(final ChannelHandlerContext context, final MySQLPacket message, final ByteBuf out) {
    // 开辟一个MySQLPacketPayload(一个ByteBuf的包装)
    try (MySQLPacketPayload payload = new MySQLPacketPayload(context.alloc().buffer())) {
        // 业务数据写入MySQLPacketPayload
        message.write(payload);
        // 写入Payload长度
        out.writeMediumLE(payload.getByteBuf().readableBytes());
        // 写入SequenceId
        out.writeByte(message.getSequenceId());
        // 写入Payload
        out.writeBytes(payload.getByteBuf());
    }
}

二、FrontendChannelInboundHandler

FrontendChannelInboundHandler负责实际业务处理。

1、成员变量

FrontendChannelInboundHandler在构造ChannelPipeline时被创建。

@RequiredArgsConstructor
public final class FrontendChannelInboundHandler extends ChannelInboundHandlerAdapter {
    // 数据库协议引擎
    private final DatabaseProtocolFrontendEngine databaseProtocolFrontendEngine;
    // 是否授权(用户名密码校验通过)
    private volatile boolean authorized;
    // BackendConnection
    private final BackendConnection backendConnection = new BackendConnection(
           TransactionType.valueOf(ShardingProxyContext.getInstance().getProperties().getValue(ConfigurationPropertyKey.PROXY_TRANSACTION_TYPE)),
            ShardingProxyContext.getInstance().getProperties().<Boolean>getValue(ConfigurationPropertyKey.PROXY_HINT_ENABLED));
}

重点关注BackendConnection成员变量,可以类比sharding-jdbc的AbstractConnectionAdapter,主要负责获取连接。一个客户端连接对应一个FrontendChannelInboundHandler,所以一个客户端持有一个BackendConnection。

@Getter
public final class BackendConnection implements AutoCloseable {
    // 逻辑schema
    private volatile String schemaName;
    // 逻辑schema
    private LogicSchema logicSchema;
    // 全局事务类型
    private TransactionType transactionType;
    // 是否支持Hint
    private boolean supportHint;
    // 针对单个客户端连接(channel)的唯一标识
    @Setter
    private int connectionId;
    // 用户名
    @Setter
    private String userName;
    // 缓存连接
    private final Multimap<String, Connection> cachedConnections = LinkedHashMultimap.create();
    // 缓存Statement
    private final Collection<Statement> cachedStatements = new CopyOnWriteArrayList<>();
    // 缓存ResultSet
    private final Collection<ResultSet> cachedResultSets = new CopyOnWriteArrayList<>();
    // 方法调用记录 用于后期反射调用
    private final Collection<MethodInvocation> methodInvocations = new ArrayList<>();
    // 自定义的同步器
    @Getter
    private final ResourceSynchronizer resourceSynchronizer = new ResourceSynchronizer();
    // 连接状态管理
    private final ConnectionStateHandler stateHandler = new ConnectionStateHandler(resourceSynchronizer);
}

2、channelActive

当channel首次注册到selector上时触发channelActive。

public final class FrontendChannelInboundHandler extends ChannelInboundHandlerAdapter {
    private final DatabaseProtocolFrontendEngine databaseProtocolFrontendEngine;
    private final BackendConnection backendConnection
    public void channelActive(final ChannelHandlerContext context) {
        // ChannelThreadExecutorGroup创建与channel一一对应的线程处理业务
        ChannelThreadExecutorGroup.getInstance().register(context.channel().id());
        // MySQLAuthenticationEngine回复握手消息给客户端
        databaseProtocolFrontendEngine.getAuthEngine().handshake(context, backendConnection);
    }
}

ChannelThreadExecutorGroup全局单例,维护channel和业务线程的一一对应关系。

public final class ChannelThreadExecutorGroup {
    
    private static final ChannelThreadExecutorGroup INSTANCE = new ChannelThreadExecutorGroup();
    
    private final Map<ChannelId, ExecutorService> executorServices = new ConcurrentHashMap<>();
    
    public static ChannelThreadExecutorGroup getInstance() {
        return INSTANCE;
    }
    
    public void register(final ChannelId channelId) {
        executorServices.put(channelId, Executors.newSingleThreadExecutor());
    }
    
    public ExecutorService get(final ChannelId channelId) {
        return executorServices.get(channelId);
    }
    
    public void unregister(final ChannelId channelId) {
        executorServices.remove(channelId).shutdown();
    }
}

MySQLAuthenticationEngine通过写入MySQLHandshakePacket,回复握手消息。注意这里为BackendConnection创建了唯一id标识。

public final class MySQLAuthenticationEngine implements AuthenticationEngine {
    
    private final MySQLAuthenticationHandler authenticationHandler = new MySQLAuthenticationHandler();
    
    private MySQLConnectionPhase connectionPhase = MySQLConnectionPhase.INITIAL_HANDSHAKE;
    
    @Override
    public void handshake(final ChannelHandlerContext context, final BackendConnection backendConnection) {
        // 为BackendConnection分配id
        int connectionId = ConnectionIdGenerator.getInstance().nextId();
        backendConnection.setConnectionId(connectionId);
        // 修改连接阶段
        connectionPhase = MySQLConnectionPhase.AUTH_PHASE_FAST_PATH;
        // 写入握手消息(如果关注握手消息的写入协议,可以进入MySQLHandshakePacket的write方法)
        context.writeAndFlush(new MySQLHandshakePacket(connectionId, authenticationHandler.getAuthPluginData()));
    }
}

3、channelRead

 public void channelRead(final ChannelHandlerContext context, final Object message) {
    // 如果当前channel还没授权,执行授权(校验用户名密码)
    if (!authorized) {
        authorized = auth(context, (ByteBuf) message);
        return;
    }
    CommandExecutorSelector
            // CommandExecutorSelector根据条件,选择合适的线程池执行业务
            .getExecutor(
                    databaseProtocolFrontendEngine.getFrontendContext().isOccupyThreadForPerConnection()
                    , backendConnection.isSupportHint()
                    , backendConnection.getTransactionType()
                    , context.channel().id()
            )
            // CommandExecutorTask执行业务
            .execute(new CommandExecutorTask(databaseProtocolFrontendEngine, backendConnection, context, message));
}

当首次收到数据时,会先进行授权校验。一方面需要校验用户名密码,另一方面还需要校验schema权限,这都是由MySQLAuthenticationEngine处理的。

private boolean auth(final ChannelHandlerContext context, final ByteBuf message) {
    // 创建PacketPayload
    try (PacketPayload payload = databaseProtocolFrontendEngine.getCodecEngine().createPacketPayload(message)) {
        // MySQLAuthenticationEngine做用户名密码及schema权限校验
        return databaseProtocolFrontendEngine.getAuthEngine().auth(context, payload, backendConnection);
    } catch (final Exception ex) {
        context.write(databaseProtocolFrontendEngine.getCommandExecuteEngine().getErrorPacket(ex));
    }
    return false;
}

授权通过后,CommandExecutorSelector会根据当前的情况选择业务线程池提交任务CommandExecutorTask

public final class CommandExecutorSelector {
    
    public static ExecutorService getExecutor(final boolean isOccupyThreadForPerConnection, final boolean supportHint, final TransactionType transactionType, final ChannelId channelId) {
        return (isOccupyThreadForPerConnection || supportHint || TransactionType.XA == transactionType || TransactionType.BASE == transactionType)
                ? ChannelThreadExecutorGroup.getInstance().get(channelId) : UserExecutorGroup.getInstance().getExecutorService();
    }
}

满足下面任一条件,使用ChannelThreadExecutorGroup中channel对应的线程池处理

  • isOccupyThreadForPerConnection:是否连接独占线程,由数据库类型决定,MySQL为false,PostgreSQL为true。
  • supportHint:是否支持Hint。由proxy.hint.enabled决定,默认false。
  • transactionType:事务类型,如果是XA或BASE事务(非本地事务),则用ChannelThreadExecutorGroup中的线程。

默认情况下MySQL都是用UserExecutorGroup中维护的公共线程池执行CommandExecutorTask业务任务。核心线程数=acceptor.size,默认为2倍核数。ShardingSphereExecutorService底层是通过Executors.newFixedThreadPool(executorSize)创建的线程池,等待队列是LinkedBlockingQueue,可认为无界。

public final class UserExecutorGroup implements AutoCloseable {
    
    private static final ShardingProxyContext SHARDING_PROXY_CONTEXT = ShardingProxyContext.getInstance();
    
    private static final String NAME_FORMAT = "Command-%d";
    
    private static final UserExecutorGroup INSTANCE = new UserExecutorGroup();
    
    private final ShardingSphereExecutorService shardingSphereExecutorService;
    
    @Getter
    private final ListeningExecutorService executorService;
    
    private UserExecutorGroup() {
        // 核心线程数 = acceptor.size 默认为2*核数
        shardingSphereExecutorService = new ShardingSphereExecutorService(SHARDING_PROXY_CONTEXT.getProperties().<Integer>getValue(ConfigurationPropertyKey.ACCEPTOR_SIZE), NAME_FORMAT);
        executorService = shardingSphereExecutorService.getExecutorService();
    }
    
    public static UserExecutorGroup getInstance() {
        return INSTANCE;
    }
    
    @Override
    public void close() {
        shardingSphereExecutorService.close();
    }
}

4、channelInactive

客户端关闭时,释放所有资源。

@Override
public void channelInactive(final ChannelHandlerContext context) {
    context.fireChannelInactive();
    // MySQL空实现
    databaseProtocolFrontendEngine.release(backendConnection);
    // 关闭backendConnection持有的资源,如Connection、ResultSet、Statement
    backendConnection.close(true);
    // 关闭ChannelThreadExecutorGroup中channel对应的线程池
    ChannelThreadExecutorGroup.getInstance().unregister(context.channel().id());
}

5、channelWritabilityChanged

当channel无法写入数据时,会调用ResourceSynchronizer的doAwait方法阻塞等待直至被唤醒。当channel可写时,此处会唤醒所有阻塞线程。

@Override
public void channelWritabilityChanged(final ChannelHandlerContext context) {
    if (context.channel().isWritable()) {
        backendConnection.getResourceSynchronizer().doNotify();
    }
}

ResourceSynchronizer:轻量级的自定义同步器,支持无限等待,超时等待。使用ReentrantLock和Condition实现。

public final class ResourceSynchronizer {
    
    private static final long DEFAULT_TIMEOUT_MILLISECONDS = 200;
    
    private final Lock lock = new ReentrantLock();
    
    private final Condition condition = lock.newCondition();
    
    public void doAwait() {
        lock.lock();
        try {
            condition.await();
        } catch (final InterruptedException ignore) {
        } finally {
            lock.unlock();
        }
    }
    
    void doAwaitUntil() throws InterruptedException {
        lock.lock();
        try {
            condition.await(DEFAULT_TIMEOUT_MILLISECONDS, TimeUnit.MILLISECONDS);
        } finally {
            lock.unlock();
        }
    }
    
    public void doNotify() {
        lock.lock();
        try {
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }
}

三、执行

1、连接状态

BackendConnection的连接状态枚举如下:

public enum ConnectionStatus {
    INIT, RUNNING, RELEASE, TRANSACTION, TERMINATED
}
  • INIT:BackendConnection刚创建时(客户端channel注册到selector上时,ChannelPipeline加入FrontendChannelInboundHandler)的状态。
  • RUNNING:CommandExecutorTask业务处理任务执行时。
  • RELEASE:CommandExecutorTask业务处理任务完成时。
  • TRANSACTION:开启全局事务时。
  • TERMINATED:全局事务提交或回滚时。

整个状态流转如下:

无事务的情况下,除了首次创建时的INIT,只有业务任务运行时(RUNNING)和结束时(RELEASE)的状态:

INIT -> RUNNING -> RELEASE -> RUNNING -> RELEASE -> ...

存在事务的情况下,多了TRANSACTION和TERMINATED状态,前者表示处于全局事务中,后者表示事务已经提交或回滚:

INIT -> RUNNING (Task开始)-> TRANSACTION (开启事务)-> ... 执行n次业务Task -> TERMINATED(提交或回滚事务)-> RELEASE -> ...

2、连接状态管理

ConnectionStateHandler作为BackendConnection的成员变量,负责管理BackendConnection的状态流转。

@RequiredArgsConstructor
public final class ConnectionStateHandler {
    // 当前状态
    private final AtomicReference<ConnectionStatus> status = new AtomicReference<>(ConnectionStatus.INIT);
    // 同步器
    private final ResourceSynchronizer resourceSynchronizer;
}

设置RUNNING

仅当执行中(RUNNING)或事务结束(TERMINATED)时,循环等待当前BackendConnection变为RELEASE状态,并更新为RUNNING执行中状态。

public void waitUntilConnectionReleasedIfNecessary() throws InterruptedException {
    if (ConnectionStatus.RUNNING == status.get() || ConnectionStatus.TERMINATED == status.get()) {
        while (!status.compareAndSet(ConnectionStatus.RELEASE, ConnectionStatus.RUNNING)) {
            resourceSynchronizer.doAwaitUntil();
        }
    }
}

仅当状态为INIT或RELEASE或TERMINATED时,才能设置为RUNNING状态。(业务上只有INIT时才会设置为RUNNING)

public void setRunningStatusIfNecessary() {
    if (ConnectionStatus.TRANSACTION != status.get() && ConnectionStatus.RUNNING != status.get()) {
        status.getAndSet(ConnectionStatus.RUNNING);
    }
}

设置RELEASE

仅当普通Task(非事务)执行结束后(RUNNING),或事务提交后(TERMINATED),设置状态为RELEASE,并通知所有等待线程。代表BackendConnection不再被使用,可以用于执行下一个Task。

void doNotifyIfNecessary() {
      if (status.compareAndSet(ConnectionStatus.RUNNING, ConnectionStatus.RELEASE) || status.compareAndSet(ConnectionStatus.TERMINATED, ConnectionStatus.RELEASE)) {
          resourceSynchronizer.doNotify();
      }
  }

事务相关

业务上只有事务相关状态,会调用这个方法。事务开启,设置为TRANSACTION,事务结束,设置为TERMINATED,如果当前状态为TERMINATED会通知所有等待线程。

public void setStatus(final ConnectionStatus update) {
    status.getAndSet(update);
    if (ConnectionStatus.TERMINATED == status.get()) {
        resourceSynchronizer.doNotify();
    }
}

3、CommandExecutorTask

CommandExecutorTask实际的业务处理任务。

public final class CommandExecutorTask implements Runnable {
    // 数据库协议引擎
    private final DatabaseProtocolFrontendEngine databaseProtocolFrontendEngine;
    // channel对应BackendConnection
    private final BackendConnection backendConnection;
    // 上下文
    private final ChannelHandlerContext context;
    // PacketCodec解码之后的ByteBuf实例
    private final Object message;
    
    @Override
    public void run() {
        boolean isNeedFlush = false;
        // BackendConnection实现AutoCloseable,结束后如果是RUNNING或TERMINATED会设置为RELEASE状态
        try (BackendConnection backendConnection = this.backendConnection;
             PacketPayload payload = databaseProtocolFrontendEngine.getCodecEngine().createPacketPayload((ByteBuf) message)) {
            // 如果有任务在执行,等待BackendConnection变为RELEASE状态,更新为RUNNING状态
            backendConnection.getStateHandler().waitUntilConnectionReleasedIfNecessary();
            // 如果当前任务是INIT状态,设置为RUNNING状态
            backendConnection.getStateHandler().setRunningStatusIfNecessary();
            // 执行
            isNeedFlush = executeCommand(context, payload, backendConnection);
        } catch (final Exception ex) {
           // ...
        } finally {
            if (isNeedFlush) {
                context.flush();
            }
        }
    }
}

可以看到run方法主要是控制BackendConnection的状态(RELEASE->RUNNING->RELEASE),必须从RELEASE(或INIT)变为RUNNING之后,才能执行业务方法,保证BackendConnection同时只能被一个Task执行。

接下来看一下CommandExecutorTask的executeCommand方法。

private boolean executeCommand(final ChannelHandlerContext context, final PacketPayload payload, final BackendConnection backendConnection) throws SQLException {
    // MySQLCommandExecuteEngine
    CommandExecuteEngine commandExecuteEngine = databaseProtocolFrontendEngine.getCommandExecuteEngine();
    // 根据payload获取命令类型
    CommandPacketType type = commandExecuteEngine.getCommandPacketType(payload);
    // 工厂 根据命令类型构造CommandPacket - MySQLPacket的实现类
    CommandPacket commandPacket = commandExecuteEngine.getCommandPacket(payload, type, backendConnection);
    // 工厂 根据命令类型构造CommandExecutor
    CommandExecutor commandExecutor = commandExecuteEngine.getCommandExecutor(type, commandPacket, backendConnection);
    // 执行(业务逻辑)
    Collection<DatabasePacket> responsePackets = commandExecutor.execute();
    if (responsePackets.isEmpty()) {
        return false;
    }
    // 提交到channel对应EventLoop写入ChannelOutboundBuffer
    // 并没有flush,没写给客户端,是否flush通过协议的isFlushForPerCommandPacket返回给外部
    for (DatabasePacket each : responsePackets) {
        context.write(each);
    }
    // 如果是查询,需要将查询结果写回客户端,执行CommandExecuteEngine的writeQueryData方法
    if (commandExecutor instanceof QueryCommandExecutor) {
        commandExecuteEngine.writeQueryData(context, backendConnection, (QueryCommandExecutor) commandExecutor, responsePackets.size());
        return true;
    }
    // 返回这个数据库协议 最终是否需要flush
    return databaseProtocolFrontendEngine.getFrontendContext().isFlushForPerCommandPacket();
}

整个sharding业务都是在CommandExecutor中执行的,而CommandExecutor是由CommandExecuteEngine构造的。

CommandExecuteEngine创建CommandExecutor

commandExecuteEngine.getCommandPacketType首先根据payload解析出命令类型CommandPacketType,定位到MySQLCommandPacketTypeLoader

public final class MySQLCommandPacketTypeLoader {
    public static MySQLCommandPacketType getCommandPacketType(final MySQLPacketPayload payload) {
         // 第一个字节是SequenceID,必须是0
        Preconditions.checkArgument(0 == payload.readInt1(), "Sequence ID of MySQL command packet must be `0`.");
        // 根据第二个字节决定命令类型
        return MySQLCommandPacketType.valueOf(payload.readInt1());
    }
}

MySQLPacketPayload里封装了一个ByteBuf,在第一部分PacketCodec编解码时看到这个ByteBuf里装的就是SequenceID和Payload。此处getCommandPacketType先读取一个字节,要求SequenceID必须是0,接着读取Payload中第一个字节决定了MySQL的命令类型。MySQLCommandPacketType是一个枚举。

public enum MySQLCommandPacketType implements CommandPacketType {
    COM_SLEEP(0x00),
    COM_QUIT(0x01),
    COM_INIT_DB(0x02),
    COM_QUERY(0x03),
    // ... 其他命令类型
    ;
}

commandExecuteEngine.getCommandPacket创建MySQLCommandPacket,定位到MySQLCommandPacketFactory。可以看到这里就是一个简单工厂,根据命令类型,构造不同的MySQLCommandPacket返回。

public final class MySQLCommandPacketFactory {
    public static MySQLCommandPacket newInstance(final MySQLCommandPacketType commandPacketType, final MySQLPacketPayload payload) throws SQLException {
        switch (commandPacketType) {
            case COM_QUIT:
                return new MySQLComQuitPacket();
            case COM_INIT_DB:
                return new MySQLComInitDbPacket(payload);
            case COM_FIELD_LIST:
                return new MySQLComFieldListPacket(payload);
            case COM_QUERY:
                return new MySQLComQueryPacket(payload);
            case COM_STMT_PREPARE:
                return new MySQLComStmtPreparePacket(payload);
            case COM_STMT_EXECUTE:
                return new MySQLComStmtExecutePacket(payload);
            case COM_STMT_RESET:
                return new MySQLComStmtResetPacket(payload);
            case COM_STMT_CLOSE:
                return new MySQLComStmtClosePacket(payload);
            case COM_PING:
                return new MySQLComPingPacket();
            default:
                return new MySQLUnsupportedCommandPacket(commandPacketType);
        }
    }
}

commandExecuteEngine.getCommandExecutor最终创建了CommandExecutor,定位到MySQLCommandExecutorFactory。仍然是个简单工厂,根据命令类型,构造不同的CommandExecutor。

public final class MySQLCommandExecutorFactory {
    
    public static CommandExecutor newInstance(final MySQLCommandPacketType commandPacketType, final CommandPacket commandPacket, final BackendConnection backendConnection) {
        switch (commandPacketType) {
            case COM_QUERY:
                return new MySQLComQueryPacketExecutor((MySQLComQueryPacket) commandPacket, backendConnection);
            case COM_STMT_PREPARE:
                return new MySQLComStmtPrepareExecutor((MySQLComStmtPreparePacket) commandPacket, backendConnection);
            case COM_STMT_EXECUTE:
                return new MySQLComStmtExecuteExecutor((MySQLComStmtExecutePacket) commandPacket, backendConnection);
            // ...
            default:
                return new MySQLUnsupportedCommandExecutor(commandPacketType);
        }
    }
}

CommandExecutor执行

这里重点关注查询语句的执行,对于Statement执行,属于COM_QUERY命令,由MySQLComQueryPacketExecutor处理;对于PrepareStatement执行,属于COM_STMT_EXECUTE命令,由MySQLComStmtExecuteExecutor处理。这里重点看后者。

CommandExecutor命令执行能力。

public interface CommandExecutor {
    Collection<DatabasePacket> execute() throws SQLException;
}

QueryCommandExecutor查询能力。

public interface QueryCommandExecutor extends CommandExecutor {
    // 是否时错误响应
    boolean isErrorResponse();
    // 是否是查询
    boolean isQuery();
    // 移动结果集游标
    boolean next() throws SQLException;
    // 获取结果集当前行
    DatabasePacket getQueryData() throws SQLException;
}

MySQLComStmtExecuteExecutor实现COM_STMT_EXECUTE命令执行和查询能力。不过基本是委托DatabaseCommunicationEngine数据库交互引擎实现的。

public final class MySQLComStmtExecuteExecutor implements QueryCommandExecutor {
    // 数据库交互引擎
    private final DatabaseCommunicationEngine databaseCommunicationEngine;
    // 是否是查询(实现isQuery方法)
    private volatile boolean isQuery;
    // 是否是错误响应(实现isErrorResponse方法)
    private volatile boolean isErrorResponse;
    // 当前SequenceID
    private int currentSequenceId;
    // 构造方法创建DatabaseCommunicationEngine
    public MySQLComStmtExecuteExecutor(final MySQLComStmtExecutePacket comStmtExecutePacket, final BackendConnection backendConnection) {
        databaseCommunicationEngine = DatabaseCommunicationEngineFactory.getInstance().newBinaryProtocolInstance(
                backendConnection.getLogicSchema(), comStmtExecutePacket.getSql(), comStmtExecutePacket.getParameters(), backendConnection);
    }
    
    @Override
    public Collection<DatabasePacket> execute() {
        // 断路器是否打开(忽略)
        if (ShardingProxyContext.getInstance().isCircuitBreak()) {
            return Collections.singletonList(new MySQLErrPacket(1, CommonErrorCode.CIRCUIT_BREAK_MODE));
        }
        // 执行
        BackendResponse backendResponse = databaseCommunicationEngine.execute();
        // 是错误响应
        if (backendResponse instanceof ErrorResponse) {
            isErrorResponse = true;
            return Collections.singletonList(createErrorPacket(((ErrorResponse) backendResponse).getCause()));
        }
        // 是更新响应
        if (backendResponse instanceof UpdateResponse) {
            return Collections.singletonList(createUpdatePacket((UpdateResponse) backendResponse));
        }
        // 是查询响应
        isQuery = true;
        return createQueryPacket((QueryResponse) backendResponse);
    }
	
    @Override
    public boolean next() throws SQLException {
        return databaseCommunicationEngine.next();
    }
    
    @Override
    public MySQLPacket getQueryData() throws SQLException {
        QueryData queryData = databaseCommunicationEngine.getQueryData();
        return new MySQLBinaryResultSetRowPacket(++currentSequenceId, queryData.getData(), getMySQLColumnTypes(queryData));
    }
}

先看一下createQueryPacket((QueryResponse) backendResponse)这个方法,如何返回查询报文。首先看一下QueryResponse的结构,这也是DatabaseCommunicationEngine返回的结果。

QueryResponse封装了查询头QueryHeader和查询结果QueryResult。其中QueryResult和sharding-jdbc是一样的,分为流式归并StreamQueryResult和内存归并MemoryQueryResult。

@RequiredArgsConstructor
@Getter
public final class QueryResponse implements BackendResponse {
    // 查询头
    private final List<QueryHeader> queryHeaders;
    // 查询结果(分为内存归并MemoryQueryResult和流式归并StreamQueryResult)
    private final List<QueryResult> queryResults = new LinkedList<>();
}

QueryHeader封装了字段的元数据信息,部分成员变量如下。

public final class QueryHeader {
    private final String schema;
    private final String table;
    private final String columnLabel;
    private final String columnName;
    private final int columnLength;
    private final Integer columnType;
    // ... 省略其他
}

MySQLComStmtExecuteExecutor构建QueryPacket,先写一个packet表示有几个字段,再写所有字段定义packet,最后写一个eof结束packet,发现这里并没有写实际的QueryResult。写QueryResult是在执行CommandExecutor的execute方法之后,在CommandExecutorTask#executeCommand里做的,数据来源也是委托DatabaseCommunicationEngine

private Collection<DatabasePacket> createQueryPacket(final QueryResponse backendResponse) {
    Collection<DatabasePacket> result = new LinkedList<>();
    // QueryHeader 字段定义列表
    List<QueryHeader> queryHeader = backendResponse.getQueryHeaders();
    // 第一个Packet表示查询结果有几个字段
    result.add(new MySQLFieldCountPacket(++currentSequenceId, queryHeader.size()));
    // 接下来几个Packet给出字段定义
    for (QueryHeader each : queryHeader) {
        result.add(new MySQLColumnDefinition41Packet(++currentSequenceId, each.getSchema(), each.getTable(), each.getTable(),
                each.getColumnLabel(), each.getColumnName(), each.getColumnLength(), MySQLColumnType.valueOfJDBCType(each.getColumnType()), each.getDecimals()));
    }
    // 结束Packet
    result.add(new MySQLEofPacket(++currentSequenceId));
    return result;
}

4、DatabaseCommunicationEngine

DatabaseCommunicationEngine:实际实现解析、路由、重写、执行、合并流程的引擎。

public interface DatabaseCommunicationEngine {
    // 执行
    BackendResponse execute();
    // 移动结果集游标
    boolean next() throws SQLException;
    // 获取结果集当前行
    QueryData getQueryData() throws SQLException;
}

JDBCDatabaseCommunicationEngine:JDBC是唯一的实现引擎。

public final class JDBCDatabaseCommunicationEngine implements DatabaseCommunicationEngine {
    // LogicSchema
    private final LogicSchema logicSchema;
    // 实际SQL
    private final String sql;
    // JDBC执行引擎
    private final JDBCExecuteEngine executeEngine;
    // 响应
    private BackendResponse response;
    // 查询归并结果
    private MergedResult mergedResult;
}

看一下execute方法,这里我把所有的私有方法都合并到这一个方法里了,并且省略了部分代码。可以看到sharding-jdbc的MergeEngine负责将QueryResult转换为MergedResult。其余的四个关键步骤委托JDBCExecuteEngine执行。

public BackendResponse execute() {
    // 解析 路由 重写
    ExecutionContext executionContext = executeEngine.getJdbcExecutorWrapper().route(sql);
    SQLStatementContext sqlStatementContext = executionContext.getSqlStatementContext();
    // 执行
    response = executeEngine.execute(executionContext);
    // 合并
    MergeEngine mergeEngine = new MergeEngine(logicSchema.getShardingRule().toRules(), ShardingProxyContext.getInstance().getProperties(), LogicSchemas.getInstance().getDatabaseType(), logicSchema.getMetaData().getSchema());
    mergedResult = mergeEngine.merge(((QueryResponse) response).getQueryResults(), sqlStatementContext);
    return response;
}

5、JDBCExecuteEngine

public final class JDBCExecuteEngine implements SQLExecuteEngine {
    private final BackendConnection backendConnection;
    private final JDBCExecutorWrapper jdbcExecutorWrapper;
    private final SQLExecutePrepareTemplate sqlExecutePrepareTemplate;
    private final SQLExecuteTemplate sqlExecuteTemplate;
}

JDBCExecuteEngine组合各种执行SQL必要的组件,适配sharding-jdbc。其中SQLExecutePrepareTemplate和SQLExecuteTemplate都是属于原来ShardingJDBC执行SQL的组件,前者负责分组、执行创建连接callback、执行创建语句callback,后者负责执行SQL

解析&路由&重写

首先解析&路由&重写都是由JDBCExecutorWrapper的route方法实现的。这里判断配置类型,执行不同的逻辑,重点看分片配置的执行逻辑。

public final class PreparedStatementExecutorWrapper implements JDBCExecutorWrapper {
    // 持有kv配置 授权信息
    private static final ShardingProxyContext SHARDING_PROXY_CONTEXT = ShardingProxyContext.getInstance();
    // 类比ShardingRuntimeContext 啥都有 第一章讲过
    private final LogicSchema logicSchema;
    // 参数列表
    private final List<Object> parameters;
    
    @Override
    public ExecutionContext route(final String sql) {
        if (logicSchema instanceof ShardingSchema) {
            return doShardingRoute(sql);
        }
        if (logicSchema instanceof MasterSlaveSchema) {
            return doMasterSlaveRoute(sql);
        }
        if (logicSchema instanceof EncryptSchema) {
            return doEncryptRoute(sql);
        }
        if (logicSchema instanceof ShadowSchema) {
            return doShadowRoute(sql);
        }
        return doTransparentRoute(sql);
    }
}

JDBCExecutorWrapper的doShardingRoute方法,委托sharding-jdbc的PreparedQueryPrepareEngine实现解析&路由&重写。

private ExecutionContext doShardingRoute(final String sql) {
    PreparedQueryPrepareEngine prepareEngine = new PreparedQueryPrepareEngine(
    logicSchema.getShardingRule().toRules(), 
    ShardingProxyContext.getInstance().getProperties(), 
    logicSchema.getMetaData(), 
    logicSchema.getSqlParserEngine());
    return prepareEngine.prepare(sql, parameters);
}

执行

重写完成以后进入JDBCExecuteEngine的execute方法,做SQL执行。

public BackendResponse execute(final ExecutionContext executionContext) throws SQLException {
    SQLStatementContext sqlStatementContext = executionContext.getSqlStatementContext();
    boolean isReturnGeneratedKeys = sqlStatementContext.getSqlStatement() instanceof InsertStatement;
    boolean isExceptionThrown = ExecutorExceptionHandler.isExceptionThrown();
    // 分组回调 backendConnection获取连接 jdbcExecutorWrapper创建Statement
    ProxyJDBCExecutePrepareCallback callback01 = new ProxyJDBCExecutePrepareCallback(backendConnection, jdbcExecutorWrapper, isReturnGeneratedKeys);
    // 分组
    Collection<InputGroup<StatementExecuteUnit>> inputGroups = sqlExecutePrepareTemplate.getExecuteUnitGroups(executionContext.getExecutionUnits(), callback01);
    // 首次执行回调 需要返回字段metadata用于组装QueryHeader jdbcExecutorWrapper负责执行
    ProxySQLExecuteCallback callback02 = new ProxySQLExecuteCallback(sqlStatementContext, backendConnection, jdbcExecutorWrapper, isExceptionThrown, isReturnGeneratedKeys, true);
    // 后续执行回调 不需要返回字段metadata jdbcExecutorWrapper负责执行
    ProxySQLExecuteCallback callback03 = new ProxySQLExecuteCallback(sqlStatementContext, backendConnection, jdbcExecutorWrapper, isExceptionThrown, isReturnGeneratedKeys, false);
    // 执行
    Collection<ExecuteResponse> executeResponses = sqlExecuteTemplate.execute((Collection) inputGroups, callback02, callback03);
    // 组装返回
    ExecuteResponse executeResponse = executeResponses.iterator().next();
    return executeResponse instanceof ExecuteQueryResponse
            ? getExecuteQueryResponse(((ExecuteQueryResponse) executeResponse).getQueryHeaders(), executeResponses)
            : new UpdateResponse(executeResponses);
}

分组、创建连接、创建语句

JDBCExecuteEngine委托SQLExecutePrepareTemplate(sharding-jdbc),实现分组、创建连接、创建语句。重点关注传入template的callback方法(callback01),callback以外的逻辑与sharding-jdbc是一套逻辑(如何分组、创建多少连接、ConnectionMode如何决定等)。

ProxyJDBCExecutePrepareCallback

public final class ProxyJDBCExecutePrepareCallback implements SQLExecutePrepareCallback {
    private final BackendConnection backendConnection;
    private final JDBCExecutorWrapper jdbcExecutorWrapper;
    private final boolean isReturnGeneratedKeys;
    
    @Override
    public List<Connection> getConnections(final ConnectionMode connectionMode, final String dataSourceName, final int connectionSize) throws SQLException {
        return backendConnection.getConnections(connectionMode, dataSourceName, connectionSize);
    }
    
    @Override
    public StatementExecuteUnit createStatementExecuteUnit(final Connection connection, final ExecutionUnit executionUnit, final ConnectionMode connectionMode) throws SQLException {
        // connection创建PrepareStatement,填充params
        Statement statement = jdbcExecutorWrapper.createStatement(connection, executionUnit.getSqlUnit(), isReturnGeneratedKeys);
        // 如果内存限制模式,设置fetchSize防止OOM
        if (connectionMode.equals(ConnectionMode.MEMORY_STRICTLY)) {
            if (LogicSchemas.getInstance().getDatabaseType() instanceof MySQLDatabaseType) {
                statement.setFetchSize(Integer.MIN_VALUE);
            } else if (LogicSchemas.getInstance().getDatabaseType() instanceof PostgreSQLDatabaseType) {
                statement.setFetchSize(1);
            }
        }
        return new StatementExecuteUnit(executionUnit, statement, connectionMode);
    }
}

ProxyJDBCExecutePrepareCallback操作BackendConnection获取数据库连接,分为事务中和非事务中两种处理方案。

// BackendConnection
private final Multimap<String, Connection> cachedConnections = LinkedHashMultimap.create();
public List<Connection> getConnections(final ConnectionMode connectionMode, final String dataSourceName, final int connectionSize) throws SQLException {
    if (stateHandler.isInTransaction()) {
        // BackendConnection事务中,使用缓存连接
        return getConnectionsWithTransaction(connectionMode, dataSourceName, connectionSize);
    } else {
        // BackendConnection非事务,获取新连接
        return getConnectionsWithoutTransaction(connectionMode, dataSourceName, connectionSize);
    }
}

// 事务中获取连接
private List<Connection> getConnectionsWithTransaction(final ConnectionMode connectionMode, final String dataSourceName, final int connectionSize) throws SQLException {
    Collection<Connection> connections;
    synchronized (cachedConnections) {
        connections = cachedConnections.get(dataSourceName);
    }
    List<Connection> result;
    if (connections.size() >= connectionSize) {
        result = new ArrayList<>(connections).subList(0, connectionSize);
    } else if (!connections.isEmpty()) {
        result = new ArrayList<>(connectionSize);
        result.addAll(connections);
        List<Connection> newConnections = createNewConnections(connectionMode, dataSourceName, connectionSize - connections.size());
        result.addAll(newConnections);
        synchronized (cachedConnections) {
            cachedConnections.putAll(dataSourceName, newConnections);
        }
    } else {
        result = createNewConnections(connectionMode, dataSourceName, connectionSize);
        synchronized (cachedConnections) {
            cachedConnections.putAll(dataSourceName, result);
        }
    }
    return result;
}
// 非事务中获取连接
private List<Connection> getConnectionsWithoutTransaction(final ConnectionMode connectionMode, final String dataSourceName, final int connectionSize) throws SQLException {
    List<Connection> result = getConnectionFromUnderlying(connectionMode, dataSourceName, connectionSize);
    synchronized (cachedConnections) {
        cachedConnections.putAll(dataSourceName, result);
    }
    return result;
}

private List<Connection> createNewConnections(final ConnectionMode connectionMode, final String dataSourceName, final int connectionSize) throws SQLException {
    Preconditions.checkNotNull(logicSchema, "current logic schema is null");
    List<Connection> result = getConnectionFromUnderlying(connectionMode, dataSourceName, connectionSize);
    for (Connection each : result) {
        replayMethodsInvocation(each);
    }
    return result;
}

private List<Connection> getConnectionFromUnderlying(final ConnectionMode connectionMode, final String dataSourceName, final int connectionSize) throws SQLException {
    return logicSchema.getBackendDataSource().getConnections(connectionMode, dataSourceName, connectionSize, transactionType);
}

无论是事务中,还是非事务中,获取新连接都是通过JDBCBackendDataSource的getConnections方法获取。和AbstractConnectionAdapter#createConnections基本类似,根据连接模式,决定是否需要加锁。参照ShardingJDBC源码阅读(七)执行

# JDBCBackendDataSource
public List<Connection> getConnections(final ConnectionMode connectionMode, final String dataSourceName, final int connectionSize, final TransactionType transactionType) throws SQLException {
    DataSource dataSource = dataSources.get(dataSourceName);
    if (1 == connectionSize) {
        return Collections.singletonList(createConnection(transactionType, dataSourceName, dataSource));
    }
    // 连接限制模式,因为是一次性读取数据到内存,很快会释放资源,不需要加锁
    if (ConnectionMode.CONNECTION_STRICTLY == connectionMode) {
        return createConnections(transactionType, dataSourceName, dataSource, connectionSize);
    }
    // 内存限制模式 需要加锁
    synchronized (dataSource) {
        return createConnections(transactionType, dataSourceName, dataSource, connectionSize);
    }
}

为什么BackendConnection获取连接要分为两种方案?

定位到Github #1516 optimize backendConnection for OLAP,在这个版本中,提供了本地事务的支持,这里是对于查询的性能优化。因为使用缓存连接需要加synchronized锁,直接获取新的连接不需要加锁。

连接创建完成后,ProxyJDBCExecutePrepareCallbackProxy执行createStatementExecuteUnit创建语句,委托JDBCExecutorWrapper实现,这里不看了,简单的通过connection创建statement。

执行

JDBCExecuteEngine委托SQLExecuteTemplate(sharding-jdbc),实现执行SQL。重点关注传入template的ProxySQLExecuteCallback(callback02、callback03),callback以外的逻辑与sharding-jdbc是一套逻辑(串行执行和并行执行的策略逻辑)。

JDBCExecuteEngine构造SQLExecuteTemplate时,决定是并行还是串行执行,和sharding-jdbc一样,LOCAL或XA事务中,串行执行;否则并行执行。

// BackendConnection
public boolean isSerialExecute() {
    return stateHandler.isInTransaction() 
    && (TransactionType.LOCAL == transactionType || TransactionType.XA == transactionType);
}

ProxySQLExecuteCallback执行SQL,callback02作为第一个执行callback,会获取QueryHeader(表头信息),然后同样是根据ConnectionMode选择QueryResult的实现是流式归并StreamQueryResult还是内存归并MemoryQueryResult。

private ExecuteResponse executeSQL(final Statement statement, final String sql, final ConnectionMode connectionMode, final boolean withMetadata) throws SQLException {
    // ...
    // 执行SQL
    if (jdbcExecutorWrapper.executeSQL(statement, sql, isReturnGeneratedKeys)) {
        ResultSet resultSet = statement.getResultSet();
        List<QueryHeader> queryHeaders = null;
        if (withMetadata) {
            // 如果是第一个sql 这里就需要返回QueryHeader表头
            queryHeaders = getQueryHeaders(sqlStatementContext, resultSet.getMetaData());
        }
        // 根据ConnectionMode选择流式归并还是内存归并
        QueryResult queryResult = createQueryResult(resultSet, connectionMode);
        return new ExecuteQueryResponse(queryHeaders, queryResult);
    }
    // ...
}

6、写客户端

回到CommandExecutorTask#executeCommand,此时SQL执行完毕,并且结果集也已经归并为MergedResult了。

private boolean executeCommand(final ChannelHandlerContext context, final PacketPayload payload, final BackendConnection backendConnection) throws SQLException {
    // ...
    // 执行(业务逻辑)
    Collection<DatabasePacket> responsePackets = commandExecutor.execute();
    // 提交到channel对应EventLoop写入ChannelOutboundBuffer
    // 并没有flush,还不会给客户端,是否flush通过协议的isFlushForPerCommandPacket返回给外部
    for (DatabasePacket each : responsePackets) {
        context.write(each);
    }
    // 如果是查询,需要将查询结果写回客户端,执行CommandExecuteEngine的writeQueryData方法
    if (commandExecutor instanceof QueryCommandExecutor) {
        commandExecuteEngine.writeQueryData(context, backendConnection, (QueryCommandExecutor) commandExecutor, responsePackets.size());
        return true;
    }
    // 返回这个数据库协议 最终是否需要flush
    return databaseProtocolFrontendEngine.getFrontendContext().isFlushForPerCommandPacket();
}

首先把执行结果DatabasePacket通过ChannelHandlerContext提交到channel对应EventLoop写入ChannelOutboundBuffer。接着如果是查询,需要把查询结果集写回客户端,进入MySQLCommandExecuteEngine#writeQueryData

public void writeQueryData(final ChannelHandlerContext context,
                           final BackendConnection backendConnection, final QueryCommandExecutor queryCommandExecutor, final int headerPackagesCount) throws SQLException {
    int count = 0;
    // proxy.frontend.flush.threshold 默认 128
    int flushThreshold = ShardingProxyContext.getInstance().getProperties().<Integer>getValue(ConfigurationPropertyKey.PROXY_FRONTEND_FLUSH_THRESHOLD);
    int currentSequenceId = 0;
    while (queryCommandExecutor.next()) {
        count++;
        // channel不可写时,flush一次,然后阻塞等待channel可写
        while (!context.channel().isWritable() && context.channel().isActive()) {
            context.flush();
            backendConnection.getResourceSynchronizer().doAwait();
        }
        // DatabaseCommunicationEngine缓存的MergedResult一行一行取出来写入ChannelOutboundBuffer
        DatabasePacket dataValue = queryCommandExecutor.getQueryData();
        context.write(dataValue);
        // 满128,flush一次,真正写回客户端
        if (flushThreshold == count) {
            context.flush();
            count = 0;
        }
        currentSequenceId++;
    }
    // 最后写eof包
    context.write(new MySQLEofPacket(++currentSequenceId + headerPackagesCount));
}

重点关注一个参数proxy.frontend.flush.threshold,默认128,决定了查询结果集每次最大发送给客户端的行数,到了128会flush一次写给客户端。如果没满128,这里并不会实际写到客户端,仅仅保存在ChannelOutboundBuffer内存中。

MySQLComStmtExecuteExecutor#getQueryData方法获取结果集packet。

public MySQLPacket getQueryData() throws SQLException {
    QueryData queryData = databaseCommunicationEngine.getQueryData();
    return new MySQLBinaryResultSetRowPacket(++currentSequenceId, queryData.getData(), getMySQLColumnTypes(queryData));
}

JDBCDatabaseCommunicationEngine#getQueryData返回QueryData。可以看到在这一步,通过已经持有的MergedResult组装结果,并且在这一步实现了数据脱敏。

// 响应
private BackendResponse response;
// 查询归并结果
private MergedResult mergedResult;
public QueryData getQueryData() throws SQLException {
    List<QueryHeader> queryHeaders = ((QueryResponse) response).getQueryHeaders();
    List<Object> row = new ArrayList<>(queryHeaders.size());
    for (int columnIndex = 1; columnIndex <= queryHeaders.size(); columnIndex++) {
        Object value = mergedResult.getValue(columnIndex, Object.class);
        if (isQueryWithCipherColumn && encryptRule.isPresent()) {
            // ... 省略数据脱敏
        }
        row.add(value);
    }
    return new QueryData(getColumnTypes(queryHeaders), row);
}

最后executeCommand返回当前数据库协议是否需要最终执行一次flush操作,对于MySQL来说,很明显查询结果集不满128行数据的时候,还没执行flush操作,需要外部执行flush,所以返回true。

public void run() {
    try (BackendConnection backendConnection = this.backendConnection;
         PacketPayload payload = databaseProtocolFrontendEngine.getCodecEngine().createPacketPayload((ByteBuf) message)) {
         // ...
         // 对于MySQL来说这里会返回true
        isNeedFlush = executeCommand(context, payload, backendConnection);
    } catch (final Exception ex) {
       // ...
    } finally {
        // 最终将剩余数据刷写回客户端
        if (isNeedFlush) {
            context.flush();
        }
    }
}