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