Написание высокопроизводительного кэша

Я написал симулятор фондового рынка, который использует ConcurrentHashMap в качестве кеша.

Кэш содержит около 75 элементов, но они очень быстро обновляются и извлекаются (~ 500 раз в секунду).

Вот что я сделал:

Тема 1:

Подключен к внешней системе, которая предоставляет мне потоковые котировки для данного символа акции.

Поток 2 (поток обратного вызова):

Ожидает, пока данные не будут доставлены ему внешней системой. Как только он получает данные, он анализирует их, создает неизменяемый объект DataEntry, кэширует его и отправляет сигнал в thread3.

Поток 3 (потребительский поток): после получения сигнала извлеките DataEntry из кеша и используйте его. (Часть задачи состоит в том, чтобы не позволить потоку 2 отправлять данные непосредственно в поток 3).

public final class DataEntry{

      private final String field1;
      private final String field2;
      //...
      private final String field25;

      // Corresponding setters and getters

}

public final class Cache{

        private final Map<String, DataEntry> cache;

        public Cache( ){
           this.cache = new ConcurrentHashMap<String, DataEntry> ( 65, 0.75, 32 );
        }

        // Methods to update and retrieve DataEntry from the cache.
}

Запустив его через профилировщик, я заметил, что создаю много объектов DataEntry. И поэтому рай заполняется очень быстро.

Итак, я думаю немного изменить дизайн:

a) Создание изменяемого класса DataEntry.

b) Предварительное заполнение кеша пустыми DataEntry объектами.

c) Когда появится обновление, извлеките объект DataEntry с карты и заполните поля.

Таким образом, количество объектов DataEntry будет постоянным и равным количеству элементов.

Мои вопросы:

a) Есть ли в этом дизайне какие-либо проблемы параллелизма, которые я мог создать, сделав DataEntry изменяемым.

b) Можно ли еще что-нибудь сделать для оптимизации кеша?

Спасибо.


person CaptainHastings    schedule 22.12.2011    source источник
comment
Вы можете получить доступ к ConcurrentHasMap более миллиона раз в секунду, и это не окажет большого влияния, если вы обращаетесь к нему только 500 раз в секунду.   -  person Peter Lawrey    schedule 22.12.2011
comment
Я бы увеличил размер раздела только в том случае, если вы ожидаете, что около 16 или более ядер будут одновременно обращаться к карте. Если вы используете менее 4 ядер для одновременного доступа к карте (и не делаете ничего другого), вряд ли это будет иметь большое значение.   -  person Peter Lawrey    schedule 22.12.2011
comment
Почему быстрое заполнение Эдема является проблемой? Вы действительно испытываете проблемы из-за Eden GC?   -  person kdgregory    schedule 23.12.2011
comment
Выделение короткоживущих объектов в Java чрезвычайно дешево, так что меня это не беспокоит. Вы можете повторно использовать записи данных, но вам все равно придется вставлять их заново, поэтому я сомневаюсь, что это будет иметь большое значение.   -  person Voo    schedule 23.12.2011


Ответы (4)


Я бы не беспокоился о скорости ConcurrentHashMap

Map<Integer, Integer> map = new ConcurrentHashMap<>();
long start = System.nanoTime();
int runs = 200*1000*1000;
for (int r = 0; r < runs; r++) {
    map.put(r & 127, r & 127);
    map.get((~r) & 127);
}
long time = System.nanoTime() - start;
System.out.printf("Throughput of %.1f million accesses per second%n",
        2 * runs / 1e6 / (time / 1e9));

отпечатки

Throughput of 72.6 million accesses per second

Это намного превышает скорость доступа, которую вы, кажется, используете.

Если вы хотите уменьшить количество мусора, вы можете использовать изменяемые объекты и примитивы. По этой причине я бы не стал использовать String (поскольку у вас гораздо больше строк, чем записей данных)

