Один из самых неприятных production-багов, с которыми я недавно столкнулся, был связан не с Kafka, не с гонками и не со сложной бизнес-логикой.
Сломало все обычное @Cacheable.
Сценарий был такой: один метод возвращал коллекцию и был обернут кешированием. Метод вызывался из разных мест. Где-то результат просто читали, а где-то эту же коллекцию дальше меняли — сортировали, удаляли элементы, дополняли.
Проблема в том, что после @Cacheable ты уже не каждый раз получаешь новый результат. Ты можешь получить тот же самый объект из кеша.
То есть в момент, когда один участок кода менял коллекцию, он по сути менял не локальные данные, а закешированный объект. Следующий вызов — из другого сервиса, другого запроса или вообще от другого пользователя — получал уже измененную версию.
Из-за этого баг выглядел почти мистически. Симптомы плавали, воспроизводимость была плохая, причина не читалась по логам. На поверхности все выглядело нормально: метод отработал, кеш сработал, коллекция на месте. Но внутри система уже жила с shared mutable state, который никто не ожидал увидеть.
Для меня главный вывод здесь такой: кеширование меняет не только производительность, но и контракт метода.
Если метод помечен @Cacheable, его результат лучше сразу считать immutable.
На практике это значит:
- возвращать
List.copyOf(...)илиSet.copyOf(...) - не отдавать наружу коллекции, которые кто-то потом может поменять
- в спорных местах явно делать защитную копию
Сам баг оказался не самым сложным технически. Сложнее всего было его найти, потому что проблема была не в самом кеше и не в конкретной мутации, а в их сочетании.
С тех пор у меня простое правило: если что-то кладется в кеш, это должно быть либо immutable, либо использоваться только как read-only.
Иначе вместе с оптимизацией можно незаметно закешировать и будущий баг.
Top comments (0)