0%

Spring 缓存解决方案

伴随信息量的爆炸式增长以及构建的应用系统越来越多样化、复杂化,特别是企业级应用互联网化的趋势,缓存(Cache)对应用程序性能的优化变的越来越重要。 将所需服务请求的数据放在缓存中,既可以提高应用程序的访问效率,又可以减少数据库服务器的压力,从而让用户获得更好的用户体验。

Spring 从 3.1 开始,以一贯的优雅风格提供了一种透明的缓存解决方案,这使得 Spring 可以在后台使用不同的缓存框架(如EhCache、GemFire、HazelCast 和 Guava)时保持编程的一致。

Spring 从 4.0 开始则全面支持 JSR-107 annotations 和自定义的缓存标签。

简介

Spring 提供可一种可以在方法级别进行缓存的缓存抽象。 通过使用 AOP 对方法机型织入,如果已经为特定方法入参执行过该方法,那么不必执行实际方法就可以返回被缓存的结果。

Spring Cache的好处

  • 支持开箱即用(Out Of The Box),并提供基本的 Cache 抽象,方便切换各种底层 Cache
  • 通过 Cache 注解即可实现缓存逻辑透明化,让开发者关注业务逻辑
  • 当事务回滚时,缓存也会自动回滚
  • 支持比较复杂的缓存逻辑
  • 提供缓存编程的一致性抽象,方便代码维护。

Spring Cache的缺点

  • Spring Cache 并不针对多进程的应用环境进行专门的处理。
  • 另外 Spring Cache 抽象的操作中没有锁的概念,当多线程并发操作(更新或者删除)同一个缓存项时,有可能读取到过期的数据。

缓存管理器

缓存管理器封装了 Spring Cache 的执行逻辑,Spring 提供了多种 CacheManager:org.springframework.cache.support.SimpleCacheManagerorg.springframework.data.redis.cache.RedisCacheManager 等。

以 RedisCacheManager 为例,其继承链为 RedisCacheManager -> AbstractTransactionSupportingCacheManager -> AbstractCacheManager -> CacheManager

CacheManager 缓存管理器底层接口

CacheManager 为基本接口,定义了获取缓存器的方法。

public interface CacheManager {  
  
   Cache getCache(String name);  
   
   Collection<String> getCacheNames();  
}

AbstractCacheManager 缓存管理器公共抽象

AbstractCacheManager 定义了缓存管理过程:初始化缓存管理器、新增缓存器、根据名称获取缓存器、装饰缓存器、Miss时补偿获取缓存器。

AbstractTransactionSupportingCacheManager 缓存管理器事务装饰

通过重载 decorateCache 装饰缓存器方法,创建 TransactionAwareCacheDecorator 代理 Cache,达到支持事务。

@Override  
protected Cache decorateCache(Cache cache) {  
   return isTransactionAware() ? 
	   new TransactionAwareCacheDecorator(cache) : cache;  
}

以 put 操作为例,当事务开启时,会在事务成功提交后才执行。

@Override  
public void put(final Object key, @Nullable final Object value) {  
   if (TransactionSynchronizationManager.isSynchronizationActive()) {  
	    TransactionSynchronizationManager.registerSynchronization(
		    new TransactionSynchronization() {  
		        @Override  
		         public void afterCommit() {  
		            TransactionAwareCacheDecorator.this.targetCache.put(key, value);  
		        }      
			}
		);   
	}  else {  
		this.targetCache.put(key, value);  
	}
}

RedisCacheManager 缓存管理器Redis支持

缓存写入器

RedisCacheWriter 封装了操作 Redis Client 的底层实现。

锁机制

RedisCacheWriter 内部提供了两种写入锁机制:有锁写入和无锁写入。无锁写入顾名思义,多线程写时不加锁。

RedisCacheWriter.lockingRedisCacheWriter(); // 有锁写入
RedisCacheWriter.nonLockingRedisCacheWriter(); // 无锁写入

重点说下有锁写入。