person Peter Lawrey    schedule 22.12.2011
comment
Спасибо, Питер. Если можно, еще один вопрос: если я сделаю DataEntry изменяемым, мне придется заблокировать карту, пока я добавляю ее на карту, нет? Или я могу обернуть изменяемый DataEntry с помощью AtomicReference? Ура (кстати, люблю ваш блог). - person CaptainHastings; 23.12.2011
comment
Для высокопараллельных систем параллельная хэш-карта по умолчанию не будет хорошей идеей, но я согласен, что из описания это тоже маловероятно (я бы рассмотрел здесь что-либо с более чем несколькими дюжинами высокопараллельных потоков YMMV). Судя по описанию, хэш-карта в любом случае кажется странным выбором. - person Voo; 23.12.2011
comment
Если вы используете изменяемый DataEntry, я бы добавил его только один раз (желательно при запуске). Приветствия о блоге. ;) - person Peter Lawrey; 23.12.2011

Похоже, вы используете ConcurrentHashMap, когда вам на самом деле нужно что-то вроде параллельной очереди — скажем, LinkedBlockingQueue?

person Matt Ball    schedule 22.12.2011
comment
Привет, на самом деле мне нужна карта, так как я хочу, чтобы заинтересованные стороны опрашивали новые данные, а не передавали их им. - person CaptainHastings; 23.12.2011
comment
Хм... Я не уверен, что карта по-прежнему является лучшей структурой данных для этого. Думали ли вы о простом использовании отдельных очередей для каждой заинтересованной стороны? - person Matt Ball; 23.12.2011

  • а. Да, это так. Изменяемые объекты DataEntry могут быть обновлены незаметно для читателя, что приведет к несогласованным состояниям.
  • б. Да, вы можете: сделать изменяемый DataEntryCache, который по запросу возвращает неизменяемый DataEntry. Таким образом, вы будете создавать новые объекты DataEntry при чтении, а не при записи. DataEntryCache может внутренне кэшировать неизменяемый DataEntry, который он создает и возвращает, и аннулировать этот "кэш" при изменяющихся вызовах.

Изменить: я предполагаю, что причина, по которой вы кэшируете (в отличие от создания очереди между потоками 2 и 3), заключается в том, что поток потребителя может читать другие записи в дополнение к той, из которой поток 2 отправляет уведомление. Если это предположение неверно, вам может вообще не понадобиться кеш.

person Sergey Kalinichenko    schedule 22.12.2011
comment
+1, хотя ваш ответ на b выглядит для меня как состояние гонки - person kdgregory; 23.12.2011
comment
Привет, мне нужна карта, чтобы я мог попросить заинтересованные стороны опросить новые данные, а не отправлять их им. - person CaptainHastings; 23.12.2011
comment
@kdgregory эту операцию необходимо синхронизировать внутри DataEntryCache. - person Sergey Kalinichenko; 23.12.2011

a) В моем коде создание объекта часто оказывается узким местом, поэтому я думаю, что ваша собственная идея повторного использования DataEntry объектов тоже стоит реализовать. Однако, как заметил kdgregory, простая перезапись текущих элементов приведет к чтению противоречивых записей. Таким образом, при обновлении записи вместо этого запишите в новую или, если доступна, повторно используемую запись бездействия (скажем, простоя в течение нескольких минут) и поместите ее на карту. После добавления новой записи на карту поместите старую запись в какой-нибудь список незанятых. Чтобы быть полностью безопасным, потоки чтения не должны иметь доступ к DataEntry, предоставленному кешем, после, например, 1 минута. Если потоки могут заблокироваться, им следует скопировать объекты DataEntry, возможно, повторно используя для этого собственные объекты.

b) Текущий проект является модульным, но включает множество переключений контекста, поскольку потоки отражают модули. Я бы попробовал дизайн, в котором один запрос обслуживается от начала до завершения одним потоком. Запрос может быть полной обработкой нового объекта DataEntry. Для этого используются шаблоны параллельного проектирования Leader/Follower и Полусинхронный/Полуасинхронный.

person Peter G.    schedule 22.12.2011
comment
Я не вижу очевидных проблем с параллелизмом — что произойдет, если один поток считывает значения из объекта, а другой записывает значения? - person kdgregory; 23.12.2011
comment
@kdgegory, спасибо. Это было очевидно ;). Я обновил свой ответ. - person Peter G.; 23.12.2011