dubbo富客户端

2020-11-19 15:00:03 浏览数 (1)

富客户端(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端(浏览器)提供服务,这样做是没有问题的,也符合当前流行的分布式架构方式;但是高并发是所有互联网企业都要面临的问题,当然缓存是解决高并发的神器,那么如果上述架构再做一下改造如下图:

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

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

图中加了一个步骤,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富客户端实现和引用,希望给大家在日常开发中带来帮助!

0 人点赞