本文目录导读:

- 分布式缓存中的“空对象” (避免缓存穿透)
- 分布式RPC调用中的空对象 (避免NPE)
- 分布式数据库查询中的空对象 (避免多次IO)
- 分布式消息队列中的空对象 (消息消重)
- 分布式配置中心中的空对象 (默认配置)
- 分布式空对象模式的3个核心点
在Java分布式系统中,“空对象模式”和“空对象”的概念需要根据上下文区分,下面从分布式场景下的不同维度来解答,并提供代码示例。
分布式缓存中的“空对象” (避免缓存穿透)
这是最常见的场景,当查询一个不存在的数据时,如果不做处理,会直接穿透缓存打到数据库,此时将“查询为空”的结果也缓存起来(空对象),防止反复击穿。
示例 (Redis + JSON序列化)
// 假设查询用户信息
public User getUserById(String userId) {
// 1. 先从缓存查
String cachedValue = redisClient.get("user:" + userId);
if (cachedValue != null) {
// 关键:反序列化成User,但要能判断是“空对象”
User user = JsonUtil.parse(cachedValue, User.class);
if (user.isNullObject()) { // 自定义标记
return null; // 代表数据库没数据
}
return user;
}
// 2. 缓存没有,查数据库
User user = userDao.findById(userId);
// 3. 无论是否存在,都存入缓存(空对象机制)
if (user == null) {
// 存入一个标记空的对象,并设置较短过期时间(比如5分钟)
User empty = User.createEmpty(); // isNullObject() 返回true
redisClient.setex("user:" + userId, 300, JsonUtil.toJson(empty));
} else {
redisClient.setex("user:" + userId, 3600, JsonUtil.toJson(user));
}
return user;
}
User类设计:
public class User implements Serializable {
private String id;
private String name;
// ... 其他字段
private boolean nullObject = false; // 默认false
public static User createEmpty() {
User empty = new User();
empty.nullObject = true;
// 可以设置一些默认ID如 "__NULL__"
empty.id = "__NULL__";
return empty;
}
public boolean isNullObject() {
return nullObject;
}
}
分布式RPC调用中的空对象 (避免NPE)
在分布式服务调用时,如果某个服务返回null,调用方频繁判空,使用空对象模式返回一个“默认行为”的对象,避免NullPointerException。
典型场景:服务降级/熔断
// 定义一个远程调用接口
public interface InventoryService {
InventoryInfo getInventory(String skuId);
}
// 空对象实现
public class NullInventoryInfo extends InventoryInfo {
@Override
public int getAvailableQuantity() {
return 0; // 或者返回 -1 表示未知
}
@Override
public boolean isAvailable() {
return false;
}
@Override
public String getWarehouseId() {
return "UNKNOWN";
}
}
// 调用方使用(配合Hystrix/Sentinel)
public class InventoryClient {
public InventoryInfo getInventory(String skuId) {
try {
InventoryInfo info = remoteService.getInventory(skuId);
if (info == null) {
return new NullInventoryInfo(); // 返回空对象,而不是null
}
return info;
} catch (Exception e) {
log.error("调用库存服务失败, skuId={}", skuId, e);
return new NullInventoryInfo(); // 熔断降级时返回空对象
}
}
}
分布式数据库查询中的空对象 (避免多次IO)
在分布式数据库(如HBase、Cassandra、MongoDB分片)查询时,如果某行不存在,可以返回一个轻量级空对象标记,避免后续重复查询。
示例 (MongoDB + 本地缓存)
public class DocumentFetcher {
private Cache<String, Document> localCache;
private MongoCollection<Document> collection;
public Document getDocument(String key) {
// 先查本地缓存
Document cached = localCache.getIfPresent(key);
if (cached != null) {
// 检查是否是空标记
if (cached.containsKey("__null__")) {
return null;
}
return cached;
}
// 查分布式数据库
Document doc = collection.find(Filters.eq("_id", key)).first();
if (doc == null) {
// 存入一个空对象标记到本地缓存(短暂过期)
Document emptyMarker = new Document("__null__", true);
localCache.put(key, emptyMarker, 60, TimeUnit.SECONDS);
return null;
}
localCache.put(key, doc, 300, TimeUnit.SECONDS);
return doc;
}
}
分布式消息队列中的空对象 (消息消重)
当消费消息时,用空对象表示“消息已处理但结果为空”,结合Redis setnx实现幂等。
@KafkaListener(topics = "order-events")
public void handleOrderEvent(OrderEvent event) {
String eventId = event.getEventId();
// 用空对象标记已处理
String processedKey = "processed:" + eventId;
Boolean set = redisTemplate.opsForValue().setIfAbsent(processedKey, "__PROCESSED_EMPTY__", 24, TimeUnit.HOURS);
if (!set) {
// 已经处理过,跳过
return;
}
try {
// 实际业务处理
OrderResult result = processOrder(event);
if (result == null) {
// 结果为空,但已经标记了“已处理”
log.info("订单事件处理结果为空, eventId={}", eventId);
}
} catch (Exception e) {
// 处理失败,删除标记让其可以重试
redisTemplate.delete(processedKey);
throw e;
}
}
分布式配置中心中的空对象 (默认配置)
使用Apollo/Nacos配置中心时,有些配置可能不存在,定义空配置对象,避免频繁远程拉取。
@Component
public class DynamicConfig {
// 空配置对象
private static final ConfigValue EMPTY_CONFIG = new ConfigValue() {
@Override
public String getString(String key) {
return null;
}
@Override
public int getInt(String key, int defaultValue) {
return defaultValue;
}
@Override
public boolean exists() {
return false;
}
};
private ConfigValue currentConfig;
@PostConstruct
public void init() {
ConfigChangeListener listener = changeEvent -> {
// 配置变更时更新
refreshConfig();
};
configService.addChangeListener(listener);
refreshConfig();
}
private void refreshConfig() {
ConfigValue cfg = configService.getConfig("myApp");
if (cfg == null) {
this.currentConfig = EMPTY_CONFIG; // 使用空对象
} else {
this.currentConfig = cfg;
}
}
public int getTimeout() {
return currentConfig.getInt("timeout", 5000);
}
}
分布式空对象模式的3个核心点
| 维度 | 作用 | 实现方式 |
|---|---|---|
| 缓存穿透 | 缓存不存在的key,防止打穿DB | 缓存一个标记空的对象(短TTL) |
| RPC降级 | 避免频繁if null + 提供默认行为 | 实现接口的空对象子类(NullPattern) |
| 幂等/重复 | 标记已处理的空结果 | Redis setnx 标记 + 短过期时间 |
关键设计原则:
- 空对象必须有明确的标记(如
isNullObject(),__NULL__) - 空对象的过期时间要小于正常数据(防止长期占用内存)
- 空对象要轻量,只存必要字段,避免序列化开销
- 结合分布式锁/幂等标记,避免并发场景下重复创建空对象