Redisで使用される基本的なデータ構造は何ですか?


305

私は決定的なリストで2つの質問に答えようとしています:

  1. Redisで使用される基本的なデータ構造は何ですか?
  2. そして、各タイプの主な長所/短所/ユースケースは何ですか?

したがって、Redisリストは実際にはリンクリストで実装されていることを読みました。しかし、他のタイプの場合は、情報を掘り下げることができません。また、誰かがこの質問に出くわして、さまざまなデータ構造を変更またはアクセスすることの長所と短所の高レベルの概要を持っていない場合、参照するために特定のタイプ最適に使用するタイミングの完全なリストも持っています。

具体的には、文字列、リスト、セット、zset、ハッシュなど、すべてのタイプの概要を説明します。

ああ、私はこれらの記事、とりわけこれまでの記事を見てきました。


7
サーバーの使い方は雑学ですか?あるプログラミング構造を別のプログラミング構造の上でいつ使用するかをどのように決定しますか?さまざまな用途にさまざまなタイプを使用するため、これはプログラミングに直接適用できます。
Homer6 2012年

2
サーバーの使い方は必ずしも雑学ではありませんが、それは話題外です-そして、それはあなたが尋ねたものではありません。特定の目的のためにどのデータ構造を使用するかは局所的ですが、それはあなたが尋ねたものでもありません。Redisで使用されたの雑学であり、特定の状況で特定の構造を使用した理由についての追加の推論がない場合-その時点で、すでに話題になっていると私が言ったことに戻り、Redisが行うことは無関係。
Jerry Coffin 2012年

5
トピックには、「データ構造とは何か、いつ異なるタイプを使用する必要があるか」と明記されています。それはどのように話題外ですか?リンクされたリスト、ハッシュ、配列について学ぶことはプログラミングとは無関係だと言っていますか?なぜなら、それらは直接関連していると私は主張するからです。特に、主にパフォーマンスを目的として設計されたサーバーではそうです。また、間違った選択は、あるアプリケーションから次のアプリケーションへのパフォーマンスが大幅に低下する可能性があるため、これらは適切です。
Homer6 2012年

19
antirezの答えはこの質問を引き換えます。プログラマーとredisユーザーの不利益に近づきます。
John Sheehan

75
@JerryCoffinはすべての敬意を払って、ソフトウェア開発ツールであり、ソフトウェア開発ツールについて質問することはしっかりと話題になっています。「ソースから回答を得ることができる」という事実は、理由はあまりありません...ソースから回答を得るのに何時間もかかります。また、redisは非常に広く使用されているため、この質問はあまりローカライズされていません。Stack Overflowは、プログラミングについて学び、非常に人気のあるプログラミングツールがどのデータ構造を使用しているかを尋ねることで、その目標に貢献します。要するに、私はこの質問を閉じる理由を見つけることができません。
ジョエルスポルスキー

回答:


612

私はあなたの質問に答えようとしますが、最初は奇妙に見えるかもしれないものから始めます:Redisの内部に興味がない場合は、データ型が内部でどのように実装されているかを気にする必要はありません。これは単純な理由によるものです。すべてのRedis操作では、ドキュメントに時間の複雑さが見つかります。操作のセットと時間の複雑さがある場合、必要な他の唯一のことは、メモリ使用量に関する手掛かりです(そしてデータに応じて変化する可能性のある多くの最適化を行います。これらの後者の数値を取得する最良の方法は、いくつかの簡単な実世界テストを行うことです)。

しかし、あなたが尋ねたので、ここにすべてのRedisデータタイプの基本的な実装があります。

  • 文字列は、私たちがアペンド操作で配分のために(漸近的に言えば)を払っていないように、Cの動的な文字列ライブラリを使用して実装されています。このように、例えば、2次の振る舞いをする代わりに、O(N)を追加します。
  • リストはリンクリストで実装されます。
  • セットハッシュはハッシュテーブルで実装されています。
  • ソートされたセットは、スキップリスト(特異なタイプのバランスツリー)を使用して実装されます

