淘宝Tedis组件究竟是个啥(一)

淘宝的Tedis组件究竟是个啥呢?可能有一些朋友没有听过这个名字,有一些朋友会经常使用,那么今天我就来和大家深入分析一下,它的使用和原理。

一、Tedis简介

Tedis是另一个redis的java客户端,Tedis的目标是打造一个可在生产环境直接使用的高可用Redis解决方案。

特性如下:

  • 高可用:Tedis使用多写随机读做HA确保redis的高可用
  • 高性能:使用特殊的线程模型,使redis的性能不限制在客户端
  • 多种使用方式:如果你只有一个redis实例,并不需要tedis的HA功能,可以直接使用tedis-atomic;使用tedis的高可用功能需要部署多个redis实例使用tedis-group
  • 两种API:包括针对byte的底层api和面向object的高层api
  • 多种方便使用redis的工具集合:包括mysql数据同步到redis工具,利用redis做搜索工具等

二、Tedis能做啥

1、Tedis的原理
Tedis是对开源的Redis客户端组件Jedis进行的封装,在Jedis的基础上封装了一套更易于使用的byte api和object api接口,在部署上使用的是master-master结构,实现了多写与随机读的机制。既:每个写请求都会发到所有服务器上面,每个读请求随机读取一个服务器,当在某个服务器读取失败后,将该台服务器加到重试队列中,直到服务器恢复正常客户端请求才会重新访问到该服务器。

2、典型的应用场景

Paste_Image.png

本图选自周成的《聚划算架构演化与系统优化》

二、Tedis的简单使用

可以先在Maven中依赖下面的jar包,当然还有更高版本的。

<dependency> 
    <groupId>com.taobao.common</groupId>
    <artifactId>tedis-group</artifactId>
    <version>1.1.0</version>
</dependency>

最简单的使用方式就是这样,只需要声明一个TedisGroup类并且初始化,然后就可以用ValueCommands进行set,get等操作了。唯一有点别扭的可能就是Tedis自己封装了一些命令类,需要我们自己去定义然后使用。

Group tedisGroup = new TedisGroup(appName, version);
tedisGroup.init();
ValueCommands valueCommands = new DefaultValueCommands(tedisGroup.getTedis());
// 写入一条数据
valueCommands.set(1, "test", "test value object");
// 读取一条数据
valueCommands.get(1, "test");

三、Tedis的源码分析

1、Tedis的包结构

Paste_Image.png

注:由于可以看出Tedis的结构实际分为了四个工程:
tedis-atomic、tedis-common、tedis-group、tedis-replicator、tedis-search。

  • tedis-atomic

Paste_Image.png

注:这个工程主要是集中一些Tedis的初始化类,像客户端类,连接类,初始化工厂类等等。

  • tedis-common

Paste_Image.png

注:这个工程主要就是集中了整个Tedis的工具类,异常类,线程类和监控类等。

  • tedis-group

Paste_Image.png

注:这个工程也是我们在源码分析中重点介绍的,里面汇集了Tedis最核心的代码,像集群,多写随机读等功能都在此实现。

  • tedis-relicator

Paste_Image.png

注:不是本文重点,不做解释,感兴趣的读者可以自己了解。

  • tedis-search

Paste_Image.png

注:不是本文重点,不做解释,感兴趣的读者可以自己了解。

2、tedis-group源码解析
我们看源码往往是通过使用的类开始的,也就是上面提到的TedisGroup类开始,

  • 我们先来看TedisGroup类结构图:
Paste_Image.png

注:TedisGroup类是一个非常核心的类,有一个很重要的方法就是init和一个很重要的内部类RedisGroupInvocationHandler,在TedisGroup中依赖了二个重要的类,ConfigManager和RedisCommands。

  • ConfigManager解析

    Paste_Image.png

    注:这个类的作用是:
    A、通过从Zookeeper、File、Diamond中解析有哪些Redis服务器,一共有几台服务器,超时时间和失败策略是如何。
    B、写入多个Redis和读取Redis的策略是怎么样的,在这个类中是用的Router路由的方式。

  • RedisCommands解析

Paste_Image.png

注:Tedis组件把Redis的操作命令全部封装到RedisCommands接口中,最底层的实现类是Tedis,通过这个类可以实现多个Redis的写和随机读等策略,Tedis类的部分源码如下:

    @Override
    public List<byte[]> sort(byte[] key, SortParams params) {
        try {
            if (params == null) {
                client.sort(key);
            } else {
                client.sort(key, params);
            }
            return client.getBinaryMultiBulkReply();
        } catch (Exception ex) {
            throw new TedisException(ex);
        }
    }


    @Override
    public Boolean ping() {
        client.ping();
        return PONG.equals(client.getStatusCodeReply());
    }

    @Override
    public Long del(byte[]... keys) {
        client.del(keys);
        return client.getIntegerReply();
    }

    @Override
    public Boolean exists(byte[] key) {
        client.exists(key);
        return client.getIntegerReply() == 1;
    }

    @Override
    public Set<byte[]> keys(byte[] pattern) {
        client.keys(pattern);
        return new HashSet<byte[]>(client.getBinaryMultiBulkReply());
    }

    @Override
    public Boolean persist(byte[] key) {
        client.persist(key);
        return client.getIntegerReply() == 1;

    }

    @Override
    public Boolean move(byte[] key, int dbIndex) {
        client.move(key, dbIndex);
        return client.getIntegerReply() == 1;
    }

    @Override
    public byte[] randomKey() {
        client.randomKey();
        return client.getBinaryBulkReply();
    }

     @Override
    public byte[] get(byte[] key) {
        client.get(key);
        return client.getBinaryBulkReply();
    }

    @Override
    public Boolean set(byte[] key, byte[] value) {
        client.set(key, value);
        return OK.equals(client.getStatusCodeReply());
    }
  • RedisGroupInvocationHandler内部类解析

这个类使用的是动态代理的模式,实现的是InvocationHandler接口,核心方法依然是
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
另外还有一个关键的注解就是Process,在RedisCommands的实现类Tedis中有使用,比如:

   @Process(Policy.WRITE)
    public Long zAdd(@ShardKey byte[] key, Tuple... value) {
        client.zadd(key, value);
        return client.getIntegerReply();
    }

标识了这个注解的方法在代理中会进行判断是写还是读操作,如果是写操作则会读取所有Redis配置以循环的方式逐一插入数据,如果其中一个Redis报错则记录日志抛出异常,如果是读操作则采用RandomRouter的方式随机从Redis列表中选取一个进行读取操作。具体部分源码实现如下:

       @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            long time = System.currentTimeMillis();
            String name = method.getName();
            Router rr = cm.getRouter();
            Process annotation = method.getAnnotation(Process.class);
            Throwable exception = null;
            if (annotation.value() == Policy.READ) {
                while (rr.getRouteData().props.size() > 0) {
                    Single s = rr.route();
                    try {
                        Object result = method.invoke(s.getTedis(), args);
                        statLog(name, true, time);
                        return result;
                    } catch (Throwable t) {
                        statLog(name, false, time);
                        exception = t;
                        logger.warn("read exception:" + s.getProperties(), t);
                        boolean connectionError = false;
                        try {
                            if (t instanceof InvocationTargetException) {// 解包异常
                                InvocationTargetException ite = (InvocationTargetException) t;
                                UndeclaredThrowableException ute = (UndeclaredThrowableException) ite.getTargetException();
                                if (ute.getUndeclaredThrowable() instanceof TimeoutException) {
                                    connectionError = true;
                                    rr.onError(s);
                                } else {
                                    ExecutionException ee = (ExecutionException) ute.getUndeclaredThrowable();
                                    InvocationTargetException ite_1 = (InvocationTargetException) ee.getCause();
                                    TedisException te = (TedisException) ite_1.getTargetException();
                                    if (te.getCause() instanceof TedisConnectionException) {
                                        connectionError = true;
                                        rr.onError(s);
                                    }
                                }
                            }
                        } catch (Throwable tt) {
                            logger.warn("解包异常:", tt);
                            // 可能会抛出转换异常,符合预期,如果碰到转换异常,直接在connection error
                            // 过程中从新抛出
                        }

                        if (!connectionError) {
                            throw t;
                        }
                    }
                }

                throw new Exception("read RouteData is empty," + rr, exception);
            } else if (annotation.value() == Policy.WRITE) {
                Single[] ss = rr.getRouteData().group;
                if (ss == null || ss.length == 0) {
                    throw new Exception("write RouteData is empty," + rr, exception);
                }
                Object result = null;
                int e = 0;
                for (Single s : ss) {
                    try {
                        result = method.invoke(s.getTedis(), args);
                    } catch (Throwable t) {
                        e++;
                        statLog(name, false, time);
                        logger.warn("write exception:" + s.getProperties(), t);
                        exception = t;
                        try {
                            // 解包异常
                            InvocationTargetException ite = (InvocationTargetException) t;
                            UndeclaredThrowableException ute = (UndeclaredThrowableException) ite.getTargetException();
                            if (ute.getUndeclaredThrowable() instanceof TimeoutException) {
                                rr.onError(s);
                            } else {
                                ExecutionException ee = (ExecutionException) ute.getUndeclaredThrowable();
                                InvocationTargetException ite_1 = (InvocationTargetException) ee.getCause();
                                TedisException te = (TedisException) ite_1.getTargetException();
                                if (te.getCause() instanceof TedisConnectionException) {
                                    rr.onError(s);
                                }
                            }
                        } catch (Throwable tt) {
                            logger.warn("解包异常:", tt);
                        }
                    }
                }

                if (e >= 2) {// 全部都抛异常了,告知调用端
                    throw exception;
                }
                statLog(name, true, time);
                return result;
            } else if ("toString".equals(name)) {
                return "";
            } else if ("hashCode".equals(name)) {
                Single s = rr.route();
                if (s != null) {
                    return s.hashCode();
                } else {
                    return 0;
                }
            } else if ("equals".equals(name)) {
                Single s = rr.route();
                if (args.length == 1) {
                    return s.equals(args[0]);
                }
            }
            statLog(name, false, time);
            throw new Exception("method don't match:" + name);
        }
    }

对于整体架构和其他源码的分析,我们将在第二篇进行深入分析,请大家关注。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345

推荐阅读更多精彩内容