private <T> T execute(String name, Function<RedisConnection, T> callback) {  
  
   RedisConnection connection = connectionFactory.getConnection();  
   try {  
	  // 循环通过 exist 检测是否在 redis 内有锁,每次循环间隔 sleepTime
      checkAndPotentiallyWaitUntilUnlocked(name, connection);  
      // 如果检测到无锁,执行对应 redis 命令
      return callback.apply(connection);  
   } finally {  
      connection.close();  
   }
}

callback 调用的是实际的 redis 方法,有锁写入仅支持 putIfAbsent 和 clear 两种方法,以 putIfAbsent 为例。

public byte[] putIfAbsent(String name, byte[] key, byte[] value, @Nullable Duration ttl) {  
  
return execute(name, connection -> {  
	// 如果是有锁写入则给该 key 加锁
	// 锁的 key 为缓存器名称 + ~lock
	if (isLockingCacheWriter()) {  
		doLock(name, connection);  
	}  
	
	try {  
		connection.setNX(key, value);  
		return connection.get(key);  
	} finally {  

		// 如果是有锁写入则给该 key 释放if (isLockingCacheWriter()) {  
			doUnlock(name, connection);  
		}      
	}
}

BatchStrategy 批处理策略

需要注意的是在构造时有一个 BatchStrategy 批处理策略。

批处理策略有两种默认实现:keys 和 scan,分别对应 redis keys 和 scan 两个命令。当有需要调用 clean 时会根据这两种命令批量获取键。

keys 和 scan 命令区别

keys 是根据所给 pattern 遍历所有 key,复杂度是 O(n),当实例中数据量过大的时候,Redis 服务可能会卡顿,其余指令可能会延时甚至超时报错,生产环境建议屏蔽掉该指令。

scan 是 keys 命令的替代方案,其复杂度虽然也是 O(n),但有以下特性:

  1. 通过游标分步进行的,不会阻塞线程;
  2. 提供 limit 参数,可以控制每次返回结果的最大条数,limit 只是对增量式迭代命令的一种提示 (hint),返回的结果可多可少;
  3. 返回的结果可能会有重复,需要客户端去重复;
  4. 遍历的过程中如果有数据修改,改动后的数据能不能遍历到是不确定的;
  5. 单次返回的结果是空的并不意味着遍历结束,而要看返回的游标值是否为零。

缓存统计功能

可在 RedisCacheManagerBuilder 中开启。

RedisCacheManager.builder()  
	.cacheWriter(redisCacheWriter)  
	.cacheDefaults(defaultCacheConfig)  
	.withInitialCacheConfigurations(redisCacheConfigurationMap)  
//	.disableCreateOnMissingCache()  

	// 开启缓存统计
	.enableStatistics()  
	.build();

也可在 RedisCacheWriter 中开启,推荐重写 CacheStatisticsCollector 用做业务监控,默认实现统计结果存储在本地,多机情况下还需要单独聚合,使用 Prometheus 更加方便。

// 初始化一个RedisCacheWriter  
RedisCacheWriter redisCacheWriter = RedisCacheWriter  
        .nonLockingRedisCacheWriter(redisConnectionFactory)
        // 设置缓存统计收集器 效果等同于 在Builder开启缓存统计
        .withStatisticsCollector(CacheStatisticsCollector.create());

简单读取统计数据

@RestController  
public class TestController {  
  
    @Resource  
    private RedisCacheManager manager;  
  
    @Resource  
    private RedisCacheWriter writer;  
  
    @GetMapping("/statistic")  
    public Mono<Map<String, CacheStatistics>> statistic() {  
        Map<String, CacheStatistics> statisticsMap = manager.getCacheNames()  
                .stream()  
                .collect(Collectors.toMap(k -> k, writer::getCacheStatistics));  
        return Mono.just(statisticsMap);  
    }
}

缓存配置

RedisCacheManager 还支持对每一个缓存器单独定义相关配置。

先配置一个默认的配置,即缺省配置。

/**  
 * 自定义 Redis 配置  
 * 配置自动化缓存使用的序列化方式以及过期时间  
 */  
@Bean  
public RedisCacheConfiguration redisCacheConfiguration() {  
    return RedisCacheConfiguration.defaultCacheConfig()  
            // 配置value序列化方式为Jackson2JsonRedisSerializer,key序列化方式采用默认的StringRedisSerializer  
            .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(getSerializer(Object.class)))  
            // 设置默认超过期时间是1天(短时间的已经做了其他的处理,不会采用注解形式加入缓存)  
            .entryTtl(Duration.ofDays(1))  
            // 设置缓存名称前缀,最好以项目区分  
            .prefixCacheNameWith("COMMON-")  
            // 不缓存 null 值  
            .disableCachingNullValues();  
}