ただし、リスト、セット、およびソート済みセットのアイテム数が少なく、最大値のサイズが小さい場合は、別のはるかにコンパクトなエンコーディングが使用されます。このエンコーディングはタイプによって異なりますが、すべての操作でO(N)スキャンを強制することが多い、データのコンパクトなblobであるという特徴があります。このフォーマットは小さなオブジェクトにのみ使用するため、これは問題ではありません。小さなO(N)blobのスキャンはキャッシュに気づかないため、実際には非常に高速であり、要素が多すぎると、エンコーディングは自動的にネイティブエンコーディング(リンクリスト、ハッシュなど)に切り替えられます。

しかし、あなたの質問は本当に内部についてだけではなく、あなたのポイントは何を達成するためどのタイプを使用するかでしたか?

文字列

これはすべてのタイプの基本タイプです。これは4つのタイプの1つですが、リストは文字列のリスト、セットは文字列のセットなどなので、複合タイプの基本タイプでもあります。

Redis文字列は、HTMLページを保存するすべての明らかなシナリオで、既にエンコードされたデータの変換を避けたい場合にも適しています。たとえば、JSONまたはMessagePackを使用している場合、オブジェクトを文字列として格納するだけで済みます。Redis 2.6では、Luaスクリプトを使用して、この種のオブジェクトサーバー側を操作することもできます。

文字列のもう1つの興味深い使用法は、ビットマップ、および一般にバイトのランダムアクセス配列です。これは、Redisがコマンドをエクスポートして、ランダムなバイト範囲または単一ビットにアクセスするためです。たとえば、こちらの優れたブログ投稿(Redisを使用したFast Easyリアルタイムメトリック)を確認してください。

リスト

リストは、リストの極端な部分(テールの近くまたはヘッドの近く)のみに触れる可能性がある場合に適しています。ランダムアクセスが遅いO(N)であるため、リストはページ分割にあまり適していません。したがって、リストの適切な使用法は、単純なキューとスタック、またはアイテムのリングを「回転」するために同じソースと宛先でRPOPLPUSHを使用してループでアイテムを処理することです。

リストは、通常上部または下部のアイテムのみにアクセスするNアイテムのキャップされたコレクションを作成する場合や、Nが小さい場合にも適しています。

セット

セットは順序付けされていないデータコレクションであるため、アイテムのコレクションがあるたびに適切であり、コレクションの存在またはサイズを非常に高速にチェックすることが非常に重要です。セットのもう1つの優れた点は、ランダム要素のピークまたはポップのサポート(SRANDMEMBERおよびSPOPコマンド)です。

セットは、「ユーザーXの友達とは何ですか?」などの関係を表すのにも適しています。など。しかし、この種のものに適した他のデータ構造は、後で説明するようにソートされたセットです。

セットは、交差、ユニオンなどの複雑な操作をサポートしているため、データがあり、そのデータに対して変換を実行して出力を取得したい場合、これは「計算」方法でRedisを使用するのに適したデータ構造です。

小さなセットは非常に効率的な方法でエンコードされます。

ハッシュ

ハッシュは、フィールドと値で構成されるオブジェクトを表すのに最適なデータ構造です。ハッシュのフィールドは、HINCRBYを使用してアトミックに増分することもできます。ユーザー、ブログ投稿、その他の種類のアイテムなどのオブジェクトがある場合、JSONなどの独自のエンコーディングを使用したくない場合は、ハッシュが適しています。

ただし、小さなハッシュはRedisによって非常に効率的にエンコードされ、非常に高速な方法で個々のフィールドをアトミ​​ックにGET、SET、またはインクリメントするようにRedisに要求できることに注意してください。

ハッシュは、参照を使用して、リンクされたデータ構造を表すためにも使用できます。たとえば、コメントのlamernews.com実装を確認してください。

ソートされたセット

並べ替えられたセットは、リストの他に、順序付けられた要素を維持するため唯一のデータ構造です。ソートされたセットを使用して、いくつかのクールなことができます。たとえば、Webアプリケーションにあらゆる種類のトップサムシングリストを含めることができます。スコア別の上位ユーザー、ページビュー別の上位投稿、上位何でも、1つのRedisインスタンスが1秒あたりの大量の挿入およびget-top-elements操作をサポートします。

通常のセットと同様に、ソートされたセットを使用して関係を説明できますが、アイテムのリストにページ番号を付けたり、順序を覚えたりすることもできます。たとえば、ソートされたセットを使用してユーザーXの友達を覚えている場合、受け入れられた友情の順序で簡単に覚えることができます。

