Maison  >  Article  >  base de données  >  Analyse d'un exemple d'utilisation de la commande Redis

Analyse d'un exemple d'utilisation de la commande Redis

WBOY
WBOYavant
2023-05-30 15:46:52655parcourir

Cause du problème

L'application dont l'éditeur est responsable est une application de gestion en arrière-plan. Le framework Shiro est utilisé pour la gestion des autorisations. Puisqu'il existe plusieurs nœuds, des sessions distribuées doivent être utilisées. , donc Redis est utilisé ici pour stocker les informations de session .

Étant donné que Shiro ne fournit pas directement le composant Session de stockage Redis, Afan a dû utiliser shiro-redis, un composant open source de Github.

Étant donné que le framework Shiro doit vérifier régulièrement si la session est valide, la couche inférieure de Shiro appellera SessionDAO#getActiveSessions pour obtenir toutes les informations de session. SessionDAO#getActiveSessions 获取所有的 Session 信息。

shiro-redis 正好继承 SessionDAO 这个接口,底层使用用keys 命令查找 Redis 所有存储的 Session key。

public Set<byte[]> keys(byte[] pattern){
    checkAndInit();
    Set<byte[]> keys = null;
    Jedis jedis = jedisPool.getResource();
    try{
        keys = jedis.keys(pattern);
    }finally{
        jedis.close();
    }
    return keys;
}

找到问题原因,解决办法就比较简单了,github 上查找到解决方案,升级一下 shiro-redis 到最新版本。

在这个版本,shiro-redis 采用 scan命令代替 keys,从而修复这个问题。

public Set<byte[]> keys(byte[] pattern) {
    Set<byte[]> keys = null;
    Jedis jedis = jedisPool.getResource();


    try{
        keys = new HashSet<byte[]>();
        ScanParams params = new ScanParams();
        params.count(count);
        params.match(pattern);
        byte[] cursor = ScanParams.SCAN_POINTER_START_BINARY;
        ScanResult<byte[]> scanResult;
        do{
            scanResult = jedis.scan(cursor,params);
            keys.addAll(scanResult.getResult());
            cursor = scanResult.getCursorAsBytes();
        }while(scanResult.getStringCursor().compareTo(ScanParams.SCAN_POINTER_START) > 0);
    }finally{
        jedis.close();
    }
    return keys;


}

虽然问题成功解决了,但是阿粉心里还是有点不解。

为什么keys 指令会导致其他命令执行变慢?

为什么Keys 指令查询会这么慢?

为什么Scan 指令就没有问题?

Redis 执行命令的原理

首先我们来看第一个问题,为什么keys 指令会导致其他命令执行变慢?

站在客户端的视角,执行一条命令分为三步:

  1. 发送命令

  2. 执行命令

  3. 返回结果

但是这仅仅客户端自己以为的过程,但是实际上同一时刻,可能存在很多客户端发送命令给 Redis ,而 Redis 我们都知道它采用的是单线程模型。

为了处理同一时刻所有的客户端的请求命令,Redis 内部采用了队列的方式,排队执行。

于是客户端执行一条命令实际需要四步:

  1. 发送命令

  2. 命令排队

  3. 执行命令

  4. 返回结果

由于 Redis 单线程执行命令,只能顺序从队列取出任务开始执行。

只要 3 这个过程执行命令速度过慢,队列其他任务不得不进行等待,这对外部客户端看来,Redis 好像就被阻塞一样,一直得不到响应。

所以使用 Redis 过程切勿执行需要长时间运行的指令,这样可能导致 Redis 阻塞,影响执行其他指令。

KEYS 原理

接下来开始回答第二个问题,为什么Keys 指令查询会这么慢?

回答这个问题之前,请大家回想一下 Redis 底层存储结构。

不太清楚朋友的也没关系,大家可以回看一下之前的文章「阿里面试官:HashMap 熟悉吧?好的,那就来聊聊 Redis 字典吧!」。

keys命令需要返回所有的符合给定模式 pattern 的  Redis 中键,为了实现这个目的,Redis 不得不遍历字典中 ht[0]哈希表底层数组,这个时间复杂度为 「O(N)」(N 为 Redis 中 key 所有的数量)。

即使 Redis 中的键数量很少,它仍然会有很快的执行速度。当Redis键的数量逐渐增多,达到百万、千万,甚至上亿级别时,它的执行速度会变得非常缓慢。