还可以通过 Map 的形式(Key 为缓存器名称,Value 为自定义配置)自定义缓存器的执行逻辑。

  
/**  
 * 缓存过期时间自定义配置  
 */  
@Bean  
public CacheManager cacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration configuration) {  
    // 对于不同类型的Key信息进行单独的缓存配置  
    Map<String, RedisCacheConfiguration> configMap = new HashMap<>();  
    // 对于名称为USER的缓存器,单独配置  
    configMap.put("USER", configuration.entryTtl(Duration.ofDays(10)).disableCachingNullValues().prefixCacheNameWith("USER-"));  
    // 初始化 RedisCacheManager   
    return RedisCacheManager.builder()  
            .cacheWriter(cacheWriter)  
            .cacheDefaults(configuration)  
            .withInitialCacheConfigurations(configMap)  
            .build();  
}

如示例,共用 Key 前缀为 COMMON- 但是 USER 缓存器的 Key 前缀为 USER-

缓存解析器

根据缓存解析器(CacheResolver)的策略,通过 CacheManager 获取 Cache。

@Override  
public Collection<? extends Cache> resolveCaches(CacheOperationInvocationContext<?> context) {  
   Collection<String> cacheNames = getCacheNames(context);  
   if (cacheNames == null) {  
      return Collections.emptyList();  
   }   Collection<Cache> result = new ArrayList<>(cacheNames.size());  
   for (String cacheName : cacheNames) {  
      Cache cache = getCacheManager().getCache(cacheName);  
      if (cache == null) {  
         throw new IllegalArgumentException("Cannot find cache named '" +  
               cacheName + "' for " + context.getOperation());  
      }      result.add(cache);  
   }   return result;  
}

内置的比较简单,可以定制开发实现。将缓存实现逻辑完全隔离在业务逻辑外,且提升了复用性。例如:两级缓存,本地缓存 -> 远程缓存 -> DB、热点缓存自动升降级,根据数仓将热数据存入本地缓存,冷数据存入 DB 或是远程缓存。

缓存Key生成器

使用 target、method、params 三个参数,根据自定义策略生成键值。

@Bean("customKeyGenerator")  
public KeyGenerator keyGenerator() {  
    return new KeyGenerator() {  
        @Override  
        public Object generate(Object target, Method method, Object... params) {  
            return method.getName() + "[" + Arrays.asList(params) + "]";  
        }    
	};
}

注解

缓存方法级注解

通用参数