ソートされたセットは、優先キューに適しています。

ソートされたセットは、リストの中央からの範囲の挿入、削除、または取得が常に速い、より強力なリストのようなものです。しかし、それらはより多くのメモリを使用し、O(log(N))データ構造です。

結論

私はこの記事でいくつかの情報を提供したことを望みますが、http: //github.com/antirez/lamernewsからlamernewsのソースコードをダウンロードして、それがどのように機能するかを理解する方がはるかに良いです。Redisのデータ構造の多くはLamer News内で使用されており、特定のタスクを解決するために何を使用するかについては多くの手がかりがあります。

文法のタイプミスで申し訳ありません。ここは真夜中で、疲れすぎて投稿をレビューできません;)


45
これはRedisの唯一の著者です。私は彼にメールを送り、彼に返信するように頼んだ。サルヴァトーレ、本当にありがとうございました。これは素晴らしい情報です。
Homer6 2012年

58
おかげで、私は唯一の大きな貢献者ではありませんでした、Pieter Noordhuisは現在の実装の非常に大きな部分を提供しました:)
antirez

1
同じ文字列が多くの異なるセットにある場合、文字列の単一のコピーのみが保存されますか?
sbrian

スキップリストのみを使用してzscoreがO(1)にどのようにあるか?
Maxime

1
スキップリストは適切なバランスの取れたツリーではありませんが、スキップリストは「逆」ランダムツリーと見なすことができます。実装やレイアウトが異なっていても基本的には同等です。
antirez

80

ほとんどの場合、Redisで使用される基本的なデータ構造を理解する必要はありません。ただし、少し知識があれば、CPUとメモリのトレードオフを実現できます。また、効率的な方法でデータをモデル化するのにも役立ちます。

内部的には、Redisは次のデータ構造を使用します。

  1. ストリング
  2. 辞書
  3. 二重リンクリスト
  4. リストをスキップ
  5. 郵便番号リスト
  6. 整数セット
  7. Zipマップ(Redis 2.6以降ではzipリストを優先して非推奨)

特定のキーで使用されているエンコーディングを見つけるには、コマンドを使用しobject encoding <key>ます。

1.文字列

Redisでは、文字列はSimple Dynamic StringsまたはSDSと呼ばれます。これchar *は、文字列の長さと空きバイト数をプレフィックスとして保存できるの小さなラッパーです。

文字列の長さが格納されるため、strlenはO(1)操作です。また、長さがわかっているため、Redis文字列はバイナリセーフです。文字列にnull文字を含めることは完全に合法です。

文字列は、Redisで利用できる最も用途の広いデータ構造です。文字列は次のすべてです。

  1. テキストを格納できる文字列。SETおよびGETコマンドを参照してください。
  2. バイナリデータを格納できるバイト配列。
  3. A long番号を格納することができます。INCRDECRINCRBYおよびDECRBYコマンドを参照してください。
  4. アレイ(のcharsintslongs効率的なランダムアクセスを可能にすることができる、または他の任意のデータ型)。SETRANGEおよびGETRANGEコマンドを参照してください。
  5. ビット配列は、個々のビットを設定または取得することができます。SETBITおよびGETBITコマンドを参照してください。
  6. 他のデータ構造を構築するために使用できるメモリのブロック。これは内部的に使用されて、ziplistとintsetsを構築します。これらは、少数の要素のためのコンパクトでメモリ効率の高いデータ構造です。これについては、以下で詳しく説明します。

2.辞書

Redisは次の目的で辞書を使用します。

  1. キーをそれに関連付けられた値にマップします。値は文字列、ハッシュ、セット、ソートされたセットまたはリストです。
  2. キーを有効期限のタイムスタンプにマップする。
  3. ハッシュ、セット、ソート済みセットのデータ型を実装します。
  4. Redisコマンドを、それらのコマンドを処理する関数にマップするには。
  5. Redisキーを、そのキーでブロックされているクライアントのリストにマップするには。BLPOPを参照してください。

