dubbo富客户端

Posted PersistentCoder

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了dubbo富客户端相关的知识,希望对你有一定的参考价值。

    富客户端(Fat Client),是一个与瘦客户端(Thin Client)对立的概念。常见的C/S架构就是富客户端,B/S架构是典型的瘦客户端。

    当然,“瘦”与“富”是相对而言,各有各自的优缺点。

富客户端

优点:

 1)有一部分功能在C端可以完成,一定程度上减少了网络交互次数和开销

 2)有独立的端,可以独立运行,不依赖其他平台

 3)客户端体验好,可以完成复杂的功能

缺点:

 1)客户端占用用户资源,有时对用户硬件或者系统要求高

 2)如果服务端有更新或者升级,一般情况下C端也要跟着更新,并且更新成本高

瘦客户端

优点:

 1)不需要用户单独下载应用C端,对用户硬件和操作系统基本无要求

 2)服务端升级,B端完全无感知

缺点:

 1)一般B端不适用于完成较复杂的功能

 2)浏览器层面的一些全局设置会影响到功能使用


    上边简单分析了常见的两种应用架构方式各自的优缺点,接下来进入主题,dubbo实现富客户端。这个概念是不太准确的,现在分布式架构是互联网的主流,所以叫做分布式富客户端更为合适。

    那么为什么要在分布式服务中引入富客户端的概念?我们先看一张图:

     如上图描述,Web层远程调用Rpc远程服务为B端(浏览器)提供服务,这样做是没有问题的,也符合当前流行的分布式架构方式;但是高并发是所有互联网企业都要面临的问题,当然缓存是解决高并发的神器,那么如果上述架构再做一下改造如下图:

dubbo富客户端

    

         从改造后的图中可以看到,rpc服务暴露给调用放的client很薄,只有简单的接口定义,具体的实现还在provider层,也就是具体的服务实现和提供者。这样上层Web层只需要引用暴露出的client利用Rpc框架就可以调用到具体的服务,在并发查询场景下,虽然引入了缓存,但是不知道有没有人注意到,缓存是和rpc服务提供者不在一个主机甚至可能不在一个机房,Web层和Rpc服务层也可能不在一个机房,也就是说从Web层调用Rpc服务,Rpc操作缓存都有网络开销, 举个例子,如果Web层查询的数据在缓存中已经存在,那么回去直接从缓存中获取,但是调用链路还是Web调用Rpc服务,Rpc服务再从缓存获取数据返回,会有两次网络开销,我们开发者都会有一个概念,调用链路边长会增加失败的概率,在网络环境不好的情况下,一次网络开销和两次网络开销相比,体验优劣是很明显的。所以这种架构方式在流量大的项目中是不太可取的。

        根据上述描述,我们对方案再做改进:

dubbo富客户端


         图中加了一个步骤,Web层在调用Rpc服务之前,先去缓存拿数据,如果命中直接返回,否则继续调用Rpc服务。也就是说在缓存命中的场景下,减少了一次网络开销。增加的节点,其实是Rpc服务的client提供的,对缓存和rpc服务的一个封装,然后暴露给调用者,也即是我们所说的富客户端的概念。


     描述了很多概念和理论性的东西,下边我们使用Dubb框架来实现富客户端。


I 服务端编写

   服务端结构如下图(基于上一篇的代码):

dubbo富客户端


具体实现参考


        在dubbo-server-interface中添加两个关键类:

CacheManager

/**

* 缓存操作类

*/

public class CacheManager implements TCache,InitializingBean {


    private  JedisPool jedisPool;


    public void setJedisPool(JedisPool jedisPool) {

        this.jedisPool = jedisPool;

    }


    @Override

    public <T> T get(Serializable key, Class<T> type) {


        Jedis jedis = null;

        try {

          jedis  = jedisPool.getResource();

          byte[]  data = jedis.get(key.toString().getBytes());

          return SerializationUtil.deserialize(data,type);

        } catch (Exception e) {

            e.printStackTrace();

        } finally {

            if(null != jedis) {

                jedis.close();

            }

        }

        return null;

    }


    @Override

    public void put(Serializable key, Object val) {

        Jedis jedis = null;

        try {

            jedis  = jedisPool.getResource();

            byte[] data = SerializationUtil.serialize(val);

            jedis.set(key.toString().getBytes(),data);

        } catch (Exception e) {

            e.printStackTrace();

        } finally {

            if(null != jedis) {

                jedis.close();

            }

        }

    }


