Lru і fifo кеші в scala без зайвих залежностей, записки програміста

Рідкісне серйозне додаток в наші дні обходиться без кешування чого-небудь. Який-небудь LRU кеш елементарно реалізується, наприклад, за допомогою хеш-таблиць і двусвязного списків. Але не факт, що ваше рішення буде відрізнятися особливою ефективністю, тому краще скористатися готовою реалізацією. Часто в якості такої реалізації рекомендують Guava або LruMap з twitter-util. Але у цих рішень є свої мінуси, зокрема, стек Twitter'а традиційно прив'язує вас до Scala 2.10, а Guava просто страшна.

На щастя, в Scala можна легко завести простенький кешік, що не протягуючи в проект зайвих залежностей. У світі Java широко відомий прийом з оверайдом методу removeEldestEntry класу LinkedHashMap, що дозволяє отримати LRU кеш. Прийом цей не надто елегантний, але все одно не такий страшний, як Guava. У перекладі на Scala він виглядає так:

import java. util. # 123; Map, LinkedHashMap # 125;

def makeCache # 91; K, V # 93; # 40; capacity. Int # 41 ;. Map # 91; K, V # 93; = # 123;
new LinkedHashMap # 91; K, V # 93; # 40; capacity, 0.7F, true # 41; # 123;
private val cacheCapacity = capacity

override def removeEldestEntry # 40; entry. Map. Entry # 91; K, V # 93; # 41 ;. Boolean = # 123;
this. size # 40; # 41;> this. cacheCapacity
# 125;
# 125;
# 125;

scala> case class Dog (name: String)
defined class Dog

scala> val cache = makeCache [String, Dog] (3)
cache: java.util.Map [String, Dog] = <>

scala> Option (cache.get ( "8"))
res2: Option [Dog] = Some (Dog (8))

scala> Option (cache.get ( "ololo"))
res4: Option [Dog] = None

Зверніть увагу, що cache.get обертається в Option, оскільки це джавний LinkedHashMap і метод get може повертати null. Але для примітивних типів (Int і подібні), повертається значення за замовчуванням:

scala> val cache = makeCache [String, Int] (3)
cache: java.util.Map [String, Int] = <>

scala> cache.get ( "test")
res5: Int = 0

scala> val cache = makeCache [String, Boolean] (3)
cache: java.util.Map [String, Boolean] = <>

scala> cache.get ( "test")
res6: Boolean = false

Тому вам може захотітися використовувати метод containsKey:

scala> cache.containsKey ( "test")
res7: Boolean = false

Слід також взяти до уваги, що якщо з вашим кешом може працювати більше одного потоку одночасно, ходіння в нього потрібно обертати в synchonized:

scala> cache.synchronized
res8: Boolean = false

Тепер розглянемо другий спосіб.

В Scala теж є LinkedHashMap, але у нього немає аргументу accessOrder, як в джавном LinkedHashMap, в результаті чого з його допомогою можна отримати тільки FIFO кеш:

import scala. collection. _

val cacheCapacity = 10000
val cacheMutex = new Object # 40; # 41;
var cache = mutable. LinkedHashMap # 91; String, Long # 93; # 40; # 41;

// запис в кеш
cacheMutex. synchronized # 123;
cache. put # 40; "Key". 100500 # 41;
cache = cache. drop # 40; cache. size - cacheCapacity # 41;
# 125;

// читання з кешу
cacheMutex. synchronized # 123; cache. get # 40; "Key" # 41; # 125;

Тут метод get повертає Option. Підчищається кеш «вручну» за допомогою методу drop. Причому цей метод повертає новий LinkedHashMap, тому для синхронізації доступу до кешу потрібен окремий об'єкт cacheMutex. Слід також взяти до уваги, що з міркувань ефективності вам може захотітися робити drop не на кожну запис в кеш:

// запис в кеш
cacheMutex. synchronized # 123;
cache. put # 40; "Key". 100500 # 41;
if # 40; cache. size> cacheCapacity # 41; # 123;
cache = cache. drop # 40; cache. size - # 40; cacheCapacity. toDouble * 0.75 # 41 ;. toInt # 41;
# 125;
# 125;

І взагалі, можливо, робити це в окремому потоці.

Само собою зрозуміло, є багато інших способів створити кеш в Scala, але ці два здаються мені найпростішими. Звичайно ж, в реальних додатках все набагато цікавіше, оскільки кеші не повинні роз'їжджатися з реальними даними або з кешами на інших нодах, кеш може знадобитися розбити на кілька, щоб він не ставав пляшковим горлечком, і так далі.

А як ви робите кеші?