CPU缓存失效与缓存一致性问题

CPU缓存失效与缓存一致性问题

我在之前学习操作系统的时候在想,我一个搞软件层面开发的为什么要了解别人设计好的东西,直接调用接口不行吗(反正我又不去手搓一个操作系统),这种荒谬的想法一直延续到今天。直到这次在设计一个高频交易的系统时,我才意识到这些知识有多重要。

我先来大致介绍一下该系统,该系统的全名为来财智投LCST数字资产交易系统。附上仓库地址,该系统支持现金交易、币币交易,采用Disruptor高性能框架进行订单撮合,并且通过WebSocket实时推送供用户查看当前K线与盘口数据(更详细的架构可以查看前面的链接),想要做到低延迟的同时保证资金链路的安全稳定是一个巨大的挑战。

回归正题,下面我们就来聊聊我是怎么遇到CPU缓存失效问题的(为什么对操作系统转变态度哈哈)。

行业内主流的货币交易系统都有这么一个功能,就是需要在页面实时显示盘口数据供用户查看,用户根据该盘口数据就可大致判断该币种的当前价值,从而做出对自己有利的判断(博主在这提醒大家:劳动是创造价值的唯一源泉),当某个用户进行下单时,盘口数据会发生相应的变化,因此,盘口数据的即时响应就显得非常重要。

所谓盘口:及买单数据与卖单数据。想象你去校园集市上买二手手机,你出价1000一台OppoA5,但是此时集市并没有卖OppoA5或者价格低于你的期望,所以你的该委托就会被放到盘口中(这是一个买单),现在又有一个人他打算出价500卖出一辆小米su7,该委托也被放到集市的盘口中(这是一个卖单)。盘口展示了一个物品目前大致的价值。

前面我们提到盘口的实时性很重要,那么在我的该系统中是如何架构的呢?

在这里插入图片描述

通过上面的架构我们可以发现,前端通过websocket技术与chan-service服务进行通信,chan-service会实时将数据推送到前端,那么问题来了,chan-service中的数据是从哪里来的呢?我们来看下面的设计图:

在这里插入图片描述

因为委托单的吃单挂单操作都是在撮合服务中完成的,所以撮合服务中总是有当前最新的数据。

我们来看撮合服务提供给外界获取盘口数据的接口代码:

@GetMapping("/plates")
    @Override
    public Map<String, List<DepthItemVo>> querySymbolDepth(@RequestParam String symbol) {
        for (EventHandler<OrderEvent> eventHandler : eventHandlers) {
            OrderEventHandler orderEventHandler = (OrderEventHandler) eventHandler;
            if(orderEventHandler.getSymbol().equals(symbol)) {
                HashMap<String, List<DepthItemVo>> symbolDepthMap = new HashMap<>();
                symbolDepthMap.put("bids",orderEventHandler.getOrderBooks().getBuyTradePlate().getItems());
                symbolDepthMap.put("asks",orderEventHandler.getOrderBooks().getSellTradePlate().getItems());
                return symbolDepthMap;
            }
        }
        return Collections.emptyMap();
    }

// 这里只展示部分字段内容
class OrderBooks {
      /**
     * 买入的盘口数据
     */
    private TradePlate buyTradePlate;

    /**
     * 卖出的盘口数据
     */
    private TradePlate sellTradePlate;
}

这段代码有很多的问题,但是这里我们先不讨论,我们先来看看他的工作流程,当接收到一个请求时,他会去遍历事件处理器(这是架构设计的问题,我将一个事件处理器分别与一个交易对进行绑定,这样做到多交易对间并发执行,而单个交易对内的撮合则是串行处理,做到高并发的同时减少并发安全问题),查询与当前交易对相关联的事件处理器,然后从其对应的账本OrderBooks中获取盘口数据,放入新建的Map容器中。

接下来我们再看看当订单进行撮合时,盘口数据是怎么更新的:

// OrderEventHandler对象	
@Override
    public void onEvent(OrderEvent orderEvent, long l, boolean b) throws Exception {
        Order order = (Order) orderEvent.getSource();
        if (!order.getSymbol().equals(this.symbol)) { // 不处理非当前交易对的事件
            return;
        }
        log.info("开始接收订单事件," + orderEvent);

        // 撮合(匹配订单/吃单
        MatchServiceFactory.getInstance(MatchStrategy.LIMIT_MATCH).match(orderBooks,(Order) orderEvent.getSource());

        log.info("订单处理完毕...");
    }

// MatchService的实现类
 /**
     * 吃单
     *
     * @param orderBooks 该交易对下的账本
     * @param order      当前需要处理的订单
     */
    @Override
    public void match(OrderBooks orderBooks, Order order) {
    
    	// 这里省略一些订单匹配的操作
        // 。。。。
        
        // 更新盘口
        TradePlate tradePlate = order.getOrderDirection() == OrderDirection.BUY.getCode() ?
          orderBooks.getBuyTradePlate() : orderBooks.getSellTradePlate();
    
    }