Redis辞書は、ハッシュテーブルを使用して実装されます。実装について説明するのではなく、Redis固有のものについて説明します。

  1. 辞書dictTypeは、ハッシュテーブルの動作を拡張するために呼び出される構造を使用します。この構造には関数ポインタがあるため、次の演算を拡張できます。a)ハッシュ関数、b)キー比較、c)キーデストラクタ、d)値デストラクタ。
  2. 辞書はmurmurhash2を使用します。(以前は、seed = 5381でdjb2ハッシュ関数を使用していましたが、その後、ハッシュ関数はmurmur2に切り替えられました。djb2ハッシュアルゴリズムの説明については、この質問を参照してください。)
  3. Redisは、Incremental Resizingとしても知られるIncremental Hashingを使用します。辞書には2つのハッシュテーブルがあります。辞書が操作されるたびに、1つのバケットが最初の(小さい)ハッシュテーブルから2番目のバケットに移行されます。このようにして、Redisは高価なサイズ変更操作を防ぎます。

Setデータ構造は、重複しない保証するために辞書を使用しています。Sorted Set理由は、そのスコアに要素をマップするために辞書を使用ZSCOREは O(1)操作です。

3.二重にリンクされたリスト

listデータ型は、使用して実装されて二重連結リストを。Redisの実装は、アルゴリズムからの教科書です。唯一の変更は、Redisがリストデータ構造に長さを格納することです。これにより、LLENがO(1)の複雑さを持つことが保証されます。

4.リストをスキップ

Redisはスキップリストをソート済みセットの基本的なデータ構造として使用します。ウィキペディアには良い紹介があります。William Pughの論文Skip Lists:A Probabilistic Alternative to Balanced Treesに詳細があります。

ソートセットは、スキップリストと辞書の両方を使用します。辞書には、各要素のスコアが格納されます。

Redisのスキップリストの実装は、次の点で標準の実装とは異なります。

  1. Redisでは重複スコアが許可されています。2つのノードが同じスコアを持っている場合、それらは辞書式順序でソートされます。
  2. 各ノードには、レベル0のバックポインターがあります。これにより、スコアの逆順で要素をトラバースできます。

5. Zipリスト

Zipリストは、ポインタを使用せずにデータをインラインで保存することを除いて、二重リンクリストに似ています。

二重リンクリストの各ノードには3つのポインターがあります。1つは前方ポインター、もう1つは後方ポインター、そのノードに格納されているデータを参照するためのポインターです。ポインタにはメモリ(64ビットシステムでは8バイト)が必要であるため、小さなリストの場合、二重リンクリストは非常に非効率的です。

Zipリストは、Redis文字列に要素を順番に格納します。各要素には、要素の長さとデータ型、次の要素へのオフセット、および前の要素へのオフセットを格納する小さなヘッダーがあります。これらのオフセットは、前方ポインタと後方ポインタを置き換えます。データはインラインで格納されるため、データポインターは必要ありません。

Zipリストは、小さなリスト、ソートされたセット、ハッシュを格納するために使用されます。ソートされたセットは[element1, score1, element2, score2, element3, score3]、Zip リストのようなリストにフラット化され、保存されます。ハッシュは[key1, value1, key2, value2]等のようなリストにフラット化されます。

Zipリストを使用すると、CPUとメモリの間でトレードオフを行うことができます。Zipリストはメモリ効率に優れていますが、リンクリスト(またはハッシュテーブル/スキップリスト)よりも多くのCPUを使用します。zipリストで要素を検索することはO(n)です。新しい要素を挿入するには、メモリを再割り当てする必要があります。このため、Redisはこのエンコードを小さなリスト、ハッシュ、ソート済みセットにのみ使用します。redis.conf の<datatype>-max-ziplist-entriesおよびの値を変更することで、この動作を調整できます<datatype>-max-ziplist-value>。詳細については、Redisメモリの最適化のセクション「小さな集計データ型の特別なエンコーディング」を参照してください。

ziplist.cコメントは優れており、コードを読まなくてもこのデータ構造を完全に理解できます。

6. Intセット

Int Setsは、「ソートされた整数配列」のファンシーな名前です。

Redisでは、セットは通常ハッシュテーブルを使用して実装されます。小さなセットの場合、ハッシュテーブルはメモリの点で非効率的です。セットが整数のみで構成される場合、配列の方が効率的です。

Int Setは、ソートされた整数の配列です。要素を見つけるには、バイナリ検索アルゴリズムが使用されます。これはO(log N)の複雑さを持っています。この配列に新しい整数を追加すると、メモリの再割り当てが必要になる場合があります。これは、大きな整数の配列の場合、負荷が高くなる可能性があります。