参数 解释 示例
value 缓存器的名称,在 spring 配置文件中定义,必须指定至少一个
cacheNames 含义同 value
key 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 @Cacheable(cacheNames=”cache”, key=”#userName”)
keyGenerator key 和 keyGenerator 不能同时设置 @Cacheable(cacheNames = “cache”, keyGenerator = “myKeyGenerator”)
cacheManager 单独指定使用某个缓存管理器
cacheResolver 单独指定使用某个缓存解析器
condition 缓存操作执行的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才执行 @Cacheable(cacheNames=”cache”, condition=”#userName.length()>2”)

value 指定两个是什么逻辑

Cacheable

@Cacheable 的作用主要针对方法配置,能够根据方法的请求参数对其结果进行缓存。

略过通用参数介绍。

参数 解释 示例
unless 在方法执行结束后,根据方法的执行结果执行是否需要放入缓存 例如 unless = “#result != null”,表示仅当方法执行结果不为 null 时才放入缓存
sync 是否需要同步调用,如果设置为true,具有相同key的多次调用串行执行

CachePut

略过通用参数介绍。

参数 解释 示例
unless 在方法执行结束后,根据方法的执行结果执行是否需要放入缓存 例如 unless = “#result != null”,表示仅当方法执行结果不为 null 时才放入缓存

CacheEvict

略过通用参数介绍。

参数 解释 示例
allEntries 是否删除缓存中所有的记录(当前指定的cacheNames下),如果设置为false,仅删除设定的key
beforeInvocation 是否在方法调用前删除缓存,默认是false,仅当方法成功执行后才删除缓存,如果设定为true,则在调用前即删除缓存

Caching

为了一个方法操作多个缓存的情况做的扩展

public @interface Caching {
    Cacheable[] cacheable() default {};
    CachePut[] put() default {};
    CacheEvict[] evict() default {};
}

缓存类级注解

CacheConfig

@CacheConfig 是一个类级别的注解,可以在类级别上配置 cacheNames、keyGenerator、cacheManager、cacheResolver 等。

源码概略

注解 AOP 解析入口

解析 AOP 的入口是 BeanFactoryCacheOperationSourceAdvisor,其继承了 AbstractBeanFactoryPointcutAdvisor 允许在 BeanFactory 进行织入。在初始化 Bean 后会在 Bean PostProcessor 的过程中调起 getPointcut() 方法。

private final CacheOperationSourcePointcut pointcut = new CacheOperationSourcePointcut() {  
    @Nullable  
    protected CacheOperationSource getCacheOperationSource() {  
        return BeanFactoryCacheOperationSourceAdvisor.this.cacheOperationSource;  
    }
};

public Pointcut getPointcut() {  
    return this.pointcut;  
}

BeanFactoryCacheOperationSourceAdvisor 根据 CacheOperationSourcePointcut 这个抽象 Pointcut 类创建了一个内部类,并将 getPointcut() 方法绑定到该单例内部类中。

CacheOperationSourcePointcut 继承了 StaticMethodMatcherPointcut 抽象类,实现了匹配方法的规则。

@Override  
public boolean matches(Method method, Class<?> targetClass) {  
   CacheOperationSource cas = getCacheOperationSource();  
   return (cas != null && !CollectionUtils.isEmpty(cas.getCacheOperations(method, targetClass)));  
}

实际的匹配规则由 AnnotationCacheOperationSource 进行了实现。

注解 AOP 调用入口

调用 AOP 的入口是 CacheInterceptor,将原方法调用包装为 CacheOperationInvoker,通过 execute 方法触发附加操作。

public Object invoke(final MethodInvocation invocation) throws Throwable {  
   Method method = invocation.getMethod();  
  
   CacheOperationInvoker aopAllianceInvoker = () -> {  
      try {  
         return invocation.proceed();  
      }      catch (Throwable ex) {  
         throw new CacheOperationInvoker.ThrowableWrapper(ex);  
      }   };  
   Object target = invocation.getThis();  
   Assert.state(target != null, "Target must not be null");  
   try {  
      return execute(aopAllianceInvoker, target, method, invocation.getArguments());  
   }   catch (CacheOperationInvoker.ThrowableWrapper th) {  
      throw th.getOriginal();  
   }
}

根据平时的业务逻辑,实现 Cache-Aside 策略

private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {  

   // Process any early evictions  
   processCacheEvicts(contexts.get(CacheEvictOperation.class), true,  
         CacheOperationExpressionEvaluator.NO_RESULT);  
  
   // Check if we have a cached item matching the conditions  
   Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));  
  
   Object cacheValue;  
   Object returnValue;  
  
   if (cacheHit != null && !hasCachePut(contexts)) {  
      // If there are no put requests, just use the cache hit  
      cacheValue = cacheHit.get();  
      returnValue = wrapCacheValue(method, cacheValue);  
   }   else {  
      // Invoke the method if we don't have a cache hit  
      returnValue = invokeOperation(invoker);  
      cacheValue = unwrapReturnValue(returnValue);  
   }  
   // Collect any explicit @CachePuts  
   collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);  
  
   // Process any collected put requests, either from @CachePut or a @Cacheable miss  
   for (CachePutRequest cachePutRequest : cachePutRequests) {  
      cachePutRequest.apply(cacheValue);  
   }  
   // Process any late evictions  
   processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);  
  
   return returnValue;  
}

