淘寶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);
        }
    }

對于整體架構和其他源碼的分析,我們將在第二篇進行深入分析,請大家關注。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容