さらにメモリを最適化するために、Int Setsには16ビット、32ビット、64ビットの3つのバリアントがあり、整数サイズが異なります。Redisは、要素のサイズに応じて適切なバリアントを使用できるほどスマートです。新しい要素が追加され、現在のサイズを超えると、Redisはそれを自動的に次のサイズに移行します。文字列が追加されると、Redisは自動的にInt Setを通常のハッシュテーブルベースのセットに変換します。

Int Setsは、CPUとメモリの間のトレードオフです。Intセットはメモリ効率が非常に高く、小さなセットの場合はハッシュテーブルより高速です。しかし、特定の数の要素の後、O(log N)の取得時間とメモリの再割り当てのコストが大きくなりすぎます。実験に基づいて、通常のハッシュテーブルに切り替える最適なしきい値は512であることが判明しました。ただし、アプリケーションのニーズに基づいて、このしきい値を増やすことはできます(減らすことは意味がありません)。set-max-intset-entriesredis.confを参照してください。

7.ジップマップ

Zipマップは、フラット化されてリストに格納された辞書です。それらはZipリストに非常に似ています。

ZipマップはRedis 2.6から非推奨になり、小さなハッシュはZipリストに保存されます。このエンコーディングの詳細については、zipmap.cコメントを参照してください。


2

Redisは、値を指すキーを格納します。キーには、妥当なサイズまでの任意のバイナリ値を使用できます(読みやすさとデバッグの目的で、短いASCII文字列を使用することをお勧めします)。値は、5つのネイティブRedisデータ型の1つです。

1.strings — 512 MBまでのバイナリセーフバイトのシーケンス

2.ハッシュ—キーと値のペアのコレクション

3.lists —挿入順の文字列のコレクション

4.sets —順序のない一意の文字列のコレクション

5.sorted sets —ユーザー定義のスコアリングで並べられた一意の文字列のコレクション

文字列

Redis文字列はバイトのシーケンスです。

Redisの文字列はバイナリセーフ(特殊な終了文字によって決定されない既知の長さを持つ)であるため、1つの文字列に最大512メガバイトまで格納できます。

文字列は、標準的な「キー値ストア」の概念です。キーと値の両方がテキストまたはバイナリ文字列である値を指すキーがあります。

文字列で可能なすべての操作については、http://redis.io/commands/#stringを参照してください

ハッシュ

Redisハッシュは、キーと値のペアのコレクションです。

Redisハッシュは多くのキーと値のペアを保持します。各キーと値は文字列です。Redisハッシュは複雑な値を直接サポートしていません(つまり、ハッシュフィールドにリストまたはセットの値または別のハッシュを含めることはできません)が、ハッシュフィールドを使用して他の最上位の複雑な値を指すことができます。ハッシュフィールド値に対して実行できる唯一の特別な操作は、数値コンテンツのアトミックなインクリメント/デクリメントです。

Redisハッシュは、直接的なオブジェクト表現と、多くの小さな値をコンパクトに格納する方法の2つの方法で考えることができます。

直接的なオブジェクト表現は簡単に理解できます。オブジェクトには、名前(ハッシュのキー)と、値を持つ内部キーのコレクションがあります。例については、以下の例を参照してください。

ハッシュを使用して多くの小さな値を保存することは、賢いRedisの大容量データストレージテクニックです。ハッシュのフィールド数が少ない場合(〜100)、Redisはハッシュ全体のストレージとアクセス効率を最適化します。Redisの小さなハッシュストレージの最適化は興味深い動作を引き起こします。10,000個のトップレベルキーが文字列値を指すよりも、それぞれ100個の内部キーと値を持つ100個のハッシュを持つ方が効率的です。この方法でRedisハッシュを使用してデータストレージを最適化するには、データが最終的にどこに到達するかを追跡するための追加のプログラミングオーバーヘッドが必要ですが、データストレージが主に文字列ベースである場合、この1つの奇妙なトリックを使用して、多くのメモリオーバーヘッドを節約できます。

ハッシュで可能なすべての操作については、ハッシュのドキュメントを参照してください

リスト

Redisリストはリンクリストのように機能します。

