多语言多货币实时汇率系统设计指南
跨境电商独立站的一个硬性需求就是支持多语言、多币种。汇率换算看似简单,但要在高并发、实时性要求下稳定运行,背后需要一套精心设计的微服务。这篇文章就专门来聊聊,怎么搭建一个稳定又高效的汇率微服务——对接外部API(比如阿里云外汇汇率API),用Redis缓存来降低调用成本,同时保证百万级请求下的实时换算性能。案例来自Taoify跨境电商的汇率系统,每天要处理上百万次换算请求,算是一个比较典型的场景。
一、系统架构
整体架构分为三层,各司其职:
- 采集层:定时拉取实时汇率,并同时写入数据库和Redis。
- 缓存层:Redis里存放最新汇率,TTL设置为2小时,保证数据新鲜度同时控制成本。
- 计算层:对外提供REST接口,接收金额、源货币、目标货币,返回换算结果。
这三层拆得很清楚,采集层负责数据的源头,缓存层做性能保障,计算层专注业务逻辑。实际落地上,每个服务都可以独立部署、水平扩展,也方便后续替换外部API或者调整缓存策略。
二、汇率采集
采集逻辑由定时任务触发,每2小时执行一次。核心代码实现如下:
@Component
public class ExchangeRateCollector {
@Autowired
private RedisTemplate redisTemplate;
@Scheduled(cron = "0 0 */2 * * ?")
public void collect() {
// 调用阿里云外汇汇率API
String url = "https://market.aliyun.com/apimarket/detail?productId=xxx";
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "APPCODE " + APP_CODE);
HttpEntity entity = new HttpEntity<>(headers);
ResponseEntity response = restTemplate.exchange(url, HttpMethod.GET, entity, RateResponse.class);
Map rates = response.getBody().getRates();
// 存储到Redis,Hash结构
redisTemplate.opsForHash().putAll("exchange_rates", rates);
// 同时存储一份到数据库,用于历史追溯
sa veToDatabase(rates);
}
}
注意这里用了Redis的Hash结构来存所有汇率,key是货币代码,value是汇率值。这样取单个货币的汇率非常快。另外数据库也存一份,主要是为了后续做历史数据分析和审计,不是实时必需。
三、汇率换算接口
换算接口的核心逻辑是以CNY(软妹币)作为中间桥梁。先判断源货币是不是CNY,如果不是,用源货币汇率除以金额得到软妹币金额;如果目标货币不是CNY,再乘以目标货币汇率。这样做的好处是只需要维护一套以软妹币为基准的汇率表,避免全量交叉换算的复杂度。
@RestController
public class ExchangeController {
@Autowired
private RedisTemplate redisTemplate;
@GetMapping("/exchange")
public ExchangeResult convert(@RequestParam BigDecimal amount,
@RequestParam String from,
@RequestParam String to) {
// 基准货币为CNY,先转为CNY,再转为目标货币
BigDecimal cnyAmount;
if (!"CNY".equals(from)) {
BigDecimal fromRate = getRate(from);
cnyAmount = amount.divide(fromRate, 2, RoundingMode.HALF_UP);
} else {
cnyAmount = amount;
}
if ("CNY".equals(to)) {
return new ExchangeResult(cnyAmount);
}
BigDecimal toRate = getRate(to);
BigDecimal result = cnyAmount.multiply(toRate);
return new ExchangeResult(result.setScale(2, RoundingMode.HALF_UP));
}
private BigDecimal getRate(String currency) {
String rateStr = (String) redisTemplate.opsForHash().get("exchange_rates", currency);
if (rateStr == null) {
throw new BusinessException("不支持的货币类型");
}
return new BigDecimal(rateStr);
}
}
getRate方法直接去Redis的Hash里取,如果取不到就说明货币类型不支持,直接抛异常。异常处理这里用了BusinessException,实际生产环境建议统一封装错误码。
四、前端多货币展示优化
如果每个商品价格都实时调用后端接口,压力会很大。Taoify跨境电商的做法是:先用CDN缓存静态价格,当用户切换货币时,前端一次性批量调用汇率接口刷新页面所有价格。这样既保证了切换的实时性,又大幅减少了接口调用次数。
// 前端批量换算
async function convertPrices(targetCurrency) {
const amounts = collectAllPrices();
const response = await fetch('/exchange/batch', {
method: 'POST',
body: JSON.stringify({ amounts, targetCurrency })
});
const converted = await response.json();
updatePrices(converted);
}
后端批量接口在实现上用了CompletableFuture并发调用缓存,一次性处理多个货币的换算请求。原本串行需要500ms的批量操作,现在压缩到了50ms以内,提升非常明显。