    @Override

    public void afterPropertiesSet() throws Exception {

        if(null == jedisPool) {

            throw new RuntimeException("jedis没有初始化");

        }

    }

}


UserReadClient

/**

 * 用户信息操作客户端

 *

 * @author Typhoon

 * @date 2018-05-20 16:04 Sunday

 * @since V2.0.0

 */

public class UserReadClient implements InitializingBean {


    private static final String KEY_PREFIX = "userCachePrefix:";


    private CacheManager cacheManager;


    private UserService userService;


    public void setCacheManager(CacheManager cacheManager) {

        this.cacheManager = cacheManager;

    }


    public void setUserService(UserService userService) {

        this.userService = userService;

    }


    /**

     * 查询用户信息

     *

     * @param id

     * @return

     */

    public UserDto queryByPK(Long id) {

        if(null == id || id <= 0) {

            throw new IllegalArgumentException("参数非法");

        }

        UserDto userDto = this.cacheManager.get(KEY_PREFIX + id,UserDto.class);

        if(null != userDto) {//走缓存

            return userDto;

        }

        //走DB

        userDto = this.userService.queryByPK(id);

        try {

            this.cacheManager.put(KEY_PREFIX + id,userDto);

        } catch (Exception e) {

            e.printStackTrace();

        }

        return userDto;

    }


    @Override

    public void afterPropertiesSet() throws Exception {

        if(null == cacheManager || null == userService) {

            throw new IllegalArgumentException("tCache 或者userService没有初始化 ");

        }

    }

}


然后把dubbo-server-interface安装到本地maven仓库供其他消费端依赖,并启动服务.


II 消费端编写与测试

  消费端redis的配置此处不做赘述,有兴趣可参见源码。

https://gitee.com/ScorpioAeolus/dubbo-consumer

在dubbo-consumer.xml中添加远程服务引用:

<dubbo:reference interface="com.typhoon.service.UserService"  url="dubbo://localhost:20289" id="userService" protocol="dubbo" timeout="30000"/>

        在主配置文件或者其他配置中增加:

    <bean id = "cacheManager" class="com.typhoon.cache.CacheManager">

        <property name="jedisPool" ref="jedisPool" />

    </bean>

    <bean id = "userReadClient" class="com.typhoon.client.UserReadClient">

        <property name="cacheManager" ref="cacheManager" />

        <property name="userService" ref="userService" />

    </bean>


然后再在本地服务中注入UserReadClient:

@Autowired

private UserReadClient userReadClient;


@Override

public ResponseBase doQueryUserById(Long id) {

ResponseBase resp = new ResponseBase();

UserDto userDto = this.userReadClient.queryByPK(id);

resp.setAttach(userDto);

return resp;

}

编写单元测试:

    @Test

public void testA() {

try {

ResponseBase resp = this.imitateConsumerService.doQueryUserById(1L);

System.out.println(JSON.toJSONString(resp));

} catch (Exception e) {

e.printStackTrace();

}

}

运行单元测试并debug:

1)第一次运行

可以看到查询没有命中缓存,走DB查询,调用了远程Rpc服务


2)第二次运行

debug代码看到查询命中了缓存不走DB查询,直接返回


 总结

    经过上述的描述和代码验证,我们使用Dubbo富客户端,其原理就是Rpc服务编写并暴露出一个复合客户端,里边包含了查询缓存和掉服务查DB的逻辑,

        但是缓存的具体配置交给调用方,这样服务调用方使用富客户端的时候会优先走自己的缓存逻辑,如果缓存不命中就继续调用Rpc服务查询,这样就解决了

并发场景虽然后走了缓存但是依旧多走一次网络开销的问题。


    此篇我们根据真实业务场景讲解了dubbo富客户端实现和引用,希望给大家在日常开发中带来帮助!


以上是关于dubbo富客户端的主要内容,如果未能解决你的问题,请参考以下文章

富客户端 wpf, Winform 多线程更新UI控件

“智能客户端应用程序”和“富互联网应用程序”有啥区别?

在 html/css/js 之上是不是有任何用于富 Web 客户端的框架?

服务器应该如何向富客户端推送数据

如何实现一个 Android 端的富文本编辑器

用于 WPF 富客户端应用程序的图像编辑器组件