リストの先頭または末尾から、リストへの挿入、リストからの削除、リストのトラバースを行うことができます。

値を挿入順に維持する必要がある場合は、リストを使用します。(Redisは、必要に応じて任意のリスト位置に挿入するオプションを提供しますが、開始位置から遠くに挿入すると、挿入パフォーマンスが低下します。)

Redisリストは、プロデューサー/コンシューマーキューとしてよく使用されます。リストにアイテムを挿入してから、リストからアイテムをポップします。要素のないリストから消費者がポップしようとするとどうなりますか?要素が表示されるのを待ち、追加されたらすぐにそれを返すようRedisに要求できます。これにより、Redisはリアルタイムのメッセージキュー/イベント/ジョブ/タスク/通知システムに変わります。

リストの両端から要素をアトミックに削除して、リストをスタックまたはキューとして扱うことができます。

挿入するたびにリストを特定のサイズにトリミングすることで、固定長リスト(キャップ​​されたコレクション)を維持することもできます。

リストで可能なすべての操作については、リストのドキュメントを参照しください

セット

Redisセットは、まあ、セットです。

Redisセットには、一意の順序付けされていないRedis文字列が含まれ、各文字列はセットごとに1回だけ存在します。同じ要素をセットに10回追加しても、表示されるのは1回だけです。セットは、重複した要素がたまってスペースを浪費することを心配せずに、少なくとも1回は何かが遅延して存在することを保証するのに最適です。同じ文字列がすでに存在するかどうかを確認する必要なく、何度でも追加できます。

セットは、メンバーシップのチェック、セット内のメンバーの挿入、削除が高速です。

予想どおり、セットには効率的なセット演算があります。ユニオン、交差、複数セットの差を一度に取得できます。結果は呼び出し元に返すか、後で使用するために新しいセットに保存できます。

セットはメンバーシップチェックに一定時間アクセスでき(リストとは異なり)、Redisは便利なランダムメンバーの削除と戻り(「セットからランダムエレメントをポップする」)またはランダムメンバーが交換せずに戻る(「ランダムな30人のユニークなユニークユーザーを与える」 ")または交換してください(" 7枚のカードをください。ただし、選択するたびにカードを元に戻して、再度サンプリングできるようにします ")。

セットで可能なすべての操作については、セットのドキュメントを参照してください。

ソートされたセット

Redisのソートされたセットは、ユーザー定義の順序を持​​つセットです。

簡単にするために、ソートされたセットは、一意の要素を持つバイナリツリーと考えることができます。(Redisのソートされたセットは、実際にはスキップリストです。)要素のソート順は、各要素のスコアによって定義されます。

ソートされたセットはまだセットです。要素は、セット内で1回だけ表示できます。要素は、一意性を目的として、文字列の内容によって定義されます。要素 "apple"を並べ替えスコア3で挿入し、要素 "apple"を並べ替えスコア500で挿入すると、並べ替えセット内の要素 "apple"が並べ替えスコア500で1つになります。セットは、(スコア、データ)のペアではなく、データに基づいてのみ一意です。

データモデルが一意性のために要素のスコアではなく文字列の内容に依存していることを確認してください。スコアの繰り返し(またはゼロ)は許可されますが、最後に、セット要素はソートされたセットごとに1つしか存在できません。たとえば、スコアをログインのエポックおよび値をユーザーIDとして、すべてのユーザーログインの履歴をソートされたセットとして保存しようとすると、すべてのユーザーの最後のログインエポックのみが保存されます。あなたのセットはユーザーベースのサイズに成長し、ユーザーベースのログインの希望サイズにはなりません*ログイン。

要素がスコアとともにセットに追加されます。任意の要素のスコアをいつでも更新できます。新しいスコアで要素を再度追加するだけです。スコアは倍精度浮動小数点数で表されるため、必要に応じて高精度のタイムスタンプの粒度を指定できます。複数の要素が同じスコアを持つ場合があります。

要素はいくつかの方法で取得できます。すべてがソートされているため、最低のスコアから始まる要素を要求できます。最も高いスコアで始まる要素を要求できます(「逆」)。要素を並べ替えスコアで自然順または逆順で要求できます。

ソートされたセットに対するすべての可能な操作については、ソートされたセットのドキュメントを参照してください

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.