我们发现当新来一个订单时(用户下单请求),该订单会被其所在交易对的事件处理器OrderEventHandler所接收,然后该事件处理器对其进行撮合操作,撮合之后会对盘口数据造成变化,所以我们需要更新盘口数据。

以上便是该功能的大体流程,那问题在哪呢?我再进行测试的时候发现一个bug,就是当我在进行下单操作后,发现页面中的盘口数据并没有发生变化,观察浏览器控制台输出,发现从ws中订阅的消息也是没有变化(实际上是有的,只不过等个一两分钟,延迟太差了)。那这是为什么呢?经过调试发现,订单被创建后,很长一段时间并没有进入撮合操作,这是为什么?

说到这里,我们来回顾一下CPU的缓存一致性问题,现在的CPU一般是多核的,当你有一个线程A在核A运行,并且还有一个线程B在核B运行,此时线程A需要查询一个数据data,便将其放入到其CPU缓存中,然后线程B同时又要修改该数据data,此时核A中的缓存data必须失效并且同步到主内存中,这就导致了缓存一致性协议(MESI) 的额外开销。

那在我们的代码中,那里出现了这个问题。我们发现获取盘口数据的接口,他需要遍历OrderEventHandler进行获取盘口数据,然后OrderEventHandler又在撮合中修改了盘口数据,造成CPU缓存长期失效,并且获取盘口数据的接口定时任务执行的非常频繁,长期抢占CPU,导致撮合时间很短,从而导致虽然一直在获取盘口数据,但是的但却没有被正确的进行处理,盘口数据不变,过了很长一段时间,订单才撮合成功。

如何解决呢:

方向一:由于每次获取盘口数据都要遍历一次集合,所以我们将EventHandler不再使用数组存储,而是使用ConcurrentHashMap实现O(1)无锁高效查询。

方向二:我们在获取盘口数据的时候是直接获取的账本中的盘口,导致CPU缓存失效。因此我们将盘口数据保存在缓存中,撮合后写入缓存,获取盘口数据时直接查询缓存数据即可。

相关代码如下:

/**
 * 盘口数据缓存管理类
 */
@Slf4j
@Component
public class DepthCacheManager {

    // 创建缓存实例 - 多级缓存(本地内存 + Redis)
    @CreateCache(name = "market.depth.", expire = 2, timeUnit = TimeUnit.SECONDS,
            localLimit = 100, localExpire = 2,
            cacheType = CacheType.BOTH)  // 本地 + 远程两级缓存
    private Cache<String, DepthSnapshot> depthCache;
    // 符号到事件处理器的快速映射
    private final ConcurrentHashMap<String, OrderEventHandler> handlerMap = new ConcurrentHashMap<>();

    /**
     * 撮合线程调用:更新缓存
     */
    public void updateDepthSnapshot(String symbol, TradePlate buyPlate, TradePlate sellPlate) {
        DepthSnapshot snapshot = new DepthSnapshot(
                copyDepthItems(buyPlate.getItems()),  // 创建快照,避免后续修改
                copyDepthItems(sellPlate.getItems()),
                System.currentTimeMillis()
        );
        // JetCache 自动处理多级缓存
        depthCache.put(symbol, snapshot);
    }

    /**
     * 接口线程调用:查询缓存
     */
    public Map<String, List<DepthItemVo>> getSymbolDepth(String symbol) {
        try {
            DepthSnapshot snapshot = depthCache.get(symbol);
            if (snapshot != null) {
                Map<String, List<DepthItemVo>> result = new HashMap<>(2);
                result.put("bids", snapshot.getBids());
                result.put("asks", snapshot.getAsks());
                return result;
            }
        }catch (Exception e) {
            // 降级处理
            log.warn("Get depth cache failed: {}", e.getMessage());
        }
        return Collections.emptyMap();
    }

    /**
     * 注册事件处理器
     */
    public void registerHandler(OrderEventHandler handler) {
        handlerMap.put(handler.getSymbol(), handler);
    }

    /**
     * 获取事件处理器(如果需要直接访问)
     */
    public OrderEventHandler getHandler(String symbol) {
        return handlerMap.get(symbol);
    }




    /**
     * 深度拷贝,防止返回源对象引用,其他线程修改导致值变化
     */
    private List<DepthItemVo> copyDepthItems(List<DepthItemVo> original) {
        if (original == null) {
            return Collections.emptyList();
        }

        return original.stream()
                .map(this::copyDepthItem)
                .collect(Collectors.toList());
    }

    /**
     * 拷贝单个DepthItemVo
     */
    private DepthItemVo copyDepthItem(DepthItemVo original) {
        if (original == null) {
            return null;
        }

        return DepthItemVo.builder()
                .price(original.getPrice())
                .volume(original.getVolume())
                .build();
    }
}

以上还有很多处细节我会在之后继续讨论, 如果有误敬请纠正

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值