Comment supprimer atomiquement des clés correspondant à un modèle à l'aide de Redis


576

Dans ma base de données Redis, j'ai un certain nombre de prefix:<numeric_id>hachages.

Parfois, je veux tous les purger atomiquement. Comment faire cela sans utiliser un mécanisme de verrouillage distribué?


Salut Steve, Il y a un problème avec mon site Web, je l'ai ajouté à mon autre blog mind-geek.net/nosql/redis/delete-keys-specific-expiry-time , j'espère que cela vous aidera.
Gaurav Tewari

43
C'est un scénario tellement courant que j'aurais souhaité que l'équipe Redis envisage d'ajouter une commande native pour cela.
Todd Menier

De nos jours, vous pouvez simplement le faire avec Lua, voir ci-dessous.
Alexander Gladysh

3
@ToddMenier Je viens de suggérer, a récupéré ce raisonnement pour expliquer pourquoi cela ne se produira jamais: github.com/antirez/redis/issues/2042
Ray

1
Beaucoup de gens posent des questions connexes sur la façon de gérer un grand nombre de clés, des clés avec des caractères spéciaux, etc. J'ai créé une question distincte car nous avons ce problème maintenant et je ne pense pas que la réponse soit publiée sur cette question. Voici l'autre question: stackoverflow.com/questions/32890648/…
jakejgordon

Réponses:


431

À partir de redis 2.6.0, vous pouvez exécuter des scripts lua, qui s'exécutent de manière atomique. Je n'en ai jamais écrit, mais je pense que ça ressemblerait à quelque chose comme ça

EVAL "return redis.call('del', unpack(redis.call('keys', ARGV[1])))" 0 prefix:[YOUR_PREFIX e.g delete_me_*]

Avertissement : Comme le dit le document Redis , en raison de performances importantes, la keys commande ne doit pas être utilisée pour les opérations normales de production, cette commande est destinée au débogage et aux opérations spéciales. Lire la suite

Voir la documentation EVAL .


23
Remarque importante: cela échoue si vous avez plus de quelques milliers de clés correspondant au préfixe.
Nathan Osman

93
Celui-ci fonctionne pour un grand nombre de clés:EVAL "local keys = redis.call('keys', ARGV[1]) \n for i=1,#keys,5000 do \n redis.call('del', unpack(keys, i, math.min(i+4999, #keys))) \n end \n return keys" 0 prefix:*
sheerun

181
Ouch ... redis est beaucoup utilisé comme cache de clé / magasin simple. Cela semble del prefix:* être une opération fondamentale: /
Ray

5
@Ray franchement, si vous avez besoin de cette fonctionnalité, vous devez simplement partitionner les données par base de données numérique ou serveur, et utiliser flush / flushdb
Marc Gravell

9
Oui, il échoue si aucune clé ne correspond au modèle. Pour corriger cela, j'ai ajouté une clé par défaut:EVAL "return redis.call('del', 'defaultKey', unpack(redis.call('keys', ARGV[1])))" 0 prefix:*
manuelmhtr

706

Exécutez en bash:

redis-cli KEYS "prefix:*" | xargs redis-cli DEL

MISE À JOUR

Ok j'ai compris. Qu'en est-il de cette façon: stockez le préfixe incrémentiel supplémentaire actuel et ajoutez-le à toutes vos clés. Par exemple:

Vous avez des valeurs comme celle-ci:

prefix_prefix_actuall = 2
prefix:2:1 = 4
prefix:2:2 = 10

Lorsque vous devez purger des données, vous modifiez d'abord prefix_actuall (par exemple, définissez prefix_prefix_actuall = 3), de sorte que votre application écrira de nouvelles données dans les clés prefix: 3: 1 et prefix: 3: 2. Ensuite, vous pouvez prendre en toute sécurité les anciennes valeurs du préfixe: 2: 1 et du préfixe: 2: 2 et purger les anciennes clés.


14
Désolé, mais ce n'est pas une suppression atomique. Quelqu'un peut ajouter de nouvelles clés entre KEYS et DEL. Je ne veux pas les supprimer.
Alexander Gladysh

36
Les clés, qui seront créées après la commande KEYS, ne seront pas supprimées.
Casey du