示例程序

Maven 配置

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-data-redis</artifactId>  
</dependency>

spring 配置

spring.cache.type=redis  
  
spring.redis.database=0  
spring.redis.host=localhost  
spring.redis.port=6379  
spring.cache.redis.time-to-live=50000ms

Redis 配置类

  
import java.lang.reflect.Method;  
import java.time.Duration;  
import java.util.Arrays;  
import java.util.HashMap;  
import java.util.Map;  
  
import javax.sql.DataSource;  
  
import org.springframework.cache.CacheManager;  
import org.springframework.cache.annotation.CachingConfigurerSupport;  
import org.springframework.cache.annotation.EnableCaching;  
import org.springframework.cache.interceptor.KeyGenerator;  
import org.springframework.context.annotation.Bean;  
import org.springframework.context.annotation.Configuration;  
import org.springframework.data.redis.cache.BatchStrategies;  
import org.springframework.data.redis.cache.CacheStatisticsCollector;  
import org.springframework.data.redis.cache.RedisCacheConfiguration;  
import org.springframework.data.redis.cache.RedisCacheManager;  
import org.springframework.data.redis.cache.RedisCacheWriter;  
import org.springframework.data.redis.connection.RedisConnectionFactory;  
import org.springframework.data.redis.core.RedisTemplate;  
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;  
import org.springframework.data.redis.serializer.RedisSerializationContext;  
import org.springframework.data.redis.serializer.StringRedisSerializer;  
import org.springframework.jdbc.datasource.DataSourceTransactionManager;  
import org.springframework.transaction.PlatformTransactionManager;  
  
import com.fasterxml.jackson.annotation.JsonAutoDetect;  
import com.fasterxml.jackson.annotation.PropertyAccessor;  
import com.fasterxml.jackson.databind.ObjectMapper;  
  
/**  
 * 缓存Redis配置  
 */  
