DEV Community

Denis Toropov
Denis Toropov

Posted on

Когда @Cacheable кеширует не данные, а проблему

Один из самых неприятных production-багов, с которыми я недавно столкнулся, был связан не с Kafka, не с гонками и не со сложной бизнес-логикой.

Сломало все обычное @Cacheable.

Сценарий был такой: один метод возвращал коллекцию и был обернут кешированием. Метод вызывался из разных мест. Где-то результат просто читали, а где-то эту же коллекцию дальше меняли — сортировали, удаляли элементы, дополняли.

Проблема в том, что после @Cacheable ты уже не каждый раз получаешь новый результат. Ты можешь получить тот же самый объект из кеша.

То есть в момент, когда один участок кода менял коллекцию, он по сути менял не локальные данные, а закешированный объект. Следующий вызов — из другого сервиса, другого запроса или вообще от другого пользователя — получал уже измененную версию.

Из-за этого баг выглядел почти мистически. Симптомы плавали, воспроизводимость была плохая, причина не читалась по логам. На поверхности все выглядело нормально: метод отработал, кеш сработал, коллекция на месте. Но внутри система уже жила с shared mutable state, который никто не ожидал увидеть.

Для меня главный вывод здесь такой: кеширование меняет не только производительность, но и контракт метода.

Если метод помечен @Cacheable, его результат лучше сразу считать immutable.

На практике это значит:

  • возвращать List.copyOf(...) или Set.copyOf(...)
  • не отдавать наружу коллекции, которые кто-то потом может поменять
  • в спорных местах явно делать защитную копию

Сам баг оказался не самым сложным технически. Сложнее всего было его найти, потому что проблема была не в самом кеше и не в конкретной мутации, а в их сочетании.

С тех пор у меня простое правило: если что-то кладется в кеш, это должно быть либо immutable, либо использоваться только как read-only.

Иначе вместе с оптимизацией можно незаметно закешировать и будущий баг.

Top comments (0)