6
J'avais juste besoin d'effacer certaines mauvaises clés, donc la première réponse de Casey était parfaite, sauf que je devais déplacer les clés en dehors des guillemets: redis-cli KEYS "prefix: *" | xargs redis-cli DEL
jslatts

19
La première réponse m'a également aidé. Une autre variante si vos clés redis contiennent des guillemets ou d'autres caractères qui redis-cli KEYS "prefix:*" | xargs --delim='\n' redis-cli DEL
gâchent les

18
Si vous avez des bases de données multibles (espaces clés), voici l'astuce: Disons que vous devez supprimer les clés dans db3:redis-cli -n 3 KEYS "prefix:*" | xargs redis-cli -n 3 DEL
Christoffer

73

Voici une version entièrement fonctionnelle et atomique d'une suppression générique implémentée dans Lua. Il fonctionnera beaucoup plus rapidement que la version xargs en raison de beaucoup moins de va-et-vient réseau, et il est complètement atomique, bloquant toutes les autres demandes contre redis jusqu'à ce qu'il se termine. Si vous souhaitez supprimer atomiquement des clés sur Redis 2.6.0 ou supérieur, c'est certainement la voie à suivre:

redis-cli -n [some_db] -h [some_host_name] EVAL "return redis.call('DEL', unpack(redis.call('KEYS', ARGV[1] .. '*')))" 0 prefix:

Ceci est une version de travail de l'idée de @ mcdizzle dans sa réponse à cette question. Le mérite de l'idée à 100% lui revient.

EDIT: Selon le commentaire de Kikito ci-dessous, si vous avez plus de clés à supprimer que de mémoire libre sur votre serveur Redis, vous rencontrerez l' erreur "trop ​​d'éléments à décompresser" . Dans ce cas, faites:

for _,k in ipairs(redis.call('keys', ARGV[1])) do 
    redis.call('del', k) 
end

Comme l'a suggéré Kikito.