@Configuration  
@EnableCaching  
public class RedisConfig extends CachingConfigurerSupport {  
    /**  
     * 使用Jackson2JsonRedisSerialize 替换默认序列化  
     */  
    @Bean  
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {  
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();  
        // 使用Jackson2JsonRedisSerialize 替换默认序列化  
        Jackson2JsonRedisSerializer<Object> serializer = getSerializer(Object.class);  
        // 设置value的序列化规则和 key的序列化规则  
        redisTemplate.setKeySerializer(new StringRedisSerializer());  
        redisTemplate.setValueSerializer(serializer);  
        // hash参数序列化方式  
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());  
        redisTemplate.setHashValueSerializer(serializer);  
        // 缓存支持回滚(事务管理)  
        redisTemplate.setEnableTransactionSupport(true);  
        redisTemplate.setConnectionFactory(redisConnectionFactory);  
        redisTemplate.afterPropertiesSet();  
        return redisTemplate;  
    }  
    /**  
     *  配置事务管理器  
     */  
    @Bean  
    public PlatformTransactionManager transactionManager(DataSource dataSource) {  
        return new DataSourceTransactionManager(dataSource);  
    }  
    /**  
     * 自定义 Redis 配置  
     * 配置自动化缓存使用的序列化方式以及过期时间  
     */  
    @Bean  
    public RedisCacheConfiguration redisCacheConfiguration() {  
        return RedisCacheConfiguration.defaultCacheConfig()  
                // 配置value序列化方式为Jackson2JsonRedisSerializer,key序列化方式采用默认的StringRedisSerializer  
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(getSerializer(Object.class)))  
                // 设置默认超过期时间是1天(短时间的已经做了其他的处理,不会采用注解形式加入缓存)  
                .entryTtl(Duration.ofDays(1))  
                // 设置缓存名称前缀,最好以项目区分  
                .prefixCacheNameWith("COMMON-")  
                // 不缓存 null 值  
                .disableCachingNullValues();  
    }  
    /**  
     * 自定义 RedisWriter 配置  
     *  
     * @param redisConnectionFactory redis连接工厂  
     * @return RedisCacheWriter  
     */    
    @Bean  
    public RedisCacheWriter cacheWriter(RedisConnectionFactory redisConnectionFactory, CacheStatisticsCollector statisticsCollector) {  
        return RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory, BatchStrategies.scan(10))  
                .withStatisticsCollector(statisticsCollector);  
    }  
  
    /**  
     * 自定义缓存收集器配置  
     * @return CacheStatisticsCollector  
     */    @Bean  
    public CacheStatisticsCollector statisticsCollector() {  
        return CacheStatisticsCollector.create();  
    }  
    /**  
     * 缓存过期时间自定义配置  
     */  
    @Bean  
    public CacheManager cacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration configuration) {  
        // 对于不同类型的Key信息进行单独的缓存配置  
        Map<String, RedisCacheConfiguration> configMap = new HashMap<>();  
        // 对于名称为USER的缓存器,单独配置  
        configMap.put("USER", configuration.entryTtl(Duration.ofDays(10)).disableCachingNullValues().prefixCacheNameWith("USER-"));  
        // 初始化 RedisCacheManager       return RedisCacheManager.builder()  
                .cacheWriter(cacheWriter)  
                .cacheDefaults(configuration)  
                .withInitialCacheConfigurations(configMap)  
                .build();  
    }  
    /**  
     * 自定义 Key 生成器  
     * @return KeyGenerator  
     */    
    @Bean("customKeyGenerator")  
    public KeyGenerator keyGenerator() {  
        return new KeyGenerator() {  
            @Override  
            public Object generate(Object target, Method method, Object... params) {  
                return method.getName() + "[" + Arrays.asList(params) + "]";  
            }        };    }  
    private <T> Jackson2JsonRedisSerializer<T> getSerializer(Class<T> clazz) {  
        Jackson2JsonRedisSerializer<T> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(clazz);  
        jackson2JsonRedisSerializer.setObjectMapper(getObjectMapper());  
        return jackson2JsonRedisSerializer;  
    }  
    private ObjectMapper getObjectMapper() {  
        ObjectMapper objectMapper = new ObjectMapper();  
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);  
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);  
        return objectMapper;  
    }
}

缓存代理类

import org.springframework.cache.annotation.Cacheable;  
import org.springframework.stereotype.Component;  
  
@Component  
public class TestCacheProxy {  
  
    @Cacheable(value = "USER", key = "#name + ':' + #tag")  
    public String getUser(String name, String tag) {  
        return String.format("user: %s, tag: %s", name, tag);  
    }  
    @Cacheable(value = "ORDER", key = "#id")  
    public String getOrder(String id) {  
        return String.format("order: %s", id);  
    }  
    @Cacheable(value = "ORDERINFO", keyGenerator = "customKeyGenerator")  
    public String getOrderInfo(String id) {  
        return String.format("order: %s", id);  
    }
}

测试类

import org.springframework.web.bind.annotation.GetMapping;  
import org.springframework.web.bind.annotation.PathVariable;  
import org.springframework.web.bind.annotation.RestController;  
  
import lombok.AllArgsConstructor;  
import reactor.core.publisher.Mono;  
  
@RestController  
@AllArgsConstructor  
public class TestController {  
  
    private TestCacheProxy proxy;  
  
    @GetMapping("/user")  
    public Mono<String> user(String name, String tag) {  
        return Mono.just(proxy.getUser(name, tag));  
    }  
    @GetMapping("/order/{id}")  
    public Mono<String> order(@PathVariable("id") String id) {  
        return Mono.just(proxy.getOrder(id));  
    }  
    @GetMapping("/order-info/{id}")  
    public Mono<String> orderInfo(@PathVariable("id") String id) {  
        return Mono.just(proxy.getOrderInfo(id));  
    }
}