下面是阿粉本地做的一次实验,使用 lua 脚本往 Redis 中增加 10W 个 key,然后使用 keys 查询所有键,这个查询大概会阻塞十几秒的时间。

eval "for i=1,100000  do redis.call('set',i,i+1) end" 0

这里阿粉使用 Docker 部署 Redis,性能可能会稍差。

SCAN 原理

最后我们来看下第三个问题,为什么scan 指令就没有问题?

这是因为 scan命令采用一种黑科技-「基于游标的迭代器」

每次调用 scan 命令,Redis 都会向用户返回一个新的游标以及一定数量的 key。下次再想继续获取剩余的 key,需要将这个游标传入 scan 命令, 以此来延续之前的迭代过程。

简单来讲,scan 命令使用分页查询 redis 。

下面是一个 scan 命令的迭代过程示例:

scan 命令使用游标这种方式,巧妙将一次全量查询拆分成多次,降低查询复杂度。

虽然  scan 命令时间复杂度与 keys一样,都是 「O(N)」,但是由于 scan 命令只需要返回少量的 key,所以执行速度会很快。

最后,虽然scan 命令解决 keys

Et shiro-redis hérite simplement de l'interface SessionDAO La couche inférieure utilise la commande keys pour trouver tous les éléments. stocké dans la clé Redis code>Session.
    rrreee
  • Une fois que vous avez trouvé la cause du problème, la solution est relativement simple. Trouvez la solution sur github et mettez à niveau shiro-redis vers la dernière version.

    Dans cette version, shiro-redis utilise la commande scan au lieu de keys pour résoudre ce problème. #🎜🎜#rrreee#🎜🎜#Bien que le problème ait été résolu avec succès, Ah Fen était encore un peu confus. #🎜🎜##🎜🎜#Pourquoi la directive keys ralentit-elle l'exécution des autres commandes ? #🎜🎜##🎜🎜#Pourquoi la requête de la commande Keys est-elle si lente ? #🎜🎜##🎜🎜#Pourquoi n'y a-t-il aucun problème avec la commande Scan ? #🎜🎜##🎜🎜#Le principe d'exécution des commandes Redis#🎜🎜##🎜🎜#Tout d'abord, regardons la première question, pourquoi l'instruction keys provoque-t-elle l'exécution d'autres commandes Ralentissez? #🎜🎜##🎜🎜# Du point de vue du client, l'exécution d'une commande est divisée en trois étapes : #🎜🎜#
      #🎜🎜##🎜🎜#Envoyer la commande #🎜 🎜#
  • #🎜🎜##🎜🎜#Exécuter la commande#🎜🎜##🎜🎜##🎜🎜#Retour du résultat#🎜🎜##🎜 🎜# Mais ce n'est qu'un processus auquel le client pense, mais en fait, en même temps, de nombreux clients peuvent envoyer des commandes à Redis, et Redis, nous savons tous qu'il utilise un modèle à thread unique. #🎜🎜##🎜🎜#Afin de traiter toutes les commandes de requête client en même temps, Redis utilise une file d'attente en interne pour mettre en file d'attente l'exécution. #🎜🎜##🎜🎜#Le client a donc besoin de quatre étapes pour exécuter une commande : #🎜🎜#
    #🎜🎜##🎜🎜#Envoyer la commande#🎜🎜# #🎜🎜##🎜🎜#File d'attente des commandes#🎜🎜##🎜🎜##🎜🎜#Exécuter la commande#🎜🎜##🎜🎜##🎜🎜#Renvoyer le résultat # 🎜🎜#