10
Le code ci-dessus sera utilisé si vous avez un nombre important de clés (l'erreur est "trop ​​d'éléments à déballer"). Je recommande d'utiliser une boucle sur la partie Lua:for _,k in ipairs(redis.call('keys', KEYS[1])) do redis.call('del', k) end
kikito

@kikito, oui, si lua ne peut pas étendre la pile au nombre de clés que vous souhaitez supprimer (probablement en raison d'un manque de mémoire), vous devrez le faire avec une boucle for. Je ne recommanderais pas de faire cela sauf si vous le devez.
Eli

1
Lua unpacktransforme une table en une "liste de variables indépendantes" (d'autres langues appellent cela explode) mais le nombre maximum ne dépend pas de la mémoire système; il est fixé en lua à travers la LUAI_MAXSTACKconstante. Dans Lua 5.1 et LuaJIT, c'est 8000 et dans Lua 5.2, c'est 100000. L'option de boucle for est recommandée IMO.
kikito

1
Il est à noter que le script lua n'est disponible qu'à partir de Redis 2.6
wallacer

1
Toute solution basée sur Lua violera la sémantique EVALcar elle ne spécifie pas à l'avance les clés sur lesquelles elle fonctionnera. Il devrait fonctionner sur une seule instance, mais ne vous attendez pas à ce qu'il fonctionne avec Redis Cluster.
Kevin Christopher Henry

67

Avertissement: la solution suivante ne fournit pas d'atomicité.

À partir de la v2.8, vous voulez vraiment utiliser la commande SCAN au lieu de KEYS [1]. Le script Bash suivant illustre la suppression de clés par modèle:

#!/bin/bash

if [ $# -ne 3 ] 
then
  echo "Delete keys from Redis matching a pattern using SCAN & DEL"
  echo "Usage: $0 <host> <port> <pattern>"
  exit 1
fi

cursor=-1
keys=""

while [ $cursor -ne 0 ]; do
  if [ $cursor -eq -1 ]
  then
    cursor=0
  fi

  reply=`redis-cli -h $1 -p $2 SCAN $cursor MATCH $3`
  cursor=`expr "$reply" : '\([0-9]*[0-9 ]\)'`
  keys=${reply##[0-9]*[0-9 ]}
  redis-cli -h $1 -p $2 DEL $keys
done

[1] KEYS est une commande dangereuse qui peut potentiellement entraîner un DoS. Ce qui suit est une citation de sa page de documentation:

Avertissement: considérez KEYS comme une commande qui ne doit être utilisée que dans des environnements de production avec une extrême prudence. Il peut ruiner les performances lorsqu'il est exécuté sur de grandes bases de données. Cette commande est destinée au débogage et aux opérations spéciales, telles que la modification de la disposition de votre espace de clés. N'utilisez pas KEYS dans votre code d'application habituel. Si vous cherchez un moyen de trouver des clés dans un sous-ensemble de votre espace de clés, pensez à utiliser des ensembles.

MISE À JOUR: une doublure pour le même effet de base -

$ redis-cli --scan --pattern "*:foo:bar:*" | xargs -L 100 redis-cli DEL

9
Néanmoins, éviter les CLÉS est certainement considéré comme la meilleure pratique, c'est donc une excellente solution partout où des suppressions non atomiques sont possibles.
fatal_error

Cela a fonctionné pour moi; cependant, mes clés se trouvaient dans la base de données 1. J'ai donc dû ajouter -n 1à chaque redis-cliinvocation:redis-cli -n 1 --scan --pattern "*:foo:bar:*" | xargs -L 100 redis-cli -n 1 DEL
Rob Johansen

Notez que cela ne fonctionne pas si vos clés contiennent des caractères spéciaux
mr1031011

Recherche intéressante et précieuse ... Je me demande s'il y a un moyen de citer des choses pour les xargs ...
Itamar Haber

que fait -L 100 ??
Aparna

41

Pour ceux qui ont du mal à analyser d'autres réponses:

eval "for _,k in ipairs(redis.call('keys','key:*:pattern')) do redis.call('del',k) end" 0

Remplacez key:*:patternpar votre propre modèle et entrez-le dans redis-cliet vous êtes prêt à partir.

Crédit lisco de: http://redis.io/commands/del


37

J'utilise la commande ci-dessous dans redis 3.2.8

redis-cli KEYS *YOUR_KEY_PREFIX* | xargs redis-cli DEL

Vous pouvez obtenir plus d'aide concernant la recherche de modèles de clés à partir d'ici: - https://redis.io/commands/keys . Utilisez votre modèle de style glob pratique selon vos besoins comme *YOUR_KEY_PREFIX*ou YOUR_KEY_PREFIX??ou tout autre.

Et si l'un d'entre vous a intégré la bibliothèque PHP Redis, la fonction ci-dessous vous aidera.

flushRedisMultipleHashKeyUsingPattern("*YOUR_KEY_PATTERN*"); //function call

function flushRedisMultipleHashKeyUsingPattern($pattern='')
        {
            if($pattern==''){
                return true;
            }

            $redisObj = $this->redis;
            $getHashes = $redisObj->keys($pattern);
            if(!empty($getHashes)){
                $response = call_user_func_array(array(&$redisObj, 'del'), $getHashes); //setting all keys as parameter of "del" function. Using this we can achieve $redisObj->del("key1","key2);
            }
        }

Merci :)


23

La solution de @ mcdizle ne fonctionne pas, elle ne fonctionne que pour une seule entrée.

Celui-ci fonctionne pour toutes les clés avec le même préfixe

EVAL "for i, name in ipairs(redis.call('KEYS', ARGV[1])) do redis.call('DEL', name); end" 0 prefix*

Remarque: vous devez remplacer «préfixe» par votre préfixe de clé ...


2
utiliser lua est loooooot plus rapide que d'utiliser xargs, dans l'ordre 10 ^ 4.
deepak

22

Vous pouvez également utiliser cette commande pour supprimer les clés: -

Supposons qu'il existe de nombreux types de clés dans votre redis comme-

  1. «xyz_category_fpc_12»
  2. «xyz_category_fpc_245»
  3. «xyz_category_fpc_321»
  4. «xyz_product_fpc_876»
  5. «xyz_product_fpc_302»
  6. «xyz_product_fpc_01232»

Ex- ' xyz_category_fpc ' ici xyz est un nom de site , et ces clés sont liées aux produits et catégories d'un site de commerce électronique et générées par FPC.

Si vous utilisez cette commande comme ci-dessous-

redis-cli --scan --pattern 'key*' | xargs redis-cli del

OU

redis-cli --scan --pattern 'xyz_category_fpc*' | xargs redis-cli del

Il supprime toutes les clés comme « xyz_category_fpc » (supprimer 1, 2 et 3 clés). Pour supprimer les autres touches numériques 4, 5 et 6, utilisez ' xyz_product_fpc ' dans la commande ci-dessus.

Si vous souhaitez tout supprimer dans Redis , suivez ces commandes -

Avec redis-cli:

  1. FLUSHDB - Supprime les données de la base de données CURRENT de votre connexion.
  2. FLUSHALL - Supprime les données de TOUTES les bases de données.

Par exemple: - dans votre shell:

redis-cli flushall
redis-cli flushdb

3
Merci, mais la sortie de la tuyauterie redis-cli deln'est pas atomique.
Alexander Gladysh

13

Si vous avez de l'espace dans le nom des clés, vous pouvez l'utiliser dans bash:

redis-cli keys "pattern: *" | xargs -L1 -I '$' echo '"$"' | xargs redis-cli del

10

La réponse de @ itamar est excellente, mais l'analyse de la réponse ne fonctionnait pas pour moi, en particulier. dans le cas où aucune clé n'est trouvée dans un scan donné. Une solution éventuellement plus simple, directement depuis la console:

redis-cli -h HOST -p PORT  --scan --pattern "prefix:*" | xargs -n 100 redis-cli DEL

Cela utilise également SCAN, qui est préférable aux clés en production, mais n'est pas atomique.


8

J'ai juste eu le même problème. J'ai stocké les données de session pour un utilisateur au format:

session:sessionid:key-x - value of x
session:sessionid:key-y - value of y
session:sessionid:key-z - value of z

Ainsi, chaque entrée était une paire clé-valeur distincte. Lorsque la session est détruite, je voulais supprimer toutes les données de session en supprimant les clés avec le modèle session:sessionid:*- mais redis n'a pas une telle fonction.

Ce que j'ai fait: stocker les données de session dans un hachage . Je viens de créer un hachage avec l'identifiant de hachage session:sessionidpuis je pousse key-x, key-y, key-zdans ce hachage (ordre n'a pas d' importance pour moi) et si je ne avez pas besoin que hachage plus je fais juste DEL session:sessionidet toutes les données associées à cet identifiant de hachage est disparu. DELest atomique et accéder aux données / écrire des données dans le hachage est O (1).


Bonne solution, mais mes valeurs sont des hachages elles-mêmes. Et Redis stocke le hachage dans un autre hachage.
Alexander Gladysh

3
Cependant, les champs d'un hachage n'ont pas la fonctionnalité d'expiration, ce qui est parfois très utile.
Evi Song

pour moi, c'est la réponse la plus claire / la plus simple à ce jour
Sebastien H.

Un ensemble n'a-t-il pas beaucoup plus de sens?
Jack Tuck

5

Je pense que ce qui pourrait vous aider est le MULTI / EXEC / DISCARD . Bien qu'il ne soit pas équivalent à 100% des transactions , vous devriez pouvoir isoler les suppressions des autres mises à jour.


4
Mais je ne sais pas comment les utiliser ici. DEL est atomique par lui-même (ou du moins je pense). Et je ne peux pas obtenir de valeurs de KEYS jusqu'à ce que je fasse EXEC, donc je ne peux pas utiliser KEYS et DEL dans le même MULTI.
Alexander Gladysh

5

Pour info.

  • utilisant uniquement bash et redis-cli
  • ne pas utiliser keys(cela utilise scan)
  • fonctionne bien en mode cluster
  • pas atomique

Peut-être n'avez-vous besoin que de modifier les majuscules.

scan-match.sh

#!/bin/bash
rcli=“/YOUR_PATH/redis-cli" 
default_server="YOUR_SERVER"
default_port="YOUR_PORT"
servers=`$rcli -h $default_server -p $default_port cluster nodes | grep master | awk '{print $2}' | sed 's/:.*//'`
if [ x"$1" == "x" ]; then 
    startswith="DEFAULT_PATTERN"
else
    startswith="$1"
fi
MAX_BUFFER_SIZE=1000
for server in $servers; do 
    cursor=0
    while 
        r=`$rcli -h $server -p $default_port scan $cursor match "$startswith*" count $MAX_BUFFER_SIZE `
        cursor=`echo $r | cut -f 1 -d' '`
        nf=`echo $r | awk '{print NF}'`
        if [ $nf -gt 1 ]; then
            for x in `echo $r | cut -f 1 -d' ' --complement`; do 
                echo $x
            done
        fi
        (( cursor != 0 ))
    do
        :
    done
done

clear-redis-key.sh

#!/bin/bash
STARTSWITH="$1"

RCLI=YOUR_PATH/redis-cli
HOST=YOUR_HOST
PORT=6379
RCMD="$RCLI -h $HOST -p $PORT -c "

./scan-match.sh $STARTSWITH | while read -r KEY ; do
    $RCMD del $KEY 
done

Exécuter à l'invite bash

$ ./clear-redis-key.sh key_head_pattern

5

D'autres réponses peuvent ne pas fonctionner si votre clé contient des caractères spéciaux - Guide$CLASSMETADATA][1]par exemple. Enveloppant chaque clé dans des guillemets, elles seront correctement supprimées:

redis-cli --scan --pattern sf_* | awk '{print $1}' | sed "s/^/'/;s/$/'/" | xargs redis-cli del

2
Ce script fonctionne parfaitement, testé avec plus de 25000 clés.
Jordi

1
Vous pouvez également ajouter les guillemets simples dans awk en utilisant cette drôle d'expression `awk '{print"' "'"' "$ 1" '"' '' '"}' '
Roberto Congiu

3

Une version utilisant SCAN plutôt que KEYS (comme recommandé pour les serveurs de production) et --pipeplutôt que xargs.

Je préfère le pipe aux xargs car il est plus efficace et fonctionne lorsque vos clés contiennent des guillemets ou d'autres caractères spéciaux que votre shell essaie d'interpréter. La substitution d'expression régulière dans cet exemple encapsule la clé entre guillemets doubles et échappe à tout guillemet double à l'intérieur.

export REDIS_HOST=your.hostname.com
redis-cli -h "$REDIS_HOST" --scan --pattern "YourPattern*" > /tmp/keys
time cat /tmp/keys | perl -pe 's/"/\\"/g;s/^/DEL "/;s/$/"/;'  | redis-cli -h "$REDIS_HOST" --pipe

Cette solution a bien fonctionné pour moi, même sur des touches d'environ 7 m!
Danny

2

Ce n'est pas une réponse directe à la question, mais puisque je suis arrivé ici en cherchant mes propres réponses, je vais le partager ici.

Si vous avez des dizaines ou des centaines de millions de clés que vous devez faire correspondre, les réponses données ici rendront Redis non réactif pendant une durée significative (minutes?), Et risquent de planter en raison de la consommation de mémoire (assurez-vous, la sauvegarde en arrière-plan sera coup de pied au milieu de votre opération).

L'approche suivante est indéniablement laide, mais je n'en ai pas trouvé de meilleure. L'atomicité est hors de question ici, dans ce cas, l'objectif principal est de garder Redis en place et réactif 100% du temps. Cela fonctionnera parfaitement si vous avez toutes vos clés dans l'une des bases de données et que vous n'avez pas besoin de faire correspondre un modèle, mais ne pouvez pas utiliser http://redis.io/commands/FLUSHDB en raison de sa nature bloquante.

L'idée est simple: écrire un script qui s'exécute en boucle et utilise l'opération O (1) comme http://redis.io/commands/SCAN ou http://redis.io/commands/RANDOMKEY pour obtenir les clés, vérifie si elles correspondre au modèle (si vous en avez besoin) et http://redis.io/commands/DEL les un par un.

S'il y a une meilleure façon de le faire, faites-le moi savoir, je mettrai à jour la réponse.

Exemple d'implémentation avec randomkey dans Ruby, comme tâche de râteau, un substitut non bloquant de quelque chose comme redis-cli -n 3 flushdb:

desc 'Cleanup redis'
task cleanup_redis: :environment do
  redis = Redis.new(...) # connection to target database number which needs to be wiped out
  counter = 0
  while key = redis.randomkey               
    puts "Deleting #{counter}: #{key}"
    redis.del(key)
    counter += 1
  end
end

2

Il est simple à mettre en œuvre via la fonctionnalité "Supprimer la branche" dans FastoRedis , sélectionnez simplement la branche que vous souhaitez supprimer.

entrez la description de l'image ici


Est-ce que cela le fait atomiquement? Et comment cela aide-t-il à le faire dans le code ?
Matthieu lu

2

Veuillez utiliser cette commande et essayez:

redis-cli --raw keys "$PATTERN" | xargs redis-cli del

Pas atomique et reproduit d'autres réponses.
Matthieu lu

1

J'ai essayé la plupart des méthodes mentionnées ci-dessus mais elles n'ont pas fonctionné pour moi, après quelques recherches, j'ai trouvé ces points:

  • si vous avez plus d'une base de données sur redis, vous devez déterminer la base de données à l'aide -n [number]
  • si vous avez quelques utiliser les touches delmais s'il y a des milliers ou des millions de clés , il est préférable d'utiliser unlinkparce que unlink est non bloquant tout del bloque, pour plus d' informations , visitez cette page unlink vs del
  • keyssont également comme del et bloque

j'ai donc utilisé ce code pour supprimer des clés par modèle:

 redis-cli -n 2 --scan --pattern '[your pattern]' | xargs redis-cli -n 2 unlink 

0

suppression de la masse atomique du pauvre?

peut-être pourriez-vous les régler tous sur EXPIREAT la même seconde - comme quelques minutes dans le futur - puis attendre ce moment et les voir tous "s'autodétruire" en même temps.

mais je ne sais pas vraiment à quel point ce serait atomique.


0

À partir de maintenant, vous pouvez utiliser un client redis et effectuer un premier SCAN (prend en charge la correspondance de modèles) puis SUPPRIMER chaque clé individuellement.

Cependant, il y a un problème sur redis github officiel pour créer un patter-matching-del ici , allez lui montrer un peu d'amour si vous le trouvez utile!


-1

Je soutiens toutes les réponses liées à l'utilisation d'un outil ou à l'exécution de l'expression Lua.

Une option de plus de mon côté:

Dans nos bases de données de production et de pré-production, il y a des milliers de clés. De temps en temps, nous devons supprimer certaines clés (par un masque), les modifier selon certains critères, etc. Bien sûr, il n'y a aucun moyen de le faire manuellement à partir de la CLI, en particulier avec le partage (512 dbs logiques dans chaque physique).

Dans ce but, j'écris un outil client java qui fait tout ce travail. En cas de suppression de clés, l'utilitaire peut être très simple, il n'y a qu'une seule classe:

public class DataCleaner {

    public static void main(String args[]) {
        String keyPattern = args[0];
        String host = args[1];
        int port = Integer.valueOf(args[2]);
        int dbIndex = Integer.valueOf(args[3]);

        Jedis jedis = new Jedis(host, port);

        int deletedKeysNumber = 0;
        if(dbIndex >= 0){
            deletedKeysNumber += deleteDataFromDB(jedis, keyPattern, dbIndex);
        } else {
            int dbSize = Integer.valueOf(jedis.configGet("databases").get(1));
            for(int i = 0; i < dbSize; i++){
                deletedKeysNumber += deleteDataFromDB(jedis, keyPattern, i);
            }
        }

        if(deletedKeysNumber == 0) {
            System.out.println("There is no keys with key pattern: " + keyPattern + " was found in database with host: " + host);
        }
    }

    private static int deleteDataFromDB(Jedis jedis, String keyPattern, int dbIndex) {
        jedis.select(dbIndex);
        Set<String> keys = jedis.keys(keyPattern);
        for(String key : keys){
            jedis.del(key);
            System.out.println("The key: " + key + " has been deleted from database index: " + dbIndex);
        }

        return keys.size();
    }

}

-1

La commande ci-dessous a fonctionné pour moi.

redis-cli -h redis_host_url KEYS "*abcd*" | xargs redis-cli -h redis_host_url DEL

-3

Spring RedisTemplate lui-même fournit la fonctionnalité. RedissonClient dans la dernière version a déprécié la fonctionnalité "deleteByPattern".

Set<String> keys = redisTemplate.keys("geotag|*");
redisTemplate.delete(keys);

2
J'ai mis à jour l'exemple de code Redisson. Votre code n'est pas en approche atomique comme le fait Redisson. De nouvelles clés peuvent apparaître entre les invocations de méthodes keyset delete.
Nikita Koksharov
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.