Maison > Article > base de données > Partage de techniques à haute concurrence utilisant Redis et le cache local
Cet article vous apporte des connaissances pertinentes sur Redis. Il présente principalement les compétences d'utilisation du cache distribué et du cache local, y compris une introduction aux types de cache, aux divers scénarios d'utilisation et à la façon de l'utiliser. Enfin, il donnera un aperçu. regardez un cas pratique ci-dessous, j'espère qu'il sera utile à tout le monde.
Apprentissage recommandé : Tutoriel vidéo Redis
Comme nous le savons tous, l'objectif principal de la mise en cache est d'accélérer l'accès et de soulager la pression sur la base de données. Le cache le plus couramment utilisé est le cache distribué, tel que Redis. Face à la plupart des scénarios de concurrence ou aux situations où le trafic de certaines petites et moyennes entreprises n'est pas si élevé, l'utilisation de Redis peut fondamentalement résoudre le problème. Cependant, en cas de trafic élevé, vous devrez peut-être utiliser un cache local, tel que LoadingCache de Guava et ReloadableCache open source de Kuaishou.
Cette partie présentera les scénarios d'utilisation et les limitations de Redis, tels que LoadingCache de Guava et ReloadableCache open source de Kuaishou. Grâce à l'introduction de cette partie, vous pourrez savoir quel cache doit être utilisé dans quels scénarios commerciaux. . , et pourquoi.
Si nous parlons de manière générale du moment où utiliser Redis, alors il sera naturellement utilisé dans des endroits où le nombre de visites d'utilisateurs est trop élevé, accélérant ainsi l'accès et allégeant la pression sur la base de données. S'il est décomposé, il peut être divisé en problèmes à nœud unique et en problèmes non liés à un nœud unique.
Si une page a un nombre relativement élevé de visites d'utilisateurs, mais qu'ils n'accèdent pas à la même ressource. Par exemple, la page de détails de l'utilisateur a un nombre de visites relativement élevé, mais les données de chaque utilisateur sont différentes. Dans ce cas, il est évident que seul le cache distribué peut être utilisé. Si redis est utilisé, la clé est unique à l'utilisateur. clé, et la valeur correspond aux informations utilisateur.
Panne du cache causée par Redis.
Mais une chose à noter est que le délai d'expiration doit être défini et qu'il ne peut pas être configuré pour expirer en même temps. Par exemple, si un utilisateur dispose d'une page d'activité et que la page d'activité peut voir les données primées pendant l'activité de l'utilisateur, une personne imprudente peut définir le délai d'expiration des données utilisateur à la fin de l'activité, ce qui entraînera un problème unique (chaud)
Le problème de nœud unique fait référence au problème de concurrence d'un seul nœud de redis, car la même clé tombera sur le même nœud du cluster redis, donc si l'accès à cette clé est trop élevé , alors il y aura une concurrence sur ce nœud redis Danger caché, cette clé est appelée une touche de raccourci.
Si tous les utilisateurs accèdent à la même ressource, par exemple, la page d'accueil de l'application Xiao Ai affiche le même contenu à tous les utilisateurs (étape initiale), et le serveur renvoie le même gros json à h5, il faut évidemment utiliser un cache. Tout d'abord, nous examinons s'il est possible d'utiliser Redis. Puisque Redis a un problème en un seul point, si le trafic est trop important, toutes les demandes des utilisateurs atteindront le même nœud Redis, et il est nécessaire d'évaluer si le nœud peut supporter un tel problème. un flux important. Notre règle est que si le qps d'un seul nœud atteint mille niveaux, un problème en un seul point doit être résolu (même si redis prétend être capable de supporter des qps de cent mille niveaux), la méthode la plus courante est d'utiliser le cache local. . De toute évidence, le trafic sur la page d'accueil de l'application Xiaoai est inférieur à 100, il n'y a donc aucun problème à utiliser Redis. Scénarios d'utilisation et limitations de LoadingCachePour le problème de raccourci clavier mentionné ci-dessus, notre approche la plus directe consiste à utiliser le cache local, tel que le LoadingCache de goyave que vous connaissez le mieux, mais l'utilisation du cache local nécessite de pouvoir accepter une certaine quantité de données sales, car si vous mettez à jour la page d'accueil, le cache local ne sera pas mis à jour. Il rechargera uniquement le cache selon une certaine politique d'expiration. Cependant, dans notre scénario, tout va bien, car une fois la page d'accueil atteinte. est poussé en arrière-plan, il ne sera pas mis à jour. Même si cela change, il n'y a aucun problème. Vous pouvez définir l'expiration d'écriture sur une demi-heure et recharger le cache après une demi-heure. Nous pouvons accepter des données sales dans un laps de temps aussi court.Panne du cache causée par LoadingCache
Bien que le cache local soit fortement lié à la machine, bien que le niveau de code soit écrit pour expirer dans une demi-heure, en raison du temps de démarrage différent de chaque machine, le temps de chargement du cache est différent Le délai d'expiration est également différent, donc toutes les requêtes sur la machine ne demanderont pas la base de données après l'expiration du cache en même temps. Cependant, la pénétration du cache se produira également pour une seule machine. S'il y a 10 machines, chacune avec 1 000 qps, tant qu'un cache expire, ces 1 000 requêtes peuvent atteindre la base de données en même temps. Ce type de problème est en fait plus facile à résoudre, mais il est facile de l'ignorer. Autrement dit, lors de la configuration de LoadingCache, utilisez la méthode load-miss de LoadingCache au lieu de juger directement cache.getIfPresent()== null, puis de demander le db ; le premier ajoutera une machine virtuelle. Le verrouillage de couche garantit qu'une seule requête est envoyée à la base de données, résolvant ainsi parfaitement ce problème.
Cependant, s'il existe des exigences élevées en temps réel, telles que des activités fréquentes pendant une période donnée, je veux m'assurer que la page d'activité peut être mise à jour presque en temps réel, c'est-à-dire une fois que l'opérateur a configuré les informations d'activité. en arrière-plan, il doit être mis à jour côté C en temps réel. Pour afficher les informations d'activité de cette configuration en temps réel, l'utilisation de LoadingCache n'est certainement pas suffisante pour le moment.Pour les problèmes en temps réel mentionnés ci-dessus qui ne peuvent pas être résolus par LoadingCache, vous pouvez envisager d'utiliser ReloadableCache, qui est un framework de mise en cache local open source par Kuaishou. La plus grande fonctionnalité est qu'il prend en charge plusieurs machines. pour mettre à jour le cache en même temps. Supposons que nous modifiions les informations de la page d'accueil, puis la requête atteint la machine A. À ce moment, le ReloadableCache est rechargé, puis il enverra une notification aux autres machines écoutant la même chose. Le nœud zk mettra à jour le cache après avoir reçu la notification. L'exigence générale pour utiliser ce cache est de charger la totalité de la quantité de données dans le cache local, donc si la quantité de données est trop importante, cela exercera certainement une pression sur le gc et il ne pourra pas être utilisé dans ce cas. Étant donné que la page d'accueil de Xiao Ai a un statut et qu'il n'y a généralement que deux statuts en ligne, vous pouvez utiliser ReloadableCache pour charger uniquement les pages d'accueil de statut en ligne.
Trois types de caches ont été essentiellement introduits ici. Voici un résumé :
Une brève introduction à l'utilisation du cache
V get(K key, Callabled1336e9e686742fb22c1860557381905 loader)
;要么使用build的时候使用的是build(CacheLoader2ffe6bce707a67f84d4df961dbeb183a loader)
Utilisez Load-Miss car il est thread-safe. Si le cache échoue, plusieurs threads appelleront. Lors de l'obtention, un seul thread interrogera la base de données et les autres threads doivent attendre, ce qui signifie qu'il est thread-safe. LoadingCache<String, String> cache = CacheBuilder.newBuilder() .maximumSize(1000L) .expireAfterAccess(Duration.ofHours(1L)) // 多久不访问就过期 .expireAfterWrite(Duration.ofHours(1L)) // 多久这个key没修改就过期 .build(new CacheLoader<String, String>() { @Override public String load(String key) throws Exception { // 数据装载方式,一般就是loadDB return key + " world"; } }); String value = cache.get("hello"); // 返回hello world
<dependency> <groupId>com.github.phantomthief</groupId> <artifactId>zknotify-cache</artifactId> <version>0.1.22</version> </dependency>
vous devez lire la documentation, sinon cela ne fonctionnera pas. Si vous êtes intéressé, vous pouvez en écrire une vous-même.
public interface ReloadableCache<T> extends Supplier<T> { /** * 获取缓存数据 */ @Override T get(); /** * 通知全局缓存更新 * 注意:如果本地缓存没有初始化,本方法并不会初始化本地缓存并重新加载 * * 如果需要初始化本地缓存,请先调用 {@link ReloadableCache#get()} */ void reload(); /** * 更新本地缓存的本地副本 * 注意:如果本地缓存没有初始化,本方法并不会初始化并刷新本地的缓存 * * 如果需要初始化本地缓存,请先调用 {@link ReloadableCache#get()} */ void reloadLocal(); }
Le problème courant de panne/pénétration/avalanche du cache
Alors pouvons-nous concevoir un service RPC similaire aux verrous distribués mais plus fiable ? Lors de l'appel de la méthode get, ce service rpc garantit que la même clé est frappée sur le même nœud, utilise la synchronisation pour verrouiller, puis termine le chargement des données. Kuaishou fournit un framework appelé cacheSetter. Une version simplifiée est fournie ci-dessous, et elle est facile à mettre en œuvre en l'écrivant vous-même.
import com.google.common.collect.Lists; import org.apache.commons.collections4.CollectionUtils; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CountDownLatch; /** * @Description 分布式加载缓存的rpc服务,如果部署了多台机器那么调用端最好使用id做一致性hash保证相同id的请求打到同一台机器。 **/ public abstract class AbstractCacheSetterService implements CacheSetterService { private final ConcurrentMap<String, CountDownLatch> loadCache = new ConcurrentHashMap<>(); private final Object lock = new Object(); @Override public void load(Collection<String> needLoadIds) { if (CollectionUtils.isEmpty(needLoadIds)) { return; } CountDownLatch latch; Collection<CountDownLatch> loadingLatchList; synchronized (lock) { loadingLatchList = excludeLoadingIds(needLoadIds); needLoadIds = Collections.unmodifiableCollection(needLoadIds); latch = saveLatch(needLoadIds); } System.out.println("needLoadIds:" + needLoadIds); try { if (CollectionUtils.isNotEmpty(needLoadIds)) { loadCache(needLoadIds); } } finally { release(needLoadIds, latch); block(loadingLatchList); } } /** * 加锁 * @param loadingLatchList 需要加锁的id对应的CountDownLatch */ protected void block(Collection<CountDownLatch> loadingLatchList) { if (CollectionUtils.isEmpty(loadingLatchList)) { return; } System.out.println("block:" + loadingLatchList); loadingLatchList.forEach(l -> { try { l.await(); } catch (InterruptedException e) { e.printStackTrace(); } }); } /** * 释放锁 * @param needLoadIds 需要释放锁的id集合 * @param latch 通过该CountDownLatch来释放锁 */ private void release(Collection<String> needLoadIds, CountDownLatch latch) { if (CollectionUtils.isEmpty(needLoadIds)) { return; } synchronized (lock) { needLoadIds.forEach(id -> loadCache.remove(id)); } if (latch != null) { latch.countDown(); } } /** * 加载缓存,比如根据id从db查询数据,然后设置到redis中 * @param needLoadIds 加载缓存的id集合 */ protected abstract void loadCache(Collection<String> needLoadIds); /** * 对需要加载缓存的id绑定CountDownLatch,后续相同的id请求来了从map中找到CountDownLatch,并且await,直到该线程加载完了缓存 * @param needLoadIds 能够正在去加载缓存的id集合 * @return 公用的CountDownLatch */ protected CountDownLatch saveLatch(Collection<String> needLoadIds) { if (CollectionUtils.isEmpty(needLoadIds)) { return null; } CountDownLatch latch = new CountDownLatch(1); needLoadIds.forEach(loadId -> loadCache.put(loadId, latch)); System.out.println("loadCache:" + loadCache); return latch; } /** * 哪些id正在加载数据,此时持有相同id的线程需要等待 * @param ids 需要加载缓存的id集合 * @return 正在加载的id所对应的CountDownLatch集合 */ private Collection<CountDownLatch> excludeLoadingIds(Collection<String> ids) { List<CountDownLatch> loadingLatchList = Lists.newArrayList(); Iterator<String> iterator = ids.iterator(); while (iterator.hasNext()) { String id = iterator.next(); CountDownLatch latch = loadCache.get(id); if (latch != null) { loadingLatchList.add(latch); iterator.remove(); } } System.out.println("loadingLatchList:" + loadingLatchList); return loadingLatchList; } }
Mise en œuvre commerciale
import java.util.Collection; public class BizCacheSetterRpcService extends AbstractCacheSetterService { @Override protected void loadCache(Collection<String> needLoadIds) { // 读取db进行处理 // 设置缓存 } }Pénétration du cache
La solution est également très simple. La méthode d'obtention des données de la base de données (getByKey(K key)) doit donner une valeur par défaut.
Par exemple, j'ai une cagnotte avec une limite supérieure de 1W. Lorsque l'utilisateur termine la tâche, je lui envoie de l'argent, je l'enregistre à l'aide de Redis et je le connecte au tableau. la cagnotte en temps réel sur la page de la tâche. Au début de la tâche Il est évident que le montant de la cagnotte reste inchangé. Il n'y a aucune trace du montant émis dans redis et db, ce qui conduit à la nécessité de vérifier. la base de données à chaque fois. Dans ce cas, si les données ne sont pas trouvées dans la base de données, une valeur de 0 doit être mise en cache dans le cache.
Cela signifie qu'un grand nombre de pannes de cache centralisé ont frappé la base de données. Bien sûr, il doit s'agir de caches professionnels. En dernière analyse, il y a un problème avec l'écriture du code. Vous pouvez interrompre le délai d'expiration de l'invalidation du cache et ne pas le laisser échouer de manière centralisée.
Apprentissage recommandé : Tutoriel vidéo Redis
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!