前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Redis的数据记录与回放

Redis的数据记录与回放

作者头像
一个架构师
发布2022-06-27 15:16:33
4770
发布2022-06-27 15:16:33
举报
文章被收录于专栏:从码农的全世界路过

前文介绍了byteman的基本语法以及流量回放平台,今天一起看下如何使用byteman如何对 Redis 相关命令进行数据记录和回放.

这里记录和回放的难点是找到redis命令执行的处理方法.

1

Redis数据切入点

本例中, 基于spring boot框架中redis使用三方依赖包的lettuce.jar

代码语言:javascript
复制
<dependency>
  <groupId>io.lettuce</groupId>
  <artifactId>lettuce-core</artifactId>
  <version>5.0.5.RELEASE</version>
  <scope>compile</scope>
</dependency>

lettuce中集成处理所有命令的核心处理类是AbstractInvocationHandler. 而redis的单机模式, 哨兵或者cluster集群模式, 都是实现抽象方法handleInvocation()完成命令执行的.

代码语言:javascript
复制
public abstract class AbstractInvocationHandler implements InvocationHandler {
    public final Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // ...
        return handleInvocation(proxy, method, args);
    }
    protected abstract Object handleInvocation(Object proxy, Method method, Object[] args) throws Throwable;
}

2

请求记录

前文已经提过, 请求录制一般都是在生产环境中, 所以Redis处理请求记录的时机, 应该是invoke()方法执行结束后, 将参数和结果通过日志打印出来. 通过Debug可以发现参数和返回值都是byte[]或者byte[][],这并不利于收集和回放时数据的还原. 所以需要对数据进行格式化, 这里选择base64编码.这时就需要helper了. byteman配置文件及注入方式,参考前文byteman.

redis-record.btm

代码语言:javascript
复制
####### record  #####
RULE redis execution recorder InvocationHandler.exit
CLASS io.lettuce.core.internal.AbstractInvocationHandler
METHOD invoke
AT EXIT
BIND handler = $this;
  method = $2;
  methodName = method.getName();
HELPER com.in.rt.helper.RedisReplayTraceHelper
IF !method.getName().equals("getConnection")
   && !method.getName().equals("readonly")
   && !method.getName().equals("nodes")
   && !method.getName().equals("command")
   && !method.getName().equals("hashcode")
   && !method.getName().equals("equals")
   && !method.getName().equals("toString")
DO log(methodName,$3,$!);
ENDRULE

byteman辅助类: RedisReplayTraceHelper

代码语言:javascript
复制
public void log(String methodName, Object[] args, Object result) {
    record("CACHE", methodName, args, result);
}

private void record(String prefix, String methodName, Object[] args, Object result) {
    String re = toStr(result);
    String arg = toStr(args);
    log.error(marker, "{}-{}-{}-{}-{}", prefix, methodName, System.nanoTime(), arg, re);
}
public String toStr(Object result) {
    try (final ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(baos)) {
        oos.writeObject(result);
        String logs = Base64.getEncoder().encodeToString(baos.toByteArray());
        log.debug("logs:{}", logs);
        return logs;
    } catch (Exception e) {
        log.error("toStr error:", e);
        throw new RuntimeException("toStr exception:", e);
    }
}

3

请求回放

请求回放是为了验证服务的新增功能是否对预想外的接口或逻辑产生影响, 这个过程一般会在测试环境中进行. 在具体实现时, 并不需要真正去执行原有命令, 只需要根据执行方法, 参数等信息从流量银行中取就可以. 同样也是利用byteman拦截执行字节码实现.

redis-replay.btm

代码语言:javascript
复制
####### replay  #####
RULE redis execution recorder InvocationHandler.entry
CLASS io.lettuce.core.internal.AbstractInvocationHandler
METHOD invoke
AT ENTRY
BIND handler = $this;
  method = $2;
  methodName = method.getName();
HELPER com.in.rt.helper.RedisReplayTraceHelper
IF !method.getName().equals("getConnection")
   && !method.getName().equals("readonly")
   && !method.getName().equals("nodes")
   && !method.getName().equals("command")
   && is(methodName, $3)
DO return replay(methodName)
ENDRULE

byteman辅助类: RedisReplayTraceHelper

代码语言:javascript
复制
public Object toObject(String str) {
    byte[] decode = Base64.getDecoder().decode(str.getBytes());
    try (final ByteArrayInputStream bain = new ByteArrayInputStream(decode);
            ObjectInputStream oin = new ObjectInputStream(bain)) {
        Object inObj = oin.readObject();
        log.debug("inObj:{}", inObj);
        return inObj;
    } catch (Exception e) {
        log.error("toObject error:{}", e);
        throw new RuntimeException("inObj exception:", e);
    }
}

数据获取类 DataFetcher

代码语言:javascript
复制
public class DataFetcher {

    private static final Logger log = LoggerFactory.getLogger("RedisCommandTraceHelper");

    public String getData(String host, String traceId, String methodName, String nanoTime,
            String args) {
        HttpURLConnection con = null;
        try {
            log.debug("replay fetch data:{},{},{},{}", traceId, methodName, nanoTime, args);
            String spec = host + "/api/data?traceId=" + traceId
                    + "&methodName=" + methodName + "&args=" + args;
            if (nanoTime != null) {
                spec = spec + "&nanoTime=" + nanoTime;
            }
            java.net.URL url = new java.net.URL(spec);
            con = (HttpURLConnection) url.openConnection();
            int status = con.getResponseCode();
            if (status != 200) {
                return null;
            }
            try (java.io.BufferedReader in = new java.io.BufferedReader(
                    new java.io.InputStreamReader(con.getInputStream()));) {
                return in.readLine();
            }
        } catch (IOException e) {
            log.error("replay fetch data error:{},{},{},{}",
                    traceId, methodName, nanoTime, args, e);
        } finally {
            if (con != null) {
                con.disconnect();
            }
        }
        return null;
    }
}

小结

文中的实现方式都是尽量避开依赖其他三方jar, 在实际项目中, 需要根据框架的不同做调整, 但整体思路是一样的. 另外需要说明的一点是, 这里因为数据并没有真实写入到中间件和服务中, 如果在此之上又添加其他逻辑, 是无法通过测试的. 这个需要在回归测试的时候做好判断.

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-05-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 从码农的全世界路过 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云数据库 Redis®
腾讯云数据库 Redis®(TencentDB for Redis®)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档