#🎜🎜#Étant donné que Redis exécute les commandes dans un seul thread, il ne peut récupérer que séquentiellement les tâches de la file d'attente et démarrer l'exécution. #🎜🎜##🎜🎜# Tant que 3, ce processus exécute la commande trop lentement et les autres tâches de la file d'attente doivent attendre. Pour le client externe, il semble que Redis soit bloqué et ne répond jamais. #🎜🎜##🎜🎜#Ainsi, lorsque vous utilisez le processus Redis, n'exécutez pas les instructions qui doivent s'exécuter pendant une longue période. Cela pourrait entraîner le blocage de Redis et affecter l'exécution d'autres instructions. #🎜🎜##🎜🎜#KEYS Principe#🎜🎜##🎜🎜# Commençons par répondre à la deuxième question, pourquoi la requête de la commande Keys est-elle si lente ? #🎜🎜##🎜🎜#Avant de répondre à cette question, veuillez repenser à la structure de stockage sous-jacente de Redis. #🎜🎜##🎜🎜# Peu importe si vous ne le connaissez pas bien. Vous pouvez revenir sur l'article précédent « Alibaba Interviewer : Connaissez-vous HashMap ? D'accord, parlons du dictionnaire Redis ! #🎜🎜##🎜🎜# La commande keys doit renvoyer toutes les clés Redis qui correspondent au modèle pattern donné. Afin d'atteindre cet objectif, Redis doit parcourir le modèle. dictionnaire ht[0]Le tableau sous-jacent de la table de hachage, cette complexité temporelle est "O(N)" (N est le nombre de clés dans Redis). #🎜🎜##🎜🎜#Même si le nombre de clés dans Redis est petit, il aura toujours une vitesse d'exécution rapide. Lorsque le nombre de clés Redis augmente progressivement et atteint des millions, des dizaines de millions, voire des centaines de millions, sa vitesse d'exécution deviendra très lente. #🎜🎜##🎜🎜#Ce qui suit est une expérience réalisée par Ah Fen localement. Utilisez le script lua pour ajouter des clés 10W à Redis, puis utilisez keys pour interroger toutes les clés. une douzaine de secondes. #🎜🎜#rrreee
#🎜🎜#Ici, le fan utilise Docker pour déployer Redis, et les performances peuvent être légèrement pires. #🎜🎜#
#🎜🎜#Principe du SCAN#🎜🎜##🎜🎜#Enfin, regardons la troisième question, pourquoi n'y a-t-il aucun problème avec la commande scan ? #🎜🎜##🎜🎜#C'est parce que la commande scan utilise une technologie noire - "itérateur basé sur un curseur". #🎜🎜##🎜🎜#A chaque fois que la commande scan est appelée, Redis renverra un nouveau curseur et un certain nombre de clés à l'utilisateur. Si vous souhaitez continuer à obtenir les clés restantes la prochaine fois, vous devez passer ce curseur à la commande scan pour continuer le processus d'itération précédent. #🎜🎜##🎜🎜#Pour faire simple, la commande scan utilise la pagination pour interroger Redis. #🎜🎜##🎜🎜#Ce qui suit est un exemple du processus itératif de la commande scan : #🎜🎜##🎜🎜# La commande scan utilise des curseurs pour diviser intelligemment une requête complète en plusieurs fois, réduisant ainsi la complexité des requêtes. #🎜🎜##🎜🎜#Bien que la complexité temporelle de la commande scan soit la même que celle des clés, les deux sont "O(N)", mais en raison de La commande scan n'a besoin de renvoyer qu'un petit nombre de clés, la vitesse d'exécution sera donc très rapide. #🎜🎜##🎜🎜#Enfin, bien que la commande scan résolve le problème des clés, elle introduit également quelques autres défauts : #🎜🎜##🎜🎜## 🎜🎜 ##🎜🎜#Le même élément peut être retourné plusieurs fois, ce qui nécessite que notre application ajoute la fonction de gestion des éléments répétés. #🎜🎜#
  • Pendant le processus d'itération, les éléments qui sont ajoutés à Redis, ou les éléments qui sont supprimés, peuvent ou non être renvoyés.

  • Les défauts ci-dessus doivent être pris en compte dans notre développement.

    En plus de scan, redis dispose de plusieurs autres commandes pour l'itération incrémentielle : scan以外,redis 还有其他几个用于增量迭代命令:

    • sscan:用于迭代当前数据库中的数据库键,用于解决 smembers可能产生阻塞问题

    • hscan命令用于迭代哈希键中的键值对,用于解决 hgetall 可能产生阻塞问题。

    • zscan:命令用于迭代有序集合中的元素(包括元素成员和元素分值),用于产生 zrange

        #🎜 🎜## 🎜🎜#sscan : utilisé pour itérer les clés de la base de données dans la base de données actuelle, utilisé pour résoudre l'éventuel problème de blocage des smembers
    • #🎜 🎜 #
    La commande hscan est utilisée pour itérer les paires clé-valeur dans la clé de hachage et est utilisée pour résoudre l'éventuel problème de blocage de hgetall. #🎜🎜##🎜🎜##🎜🎜##🎜🎜#zscan : La commande est utilisée pour itérer les éléments de l'ensemble ordonné (y compris les membres des éléments et les scores des éléments), et est utilisée pour générer zrange peut provoquer des problèmes de blocage. #🎜🎜##🎜🎜##🎜🎜#

    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!

    Déclaration:
    Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer