Penjelasan Mendalam tentang Masalah Caching ApsaraDB for Redis

cache penetration
Cache penetration berarti data yang diminta oleh klien tidak ada di cache maupun database, sehingga cache tidak akan pernah berlaku, dan permintaan ini akan langsung mengenai database.
Jika pengguna jahat menggunakan banyak thread untuk mengakses data yang tidak ada secara bersamaan, semua permintaan ini akan mencapai database, yang kemungkinan besar akan menyebabkan database crash.
solusi
Cache objek kosong
Ide: Ketika pengguna meminta ID, baik ApsaraDB for Redis maupun database tidak memilikinya. Kami langsung meng-cache nilai null yang sesuai dengan ID ke ApsaraDB for Redis, sehingga saat pengguna berulang kali meminta ID ini, ApsaraDB for Redis dapat melakukan hit (hit null), dan tidak akan menanyakan database.
Keuntungan: mudah diimplementasikan, mudah dipelihara
kekurangan:

Konsumsi memori tambahan (dapat diselesaikan dengan menambahkan TTL)


- Dapat menyebabkan inkonsistensi jangka pendek (mengontrol waktu TTL dapat sedikit meringankan): Saat null di-cache, kami hanya mengatur nilai di database, dan kueri pengguna adalah null, padahal sebenarnya data tersebut ada di database, yang akan menyebabkan inkonsistensi (Dapat diselesaikan dengan secara otomatis menimpa data null sebelumnya saat memasukkan data)
Bloom filter
Sebuah lapisan bloom filter ditambahkan antara klien dan ApsaraDB for Redis. Saat pengguna mengakses, bloom filter akan menentukan apakah data ada. Jika tidak ada, permintaan akan ditolak langsung; jika ada, proses normal dapat dilanjutkan.
Bagaimana Bloom filter menentukan apakah data ada?

Bloom filter dapat dipahami secara sederhana sebagai array byte yang menyimpan bit biner. Saat perlu menentukan apakah data di database ada, data tidak langsung disimpan di bloom filter, melainkan nilai hash dihitung melalui algoritma hash, lalu hash ini dikonversi menjadi bit biner dan disimpan di Bloom filter. Saat menilai apakah data ada, posisi yang sesuai dapat diperiksa nilainya 0/1 (keberadaan ini bersifat statistik probabilitas, tidak 100% akurat, jadi jika dinyatakan tidak ada, belum tentu benar-benar tidak ada, sehingga masih ada risiko penetrasi)
Keuntungan: penggunaan memori lebih sedikit, tidak ada kunci redundan (biner)
kekurangan:

rumit untuk diimplementasikan
Ada kemungkinan kesalahan penilaian (belum tentu akurat)


Implementasi Java untuk cache objek kosong
/**
* Cache penetration
*
* @param id
* @return
*/
public Shop queryWithPassThrough(Long id) {
String key = CACHE_SHOP_KEY + id;
// 1. Query cache toko dari ApsaraDB for Redis
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2. Tentukan apakah ada
if (StrUtil.isNotBlank(shopJson)) {
// 3. Ada, kembalikan langsung
return JSONUtil.toBean(shopJson, Shop.class);
}
// Tentukan apakah hit adalah nilai null
if (shopJson != null) {
// kembalikan pesan error
return null;
}
// 4. Tidak ada, query database menurut id
Shop shop = getById(id);
// 5. tidak ada, kembalikan error
if (shop == null) {
// tulis nilai null ke ApsaraDB for Redis (cache penetration)
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
// 6. Ada, tulis ke ApsaraDB for Redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
// 7. kembalikan
return shop;
}
Cache Avalanche
Cache avalanche berarti sejumlah besar kunci cache menjadi tidak valid pada saat yang sama atau layanan ApsaraDB for Redis mati pada saat yang sama, mengakibatkan sejumlah besar permintaan mencapai database dan memberikan tekanan besar


solusi

Tambahkan nilai acak ke TTL dari kunci yang berbeda (untuk menyelesaikan masalah invalidasi simultan): Misalnya, saat melakukan cache warm-up, data di database perlu diimpor ke cache dalam batch sebelumnya. Karena data diimpor pada saat yang sama, nilai TTL dari data ini sama. Hal ini dapat menyebabkan data kedaluwarsa pada saat yang sama di beberapa titik, sehingga terjadi avalanche. Untuk menyelesaikan masalah ini, kita dapat menambahkan angka acak ke TTL saat mengimpor (misalnya, TTL adalah 30±1~5), sehingga waktu kedaluwarsa kunci-kunci ini akan tersebar dalam suatu periode waktu daripada tidak valid pada saat yang sama, sehingga menghindari terjadinya avalanche
Gunakan kluster ApsaraDB for Redis untuk meningkatkan ketersediaan layanan (selesaikan downtime ApsaraDB for Redis): Dengan bantuan mekanisme sentinel ApsaraDB for Redis, saat sebuah node mati, sentinel dapat secara otomatis memilih node lain untuk menggantikan node yang mati, dan master serta slave dapat menyinkronkan data untuk memastikan ketersediaan ApsaraDB for Redis yang tinggi
Tambahkan strategi downgrade dan pembatasan arus ke bisnis cache: seperti fast failure, penolakan layanan, dan mencegah permintaan diteruskan ke database
Tambahkan cache multi-level ke bisnis: browser dapat menambahkan cache (biasanya resource statis), server reverse-proxy Nginx dapat menambahkan cache, jika cache Nginx miss maka akan meminta ApsaraDB for Redis, jika cache ApsaraDB for Redis miss maka akan mencapai JVM, dan cache lokal juga dapat dibuat di dalam JVM, akhirnya baru mencapai database
cache breakdown
Masalah cache breakdown, juga dikenal sebagai masalah hot key, terjadi ketika sebuah kunci yang diakses dengan konkurensi tinggi dan memiliki proses rekonstruksi cache yang kompleks tiba-tiba kedaluwarsa. Akses permintaan yang sangat banyak akan memberikan dampak besar pada database dalam sekejap.
Rekonstruksi cache: Cache di ApsaraDB for Redis akan menjadi tidak valid setelah kedaluwarsa, dan perlu dikueri ulang dari database serta ditulis ke ApsaraDB for Redis setelah kedaluwarsa. Proses mengkueri dan membangun data dari database mungkin rumit, memerlukan kueri join multi-tabel, dll., dan akhirnya hasilnya di-cache. Proses ini mungkin memakan waktu lama (puluhan bahkan ratusan milidetik). Selama periode waktu ini, tidak ada cache di ApsaraDB for Redis, dan permintaan yang masuk akan miss sehingga mengakses database.


solusi
Mutex
Saat permintaan thread ditemukan miss, operasi penguncian dilakukan sebelum mengkueri database, dan kunci dilepaskan setelah menulis ke cache. Dengan cara ini, saat thread lain miss, kunci mutex juga akan diperoleh saat mengkueri database. Jika akuisisi gagal, thread akan tidur untuk sementara waktu dan kemudian mengkueri lagi.
Jelas, thread lain dapat memperoleh data hanya setelah data ditulis ke cache. Meskipun konsistensi dapat dijamin, performanya relatif buruk, dan dapat menyebabkan deadlock.


Implementasi Java


/**
* Dapatkan kunci
*
* @param key
* @return
*/
private boolean tryLock(String key) {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}


/**
* lepaskan kunci
*
* @param key
*/
private void unlock(String key) {
stringRedisTemplate.delete(key);
}


/**
* Mutex
*
* @param id
* @return
*/
public Shop queryWithMutex(Long id) {
String key = CACHE_SHOP_KEY + id;
// 1. Query cache toko dari ApsaraDB for Redis
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2. Tentukan apakah ada
if (StrUtil.isNotBlank(shopJson)) {
// 3. Ada, kembalikan langsung
return JSONUtil.toBean(shopJson, Shop.class);
}
// Tentukan apakah hit adalah nilai null
if (shopJson != null) {
// kembalikan pesan error
return null;
}

// 4. Implementasikan rekonstruksi cache
// 4.1 Dapatkan mutex
String lockKey = LOCK_SHOP_KEY + id;
Shop shop = null;
try {
boolean isLock = tryLock(lockKey);
// 4.2 Tentukan apakah akuisisi berhasil
if (!isLock) {
// 4.3 Gagal, tidur dan coba lagi
Thread.sleep(50);
// rekursif
return queryWithMutex(id);
}
// 4.4 Sukses, query database menurut id
shop = getById(id);
// simulasi delay rekonstruksi
Thread.sleep(200);
// 5. tidak ada, kembalikan error
if (shop == null) {
// tulis nilai null ke ApsaraDB for Redis (cache penetration)
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
// 6. Ada, tulis ke ApsaraDB for Redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 7. Lepaskan mutex
unlock(lockKey);
}
// 8. kembalikan
return shop;
}
Mari kita uji dengan JMeter, kirim 1000 permintaan, kita dapat melihat bahwa semua permintaan lolos, dan database hanya dikueri sekali



logical expiration
Sesuai namanya, ini tidak benar-benar kedaluwarsa, dapat dianggap sebagai tidak pernah kedaluwarsa. Saat kami meng-cache data di ApsaraDB for Redis, kami tidak mengatur TTL, melainkan menambahkan bidang waktu kedaluwarsa (bukan TTL, berdasarkan waktu saat ini + waktu kedaluwarsa, waktu yang dipertahankan secara logis) saat menyimpan data, sehingga thread mana pun dapat melakukan kueri dan mendapatkan hit, hanya perlu menentukan secara logis apakah telah kedaluwarsa
Seperti yang ditunjukkan pada gambar di bawah, jika thread 1 menemukan bahwa waktu logis telah kedaluwarsa saat mengkueri cache, ia perlu membangun kembali cache dan kemudian mendapatkan mutex. Thread lain yang mengalami miss akan langsung mengembalikan data yang kedaluwarsa alih-alih melakukan operasi rekonstruksi cache sendiri. Setelah rekonstruksi cache selesai, kunci dilepaskan, dan thread 1 langsung mengembalikan data yang kedaluwarsa. Saat thread lain juga miss, kegagalan untuk mendapatkan mutex akan langsung mengembalikan data kedaluwarsa. Meskipun performa dijamin, konsistensi tidak dapat dijamin.


Implementasi Java
/**
* Cache warm-up
*
* @param id
* @param expireSeconds waktu kedaluwarsa logis
*/
public void saveShop2Redis(Long id, Long expireSeconds) throws InterruptedException {
// 1. Query data toko
Shop shop = getById(id);
Thread.sleep(200);
// 2. Bungkus waktu kedaluwarsa logis
RedisData redisData = new RedisData();
redisData.setData(shop);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
// 3. Tulis ke ApsaraDB for Redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}

private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

/**
* Logical expiration
*
* @param id
* @return
*/
public Shop queryWithLogicalExpire(Long id) {
String key = CACHE_SHOP_KEY + id;
// 1. Query cache toko dari ApsaraDB for Redis
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2. Tentukan apakah ada
if (StrUtil.isBlank(shopJson)) {
// 3. Miss, kembalikan langsung
return null;
}
// 4. Hit, Anda perlu mendeserialisasi json ke objek terlebih dahulu
RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
LocalDateTime expireTime = redisData.getExpireTime();
// 5. Tentukan apakah telah kedaluwarsa
if (expireTime.isAfter(LocalDateTime.now())) {
// 5.1 Jika belum kedaluwarsa, kembalikan informasi toko langsung
return shop;
}
// 5.2 telah kedaluwarsa dan perlu dibangun kembali
// 6. Rekonstruksi cache
// 6.1 Dapatkan mutex
String lockKey = LOCK_SHOP_KEY + id;
boolean isLock = tryLock(lockKey);
// 6.2 Tentukan apakah kunci berhasil diperoleh
if (isLock) {
// 6.3 Sukses, buka thread independen untuk melakukan rekonstruksi cache
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
// bangun kembali cache
this.saveShop2Redis(id, 20L);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
// lepaskan kunci
unlock(lockKey);
}
});
}
// 6.4 Kembalikan informasi toko yang kedaluwarsa
return shop;
}

Artikel Terkait

Jelajahi Lebih Banyak Penawaran Spesial

  1. Short Message Service (SMS) & Layanan Email

    Paket 50.000 email mulai dari USD 1,99, 120 pesan singkat mulai dari hanya USD 1,00

phone Hubungi Kami
Hi, I'm Alibaba Cloud AI Assistant!
I can help with questions and solutions.