├── .github
├── CODEOWNERS
└── workflows
│ ├── compress.yml
│ └── sync.yml
├── .gitignore
├── .nojekyll
├── LICENSE
├── README.md
├── docs
├── README.md
├── redis-bitmap-introduction.md
├── redis-distributed-lock.md
├── redis-hash-introduction.md
├── redis-hash-session-token.md
├── redis-hash-shorten-url.md
├── redis-hyperloglog-introduction.md
├── redis-interview.md
├── redis-list-paginate.md
├── redis-set-introduction.md
├── redis-set-like-and-dislike.md
├── redis-sorted-set-auto-complete.md
├── redis-sorted-set-introduction.md
├── redis-sorted-set-ranking-or-trending-list.md
├── redis-sorted-set-sns-follow.md
└── redis-string-introduction.md
├── images
├── doocs.png
├── favicon-16x16.png
├── favicon-32x32.png
├── owner-favicon-16x16.png
└── owner-favicon-32x32.png
├── index.html
├── java
├── README.md
├── build.gradle
├── gradle
│ └── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
└── src
│ ├── main
│ └── java
│ │ ├── AutoComplete.java
│ │ ├── DistributedLock.java
│ │ ├── LikeService.java
│ │ ├── LoginSession.java
│ │ ├── Paginate.java
│ │ ├── Ranking.java
│ │ ├── SocialRelationship.java
│ │ ├── URLShorten.java
│ │ └── utils
│ │ ├── HLLUtils.java
│ │ └── JedisUtils.java
│ └── test
│ └── java
│ ├── AutoCompleteTest.java
│ ├── DistributedLockTest.java
│ ├── LikeServiceTest.java
│ ├── LoginSessionTest.java
│ ├── PaginateTest.java
│ ├── RankingTest.java
│ ├── SocialRelationshipTest.java
│ └── URLShortenTest.java
└── python
├── README.md
└── src
├── main
├── __init__.py
├── auto_complete.py
├── distributed_lock.py
├── like_dislike.py
├── paginate.py
├── ranking_list.py
├── session_token.py
├── shorten_url.py
└── social_relationship.py
└── test
├── __init__.py
├── test_auto_complete.py
├── test_distributed_lock.py
├── test_like_dislike.py
├── test_paginate.py
├── test_ranking_list.py
├── test_session_token.py
├── test_shorten_url.py
└── test_social_relationship.py
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | *.md @yanglbme
2 | *.py @yanglbme
3 | *.java @yanglbme
4 |
5 | docs/ @yanglbme
6 | java/ @yanglbme
7 | python/ @yanglbme
8 |
--------------------------------------------------------------------------------
/.github/workflows/compress.yml:
--------------------------------------------------------------------------------
1 | name: Compress
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | paths:
7 | - "**.jpg"
8 | - "**.jpeg"
9 | - "**.png"
10 | - "**.webp"
11 |
12 | jobs:
13 | compress:
14 | runs-on: ubuntu-latest
15 | if: github.repository == 'yanglbme/redis-multi-programming-language-practice'
16 | steps:
17 | - name: Checkout Branch
18 | uses: actions/checkout@v2
19 |
20 | - name: Compress Images
21 | uses: calibreapp/image-actions@master
22 | with:
23 | githubToken: ${{ secrets.GITHUB_TOKEN }}
24 | compressOnly: true
25 |
26 | - name: Commit Files
27 | run: |
28 | git config --local user.email "action@github.com"
29 | git config --local user.name "GitHub Action"
30 | git commit -m "[Automated] Optimize images" -a
31 |
32 | - name: Push Changes
33 | uses: ad-m/github-push-action@master
34 | with:
35 | github_token: ${{ secrets.GITHUB_TOKEN }}
36 |
--------------------------------------------------------------------------------
/.github/workflows/sync.yml:
--------------------------------------------------------------------------------
1 | name: Sync
2 |
3 | on:
4 | push:
5 | branches: [main]
6 |
7 | jobs:
8 | sync:
9 | runs-on: ubuntu-latest
10 | if: github.repository == 'yanglbme/redis-multi-programming-language-practice'
11 | steps:
12 | - name: Sync to gitee.com
13 | uses: wearerequired/git-mirror-action@master
14 | env:
15 | SSH_PRIVATE_KEY: ${{ secrets.RSA_PRIVATE_KEY }}
16 | with:
17 | source-repo: git@github.com:yanglbme/redis-multi-programming-language-practice.git
18 | destination-repo: git@gitee.com:yanglbme/redis-multi-programming-language-practice.git
19 |
20 | # - name: Build Gitee Pages
21 | # uses: yanglbme/gitee-pages-action@main
22 | # with:
23 | # gitee-username: yanglbme
24 | # gitee-password: ${{ secrets.GITEE_PASSWORD }}
25 | # gitee-repo: yanglbme/redis-multi-programming-language-practice
26 | # branch: main
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /gradle/wrapper/gradle-wrapper.properties
2 | ##----------Android----------
3 | # build
4 | *.apk
5 | *.ap_
6 | *.dex
7 | *.class
8 | bin/
9 | gen/
10 | build/
11 |
12 | # gradle
13 | .gradle/
14 | gradle-app.setting
15 | !gradle-wrapper.jar
16 | build/
17 |
18 | local.properties
19 |
20 | ##----------idea----------
21 | *.iml
22 | .idea/
23 | *.ipr
24 | *.iws
25 |
26 | # Android Studio Navigation editor temp files
27 | .navigation/
28 |
29 | ##----------Other----------
30 | # osx
31 | *~
32 | .DS_Store
33 | gradle.properties
34 |
35 | .vscode
36 |
37 | # Byte-compiled / optimized / DLL files
38 | __pycache__/
39 | *.py[cod]
40 | *$py.class
41 |
42 | # C extensions
43 | *.so
44 |
45 | # Distribution / packaging
46 | .Python
47 | build/
48 | develop-eggs/
49 | dist/
50 | downloads/
51 | eggs/
52 | .eggs/
53 | lib/
54 | lib64/
55 | parts/
56 | sdist/
57 | var/
58 | wheels/
59 | pip-wheel-metadata/
60 | share/python-wheels/
61 | *.egg-info/
62 | .installed.cfg
63 | *.egg
64 | MANIFEST
65 |
66 | # PyInstaller
67 | # Usually these files are written by a python script from a template
68 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
69 | *.manifest
70 | *.spec
71 |
72 | # Installer logs
73 | pip-log.txt
74 | pip-delete-this-directory.txt
75 |
76 | # Unit test / coverage reports
77 | htmlcov/
78 | .tox/
79 | .nox/
80 | .coverage
81 | .coverage.*
82 | .cache
83 | nosetests.xml
84 | coverage.xml
85 | *.cover
86 | .hypothesis/
87 | .pytest_cache/
88 |
89 | # Translations
90 | *.mo
91 | *.pot
92 |
93 | # Django stuff:
94 | *.log
95 | local_settings.py
96 | db.sqlite3
97 | db.sqlite3-journal
98 |
99 | # Flask stuff:
100 | instance/
101 | .webassets-cache
102 |
103 | # Scrapy stuff:
104 | .scrapy
105 |
106 | # Sphinx documentation
107 | docs/_build/
108 |
109 | # PyBuilder
110 | target/
111 |
112 | # Jupyter Notebook
113 | .ipynb_checkpoints
114 |
115 | # IPython
116 | profile_default/
117 | ipython_config.py
118 |
119 | # pyenv
120 | .python-version
121 |
122 | # pipenv
123 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
124 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
125 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
126 | # install all needed dependencies.
127 | #Pipfile.lock
128 |
129 | # celery beat schedule file
130 | celerybeat-schedule
131 |
132 | # SageMath parsed files
133 | *.sage.py
134 |
135 | # Environments
136 | .env
137 | .venv
138 | env/
139 | venv/
140 | ENV/
141 | env.bak/
142 | venv.bak/
143 |
144 | # Spyder project settings
145 | .spyderproject
146 | .spyproject
147 |
148 | # Rope project settings
149 | .ropeproject
150 |
151 | # mkdocs documentation
152 | /site
153 |
154 | # mypy
155 | .mypy_cache/
156 | .dmypy.json
157 | dmypy.json
158 |
159 | # Pyre type checker
160 | .pyre/
--------------------------------------------------------------------------------
/.nojekyll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yanglbme/redis-multi-programming-language-practice/2425ee7e1b1f193f7b5e57ad57e3b59f9adfd329/.nojekyll
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Attribution-ShareAlike 4.0 International
2 |
3 | =======================================================================
4 |
5 | Creative Commons Corporation ("Creative Commons") is not a law firm and
6 | does not provide legal services or legal advice. Distribution of
7 | Creative Commons public licenses does not create a lawyer-client or
8 | other relationship. Creative Commons makes its licenses and related
9 | information available on an "as-is" basis. Creative Commons gives no
10 | warranties regarding its licenses, any material licensed under their
11 | terms and conditions, or any related information. Creative Commons
12 | disclaims all liability for damages resulting from their use to the
13 | fullest extent possible.
14 |
15 | Using Creative Commons Public Licenses
16 |
17 | Creative Commons public licenses provide a standard set of terms and
18 | conditions that creators and other rights holders may use to share
19 | original works of authorship and other material subject to copyright
20 | and certain other rights specified in the public license below. The
21 | following considerations are for informational purposes only, are not
22 | exhaustive, and do not form part of our licenses.
23 |
24 | Considerations for licensors: Our public licenses are
25 | intended for use by those authorized to give the public
26 | permission to use material in ways otherwise restricted by
27 | copyright and certain other rights. Our licenses are
28 | irrevocable. Licensors should read and understand the terms
29 | and conditions of the license they choose before applying it.
30 | Licensors should also secure all rights necessary before
31 | applying our licenses so that the public can reuse the
32 | material as expected. Licensors should clearly mark any
33 | material not subject to the license. This includes other CC-
34 | licensed material, or material used under an exception or
35 | limitation to copyright. More considerations for licensors:
36 | wiki.creativecommons.org/Considerations_for_licensors
37 |
38 | Considerations for the public: By using one of our public
39 | licenses, a licensor grants the public permission to use the
40 | licensed material under specified terms and conditions. If
41 | the licensor's permission is not necessary for any reason--for
42 | example, because of any applicable exception or limitation to
43 | copyright--then that use is not regulated by the license. Our
44 | licenses grant only permissions under copyright and certain
45 | other rights that a licensor has authority to grant. Use of
46 | the licensed material may still be restricted for other
47 | reasons, including because others have copyright or other
48 | rights in the material. A licensor may make special requests,
49 | such as asking that all changes be marked or described.
50 | Although not required by our licenses, you are encouraged to
51 | respect those requests where reasonable. More_considerations
52 | for the public:
53 | wiki.creativecommons.org/Considerations_for_licensees
54 |
55 | =======================================================================
56 |
57 | Creative Commons Attribution-ShareAlike 4.0 International Public
58 | License
59 |
60 | By exercising the Licensed Rights (defined below), You accept and agree
61 | to be bound by the terms and conditions of this Creative Commons
62 | Attribution-ShareAlike 4.0 International Public License ("Public
63 | License"). To the extent this Public License may be interpreted as a
64 | contract, You are granted the Licensed Rights in consideration of Your
65 | acceptance of these terms and conditions, and the Licensor grants You
66 | such rights in consideration of benefits the Licensor receives from
67 | making the Licensed Material available under these terms and
68 | conditions.
69 |
70 |
71 | Section 1 -- Definitions.
72 |
73 | a. Adapted Material means material subject to Copyright and Similar
74 | Rights that is derived from or based upon the Licensed Material
75 | and in which the Licensed Material is translated, altered,
76 | arranged, transformed, or otherwise modified in a manner requiring
77 | permission under the Copyright and Similar Rights held by the
78 | Licensor. For purposes of this Public License, where the Licensed
79 | Material is a musical work, performance, or sound recording,
80 | Adapted Material is always produced where the Licensed Material is
81 | synched in timed relation with a moving image.
82 |
83 | b. Adapter's License means the license You apply to Your Copyright
84 | and Similar Rights in Your contributions to Adapted Material in
85 | accordance with the terms and conditions of this Public License.
86 |
87 | c. BY-SA Compatible License means a license listed at
88 | creativecommons.org/compatiblelicenses, approved by Creative
89 | Commons as essentially the equivalent of this Public License.
90 |
91 | d. Copyright and Similar Rights means copyright and/or similar rights
92 | closely related to copyright including, without limitation,
93 | performance, broadcast, sound recording, and Sui Generis Database
94 | Rights, without regard to how the rights are labeled or
95 | categorized. For purposes of this Public License, the rights
96 | specified in Section 2(b)(1)-(2) are not Copyright and Similar
97 | Rights.
98 |
99 | e. Effective Technological Measures means those measures that, in the
100 | absence of proper authority, may not be circumvented under laws
101 | fulfilling obligations under Article 11 of the WIPO Copyright
102 | Treaty adopted on December 20, 1996, and/or similar international
103 | agreements.
104 |
105 | f. Exceptions and Limitations means fair use, fair dealing, and/or
106 | any other exception or limitation to Copyright and Similar Rights
107 | that applies to Your use of the Licensed Material.
108 |
109 | g. License Elements means the license attributes listed in the name
110 | of a Creative Commons Public License. The License Elements of this
111 | Public License are Attribution and ShareAlike.
112 |
113 | h. Licensed Material means the artistic or literary work, database,
114 | or other material to which the Licensor applied this Public
115 | License.
116 |
117 | i. Licensed Rights means the rights granted to You subject to the
118 | terms and conditions of this Public License, which are limited to
119 | all Copyright and Similar Rights that apply to Your use of the
120 | Licensed Material and that the Licensor has authority to license.
121 |
122 | j. Licensor means the individual(s) or entity(ies) granting rights
123 | under this Public License.
124 |
125 | k. Share means to provide material to the public by any means or
126 | process that requires permission under the Licensed Rights, such
127 | as reproduction, public display, public performance, distribution,
128 | dissemination, communication, or importation, and to make material
129 | available to the public including in ways that members of the
130 | public may access the material from a place and at a time
131 | individually chosen by them.
132 |
133 | l. Sui Generis Database Rights means rights other than copyright
134 | resulting from Directive 96/9/EC of the European Parliament and of
135 | the Council of 11 March 1996 on the legal protection of databases,
136 | as amended and/or succeeded, as well as other essentially
137 | equivalent rights anywhere in the world.
138 |
139 | m. You means the individual or entity exercising the Licensed Rights
140 | under this Public License. Your has a corresponding meaning.
141 |
142 |
143 | Section 2 -- Scope.
144 |
145 | a. License grant.
146 |
147 | 1. Subject to the terms and conditions of this Public License,
148 | the Licensor hereby grants You a worldwide, royalty-free,
149 | non-sublicensable, non-exclusive, irrevocable license to
150 | exercise the Licensed Rights in the Licensed Material to:
151 |
152 | a. reproduce and Share the Licensed Material, in whole or
153 | in part; and
154 |
155 | b. produce, reproduce, and Share Adapted Material.
156 |
157 | 2. Exceptions and Limitations. For the avoidance of doubt, where
158 | Exceptions and Limitations apply to Your use, this Public
159 | License does not apply, and You do not need to comply with
160 | its terms and conditions.
161 |
162 | 3. Term. The term of this Public License is specified in Section
163 | 6(a).
164 |
165 | 4. Media and formats; technical modifications allowed. The
166 | Licensor authorizes You to exercise the Licensed Rights in
167 | all media and formats whether now known or hereafter created,
168 | and to make technical modifications necessary to do so. The
169 | Licensor waives and/or agrees not to assert any right or
170 | authority to forbid You from making technical modifications
171 | necessary to exercise the Licensed Rights, including
172 | technical modifications necessary to circumvent Effective
173 | Technological Measures. For purposes of this Public License,
174 | simply making modifications authorized by this Section 2(a)
175 | (4) never produces Adapted Material.
176 |
177 | 5. Downstream recipients.
178 |
179 | a. Offer from the Licensor -- Licensed Material. Every
180 | recipient of the Licensed Material automatically
181 | receives an offer from the Licensor to exercise the
182 | Licensed Rights under the terms and conditions of this
183 | Public License.
184 |
185 | b. Additional offer from the Licensor -- Adapted Material.
186 | Every recipient of Adapted Material from You
187 | automatically receives an offer from the Licensor to
188 | exercise the Licensed Rights in the Adapted Material
189 | under the conditions of the Adapter's License You apply.
190 |
191 | c. No downstream restrictions. You may not offer or impose
192 | any additional or different terms or conditions on, or
193 | apply any Effective Technological Measures to, the
194 | Licensed Material if doing so restricts exercise of the
195 | Licensed Rights by any recipient of the Licensed
196 | Material.
197 |
198 | 6. No endorsement. Nothing in this Public License constitutes or
199 | may be construed as permission to assert or imply that You
200 | are, or that Your use of the Licensed Material is, connected
201 | with, or sponsored, endorsed, or granted official status by,
202 | the Licensor or others designated to receive attribution as
203 | provided in Section 3(a)(1)(A)(i).
204 |
205 | b. Other rights.
206 |
207 | 1. Moral rights, such as the right of integrity, are not
208 | licensed under this Public License, nor are publicity,
209 | privacy, and/or other similar personality rights; however, to
210 | the extent possible, the Licensor waives and/or agrees not to
211 | assert any such rights held by the Licensor to the limited
212 | extent necessary to allow You to exercise the Licensed
213 | Rights, but not otherwise.
214 |
215 | 2. Patent and trademark rights are not licensed under this
216 | Public License.
217 |
218 | 3. To the extent possible, the Licensor waives any right to
219 | collect royalties from You for the exercise of the Licensed
220 | Rights, whether directly or through a collecting society
221 | under any voluntary or waivable statutory or compulsory
222 | licensing scheme. In all other cases the Licensor expressly
223 | reserves any right to collect such royalties.
224 |
225 |
226 | Section 3 -- License Conditions.
227 |
228 | Your exercise of the Licensed Rights is expressly made subject to the
229 | following conditions.
230 |
231 | a. Attribution.
232 |
233 | 1. If You Share the Licensed Material (including in modified
234 | form), You must:
235 |
236 | a. retain the following if it is supplied by the Licensor
237 | with the Licensed Material:
238 |
239 | i. identification of the creator(s) of the Licensed
240 | Material and any others designated to receive
241 | attribution, in any reasonable manner requested by
242 | the Licensor (including by pseudonym if
243 | designated);
244 |
245 | ii. a copyright notice;
246 |
247 | iii. a notice that refers to this Public License;
248 |
249 | iv. a notice that refers to the disclaimer of
250 | warranties;
251 |
252 | v. a URI or hyperlink to the Licensed Material to the
253 | extent reasonably practicable;
254 |
255 | b. indicate if You modified the Licensed Material and
256 | retain an indication of any previous modifications; and
257 |
258 | c. indicate the Licensed Material is licensed under this
259 | Public License, and include the text of, or the URI or
260 | hyperlink to, this Public License.
261 |
262 | 2. You may satisfy the conditions in Section 3(a)(1) in any
263 | reasonable manner based on the medium, means, and context in
264 | which You Share the Licensed Material. For example, it may be
265 | reasonable to satisfy the conditions by providing a URI or
266 | hyperlink to a resource that includes the required
267 | information.
268 |
269 | 3. If requested by the Licensor, You must remove any of the
270 | information required by Section 3(a)(1)(A) to the extent
271 | reasonably practicable.
272 |
273 | b. ShareAlike.
274 |
275 | In addition to the conditions in Section 3(a), if You Share
276 | Adapted Material You produce, the following conditions also apply.
277 |
278 | 1. The Adapter's License You apply must be a Creative Commons
279 | license with the same License Elements, this version or
280 | later, or a BY-SA Compatible License.
281 |
282 | 2. You must include the text of, or the URI or hyperlink to, the
283 | Adapter's License You apply. You may satisfy this condition
284 | in any reasonable manner based on the medium, means, and
285 | context in which You Share Adapted Material.
286 |
287 | 3. You may not offer or impose any additional or different terms
288 | or conditions on, or apply any Effective Technological
289 | Measures to, Adapted Material that restrict exercise of the
290 | rights granted under the Adapter's License You apply.
291 |
292 |
293 | Section 4 -- Sui Generis Database Rights.
294 |
295 | Where the Licensed Rights include Sui Generis Database Rights that
296 | apply to Your use of the Licensed Material:
297 |
298 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right
299 | to extract, reuse, reproduce, and Share all or a substantial
300 | portion of the contents of the database;
301 |
302 | b. if You include all or a substantial portion of the database
303 | contents in a database in which You have Sui Generis Database
304 | Rights, then the database in which You have Sui Generis Database
305 | Rights (but not its individual contents) is Adapted Material,
306 |
307 | including for purposes of Section 3(b); and
308 | c. You must comply with the conditions in Section 3(a) if You Share
309 | all or a substantial portion of the contents of the database.
310 |
311 | For the avoidance of doubt, this Section 4 supplements and does not
312 | replace Your obligations under this Public License where the Licensed
313 | Rights include other Copyright and Similar Rights.
314 |
315 |
316 | Section 5 -- Disclaimer of Warranties and Limitation of Liability.
317 |
318 | a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
319 | EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
320 | AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
321 | ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
322 | IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
323 | WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
324 | PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
325 | ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
326 | KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
327 | ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
328 |
329 | b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
330 | TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
331 | NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
332 | INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
333 | COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
334 | USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
335 | ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
336 | DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
337 | IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
338 |
339 | c. The disclaimer of warranties and limitation of liability provided
340 | above shall be interpreted in a manner that, to the extent
341 | possible, most closely approximates an absolute disclaimer and
342 | waiver of all liability.
343 |
344 |
345 | Section 6 -- Term and Termination.
346 |
347 | a. This Public License applies for the term of the Copyright and
348 | Similar Rights licensed here. However, if You fail to comply with
349 | this Public License, then Your rights under this Public License
350 | terminate automatically.
351 |
352 | b. Where Your right to use the Licensed Material has terminated under
353 | Section 6(a), it reinstates:
354 |
355 | 1. automatically as of the date the violation is cured, provided
356 | it is cured within 30 days of Your discovery of the
357 | violation; or
358 |
359 | 2. upon express reinstatement by the Licensor.
360 |
361 | For the avoidance of doubt, this Section 6(b) does not affect any
362 | right the Licensor may have to seek remedies for Your violations
363 | of this Public License.
364 |
365 | c. For the avoidance of doubt, the Licensor may also offer the
366 | Licensed Material under separate terms or conditions or stop
367 | distributing the Licensed Material at any time; however, doing so
368 | will not terminate this Public License.
369 |
370 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
371 | License.
372 |
373 |
374 | Section 7 -- Other Terms and Conditions.
375 |
376 | a. The Licensor shall not be bound by any additional or different
377 | terms or conditions communicated by You unless expressly agreed.
378 |
379 | b. Any arrangements, understandings, or agreements regarding the
380 | Licensed Material not stated herein are separate from and
381 | independent of the terms and conditions of this Public License.
382 |
383 |
384 | Section 8 -- Interpretation.
385 |
386 | a. For the avoidance of doubt, this Public License does not, and
387 | shall not be interpreted to, reduce, limit, restrict, or impose
388 | conditions on any use of the Licensed Material that could lawfully
389 | be made without permission under this Public License.
390 |
391 | b. To the extent possible, if any provision of this Public License is
392 | deemed unenforceable, it shall be automatically reformed to the
393 | minimum extent necessary to make it enforceable. If the provision
394 | cannot be reformed, it shall be severed from this Public License
395 | without affecting the enforceability of the remaining terms and
396 | conditions.
397 |
398 | c. No term or condition of this Public License will be waived and no
399 | failure to comply consented to unless expressly agreed to by the
400 | Licensor.
401 |
402 | d. Nothing in this Public License constitutes or may be interpreted
403 | as a limitation upon, or waiver of, any privileges and immunities
404 | that apply to the Licensor or You, including from the legal
405 | processes of any jurisdiction or authority.
406 |
407 |
408 | =======================================================================
409 |
410 | Creative Commons is not a party to its public
411 | licenses. Notwithstanding, Creative Commons may elect to apply one of
412 | its public licenses to material it publishes and in those instances
413 | will be considered the “Licensor.” The text of the Creative Commons
414 | public licenses is dedicated to the public domain under the CC0 Public
415 | Domain Dedication. Except for the limited purpose of indicating that
416 | material is shared under a Creative Commons public license or as
417 | otherwise permitted by the Creative Commons policies published at
418 | creativecommons.org/policies, Creative Commons does not authorize the
419 | use of the trademark "Creative Commons" or any other trademark or logo
420 | of Creative Commons without its prior written consent including,
421 | without limitation, in connection with any unauthorized modifications
422 | to any of its public licenses or any other arrangements,
423 | understandings, or agreements concerning use of licensed material. For
424 | the avoidance of doubt, this paragraph does not form part of the
425 | public licenses.
426 |
427 | Creative Commons may be contacted at creativecommons.org.
428 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Redis 底层原理分析与多语言应用实践[©](https://github.com/yanglbme)
2 |
3 | [](http://makeapullrequest.com)
4 | [](https://doocs.github.io/#/?id=how-to-join)
5 | [](https://gitter.im/doocs)
6 |
7 | 本项目主要讲解 Redis 的底层原理以及在各种场景下的应用。所有演示代码均基于 Redis 最新稳定版本 `v6`,不同操作系统下 Redis 的安装方式请自行搜索,就不在此赘述了。
8 |
9 | 另,本项目针对不同编程语言,使用了其对应的 Redis 库,方便程序对 Redis 进行各项操作:
10 |
11 | - Python: 使用 pip 安装 redis 库,[`pip install redis`](https://pypi.org/project/redis/)
12 | - Java: 使用 gradle 导入 jedis 库,[`implementation group: 'redis.clients', name: 'jedis', version: '3.7.0'`](https://mvnrepository.com/artifact/redis.clients/jedis/3.7.0)
13 |
14 | 欢迎补充更多的实际应用场景,让项目内容更加完善。如果你认为演示代码有待改进,可以在 Issues 区反馈,当然,你也可以直接发起 Pull Request。
15 |
16 | ## Redis 数据结构与应用
17 |
18 | ### [String 字符串](/docs/redis-string-introduction.md)
19 |
20 | - [说说如何基于 Redis 实现分布式锁?](/docs/redis-distributed-lock.md)
21 |
22 | ### [List 列表](/docs/redis-list-introduction.md)
23 |
24 | - 如何利用 Redis List 实现异步消息队列?
25 | - [用 Redis 如何实现页面数据分页的效果?](/docs/redis-list-paginate.md)
26 |
27 | ### [Set 集合](/docs/redis-set-introduction.md)
28 |
29 | - [如何用 Redis 实现论坛帖子的点赞点踩功能?](/docs/redis-set-like-and-dislike.md)
30 |
31 | ### [Sorted Sets 有序集合](/docs/redis-sorted-set-introduction.md)
32 |
33 | - [社交网站通常会有粉丝关注的功能,用 Redis 怎么实现?](/docs/redis-sorted-set-sns-follow.md)
34 | - [每日、每周、每月积分排行榜功能该怎么实现?](/docs/redis-sorted-set-ranking-or-trending-list.md)
35 | - [关键词搜索,如何用 Redis 实现自动补全?](/docs/redis-sorted-set-auto-complete.md)
36 |
37 | ### [Hash 哈希](/docs/redis-hash-introduction.md)
38 |
39 | - [登录会话,用 Redis 该怎么做?](/docs/redis-hash-session-token.md)
40 | - [如何使用 Redis 实现短网址服务?](/docs/redis-hash-shorten-url.md)
41 |
42 | ### [HyperLogLog](/docs/redis-hyperLogLog-introduction.md)
43 |
44 | ### [Bitmap 位图](/docs/redis-bitmap-introduction.md)
45 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yanglbme/redis-multi-programming-language-practice/2425ee7e1b1f193f7b5e57ad57e3b59f9adfd329/docs/README.md
--------------------------------------------------------------------------------
/docs/redis-bitmap-introduction.md:
--------------------------------------------------------------------------------
1 | # Redis Bitmap 结构底层原理剖析
--------------------------------------------------------------------------------
/docs/redis-distributed-lock.md:
--------------------------------------------------------------------------------
1 | # 使用 Redis String 实现分布式锁
2 |
3 | ## 代码实现
4 |
5 | | [Python](#Python-版本) | [Java](#Java-版本) |
6 | | ---------------------- | ------------------ |
7 |
8 | ### Python 版本
9 |
10 | ```python
11 | import random
12 | import string
13 | import time
14 |
15 | from redis import Redis
16 |
17 |
18 | def generate_random_value():
19 | return ''.join(random.sample(string.ascii_letters + string.digits, 32))
20 |
21 |
22 | # 设定默认过期时长为 10s
23 | DEFAULT_TIMEOUT = 10
24 |
25 |
26 | class DistributedLock:
27 | def __init__(self, client: Redis, key: str):
28 | self.client = client
29 | self.key = key
30 |
31 | def acquire(self, timeout=DEFAULT_TIMEOUT) -> bool:
32 | """尝试获取锁"""
33 | res = self.client.set(self.key, generate_random_value(), ex=timeout, nx=True) # ex 表示过期时长秒数,nx 表示 key 不存在时才设置
34 | return res is True
35 |
36 | def release(self) -> bool:
37 | """尝试释放锁"""
38 | return self.client.delete(self.key) == 1
39 |
40 |
41 | if __name__ == '__main__':
42 | redis = Redis(decode_responses=True)
43 | lock = DistributedLock(redis, 'bingo_distributed_lock')
44 |
45 | print(lock.acquire(10)) # True
46 | print(lock.acquire(10)) # False
47 |
48 | time.sleep(10)
49 | print(lock.acquire()) # True
50 | print(lock.release()) # True
51 |
52 | print(lock.acquire()) # True
53 | ```
54 |
55 | ### Java 版本
56 |
57 | ```java
58 | import redis.clients.jedis.Jedis;
59 | import redis.clients.jedis.params.SetParams;
60 |
61 | import java.util.UUID;
62 | import java.util.concurrent.TimeUnit;
63 |
64 |
65 | public class DistributedLock {
66 |
67 | private Jedis client;
68 | private String key;
69 |
70 | public DistributedLock(Jedis client, String key) {
71 | this.client = client;
72 | this.key = key;
73 | }
74 |
75 | /**
76 | * 获取随机字符串值
77 | *
78 | * @return 随机字符串
79 | */
80 | private String generateRandomValue() {
81 | return UUID.randomUUID().toString().replaceAll("-", "");
82 | }
83 |
84 | /**
85 | * 尝试获取锁
86 | *
87 | * @param timeout 过期时长
88 | * @return 是否获取成功
89 | */
90 | public boolean acquire(int timeout) {
91 | SetParams params = new SetParams().ex(timeout).nx();
92 | return client.set(key, generateRandomValue(), params) != null;
93 | }
94 |
95 | public boolean acquire() {
96 | int timeout = 10;
97 | return acquire(timeout);
98 | }
99 |
100 | /**
101 | * 尝试释放锁
102 | *
103 | * @return 是否成功释放锁
104 | */
105 | public boolean release() {
106 | return client.del(key) == 1;
107 | }
108 |
109 | public static void main(String[] args) throws InterruptedException {
110 | Jedis jedis = new Jedis();
111 | DistributedLock lock = new DistributedLock(jedis, "bingo_distributed_lock");
112 |
113 | System.out.println(lock.acquire(10)); // true
114 | System.out.println(lock.acquire(10)); // false
115 |
116 | TimeUnit.SECONDS.sleep(10);
117 | System.out.println(lock.acquire()); // true
118 | System.out.println(lock.release()); // true
119 |
120 | System.out.println(lock.acquire()); // true
121 | }
122 | }
123 | ```
124 |
--------------------------------------------------------------------------------
/docs/redis-hash-introduction.md:
--------------------------------------------------------------------------------
1 | # Redis Hash 结构底层原理剖析
2 |
--------------------------------------------------------------------------------
/docs/redis-hash-session-token.md:
--------------------------------------------------------------------------------
1 | # 利用 Redis 哈希实现登录会话
2 |
3 | ## 代码实现
4 |
5 | | [Python](#Python-版本) | [Java](#Java-版本) |
6 | | ---------------------- | ------------------ |
7 |
8 | ### Python 版本
9 |
10 | ```python
11 | import os
12 | from _sha256 import sha256
13 | from time import time
14 |
15 | from redis import Redis
16 |
17 | # 会话默认过期时间,一个月
18 | DEFAULT_TIMEOUT = 30 * 24 * 3600
19 |
20 | # 会话 token 及过期时间的 key
21 | SESSION_TOKEN_KEY = 'SESSION:TOKEN'
22 | SESSION_EXPIRE_TS_KEY = 'SESSION:EXPIRE'
23 |
24 | # 会话状态
25 | SESSION_NOT_LOGIN = 'SESSION_NOT_LOGIN'
26 | SESSION_EXPIRE = 'SESSION_EXPIRE'
27 | SESSION_TOKEN_CORRECT = 'SESSION_TOKEN_CORRECT'
28 | SESSION_TOKEN_INCORRECT = 'SESSION_TOKEN_INCORRECT'
29 |
30 |
31 | def generate_token():
32 | """生成一个随机的会话令牌"""
33 | return sha256(os.urandom(256)).hexdigest()
34 |
35 |
36 | class LoginSession:
37 | def __init__(self, client: Redis, user_id: str):
38 | self.client = client
39 | self.user_id = user_id
40 |
41 | def create(self, timeout=DEFAULT_TIMEOUT) -> str:
42 | """创建新的会话,并返回会话token"""
43 | session_token = generate_token()
44 |
45 | # 设置过期时间
46 | expire_time = time() + timeout
47 | self.client.hset(SESSION_TOKEN_KEY, self.user_id, session_token)
48 | self.client.hset(SESSION_EXPIRE_TS_KEY, self.user_id, expire_time)
49 | return session_token
50 |
51 | def validate(self, token) -> str:
52 | """校验token"""
53 | session_token = self.client.hget(SESSION_TOKEN_KEY, self.user_id)
54 | expire_time = self.client.hget(SESSION_EXPIRE_TS_KEY, self.user_id)
55 |
56 | if (session_token is None) or (expire_time is None):
57 | return SESSION_NOT_LOGIN
58 |
59 | # 将字符串类型的时间转换为浮点数类型
60 | if time() > float(expire_time):
61 | return SESSION_EXPIRE
62 |
63 | if session_token == token:
64 | return SESSION_TOKEN_CORRECT
65 |
66 | return SESSION_TOKEN_INCORRECT
67 |
68 | def destroy(self):
69 | """销毁会话"""
70 | self.client.hdel(SESSION_TOKEN_KEY, self.user_id)
71 | self.client.hdel(SESSION_EXPIRE_TS_KEY, self.user_id)
72 |
73 |
74 | if __name__ == '__main__':
75 | redis = Redis(decode_responses=True)
76 | session = LoginSession(redis, 'bingo')
77 |
78 | user_token = session.create()
79 | print(user_token) # 22d33cb3155533bd71240d4de2285fbf27916253828be4c3784cb5577ae7ec05
80 |
81 | res = session.validate('this is a wrong token')
82 | print(res) # SESSION_TOKEN_INCORRECT
83 |
84 | res = session.validate(user_token)
85 | print(res) # SESSION_TOKEN_CORRECT
86 |
87 | session.destroy()
88 | res = session.validate(user_token)
89 | print(res) # SESSION_NOT_LOGIN
90 | ```
91 |
92 | ### Java 版本
93 |
94 | ```java
95 | import org.apache.commons.codec.binary.Hex;
96 | import redis.clients.jedis.Jedis;
97 |
98 | import java.security.MessageDigest;
99 | import java.security.NoSuchAlgorithmException;
100 | import java.time.Instant;
101 | import java.util.Random;
102 |
103 |
104 | public class LoginSession {
105 |
106 | private final String SESSION_TOKEN_KEY = "SESSION:TOKEN";
107 | private final String SESSION_EXPIRE_TS_KEY = "SESSION:EXPIRE";
108 |
109 | private final String SESSION_NOT_LOGIN = "SESSION_NOT_LOGIN";
110 | private final String SESSION_EXPIRE = "SESSION_EXPIRE";
111 | private final String SESSION_TOKEN_CORRECT = "SESSION_TOKEN_CORRECT";
112 | private final String SESSION_TOKEN_INCORRECT = "SESSION_TOKEN_INCORRECT";
113 |
114 | private Jedis client;
115 | private String userId;
116 |
117 | public LoginSession(Jedis client, String userId) {
118 | this.client = client;
119 | this.userId = userId;
120 | }
121 |
122 | /**
123 | * 生成随机token
124 | *
125 | * @return token
126 | */
127 | private String generateToken() {
128 | byte[] b = new byte[256];
129 | new Random().nextBytes(b);
130 |
131 | MessageDigest messageDigest;
132 |
133 | String sessionToken = "";
134 | try {
135 | messageDigest = MessageDigest.getInstance("SHA-256");
136 | byte[] hash = messageDigest.digest(b);
137 | sessionToken = Hex.encodeHexString(hash);
138 | } catch (NoSuchAlgorithmException e) {
139 | e.printStackTrace();
140 | }
141 | return sessionToken;
142 | }
143 |
144 | /**
145 | * 创建会话,并返回token
146 | *
147 | * @param timeout 过期时长
148 | * @return token
149 | */
150 | public String create(int timeout) {
151 | String token = generateToken();
152 | long expireTime = Instant.now().getEpochSecond() + timeout;
153 | client.hset(SESSION_TOKEN_KEY, userId, token);
154 | client.hset(SESSION_EXPIRE_TS_KEY, userId, String.valueOf(expireTime));
155 | return token;
156 | }
157 |
158 | public String create() {
159 | // 设置默认过期时长
160 | int defaultTimeout = 30 * 24 * 3600;
161 | return create(defaultTimeout);
162 | }
163 |
164 | /**
165 | * 校验token
166 | *
167 | * @param token 输入的token
168 | * @return 校验结果
169 | */
170 | public String validate(String token) {
171 | String sessionToken = client.hget(SESSION_TOKEN_KEY, userId);
172 | String expireTimeStr = client.hget(SESSION_EXPIRE_TS_KEY, userId);
173 |
174 | if (sessionToken == null || expireTimeStr == null) {
175 | return SESSION_NOT_LOGIN;
176 | }
177 |
178 | Long expireTime = Long.parseLong(expireTimeStr);
179 | if (Instant.now().getEpochSecond() > expireTime) {
180 | return SESSION_EXPIRE;
181 | }
182 |
183 | if (sessionToken.equals(token)) {
184 | return SESSION_TOKEN_CORRECT;
185 | }
186 | return SESSION_TOKEN_INCORRECT;
187 | }
188 |
189 | /**
190 | * 销毁会话
191 | */
192 | public void destroy() {
193 | client.hdel(SESSION_TOKEN_KEY, userId);
194 | client.hdel(SESSION_EXPIRE_TS_KEY, userId);
195 | }
196 |
197 |
198 | public static void main(String[] args) {
199 | Jedis client = new Jedis();
200 | LoginSession session = new LoginSession(client, "bingo");
201 |
202 | String token = session.create();
203 | // b0bc7274a7af7a8b9aba7e25f08a80782654e9fa8b23cd4b9f417200a412c9a6
204 | System.out.println(token);
205 |
206 | String res = session.validate("this is a wrong token");
207 | // SESSION_TOKEN_INCORRECT
208 | System.out.println(res);
209 |
210 | res = session.validate(token);
211 | // SESSION_TOKEN_CORRECT
212 | System.out.println(res);
213 |
214 | session.destroy();
215 | res = session.validate(token);
216 | // SESSION_NOT_LOGIN
217 | System.out.println(res);
218 | }
219 | }
220 | ```
221 |
--------------------------------------------------------------------------------
/docs/redis-hash-shorten-url.md:
--------------------------------------------------------------------------------
1 | # 利用 Redis 哈希实现短网址
2 |
3 | ## 代码实现
4 |
5 | | [Python](#Python-版本) | [Java](#Java-版本) |
6 | | ---------------------- | ------------------ |
7 |
8 | ### Python 版本
9 |
10 | ```python
11 | from redis import Redis
12 |
13 |
14 | # 10进制数转换为36进制字符串
15 | def base10_to_base36(number: int) -> str:
16 | alphabets = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
17 | result = ''
18 | while number != 0:
19 | number, i = divmod(number, 36)
20 | result = alphabets[i] + result
21 |
22 | return result or alphabets[0]
23 |
24 |
25 | URL_HASH_SHORT_SOURCE_KEY = 'url_hash:short_source'
26 | ID_COUNTER = 'short_url:id_counter'
27 |
28 |
29 | class URLShorten:
30 |
31 | def __init__(self, client: Redis):
32 | self.client = client
33 | # 设置初始ID值,保留1-5位的短码,从6位的短码开始生成
34 | self.client.set(ID_COUNTER, 36 ** 5 - 1)
35 |
36 | def shorten(self, source_url: str) -> str:
37 | """对源网址进行缩短,返回短网址ID"""
38 | new_id = self.client.incr(ID_COUNTER)
39 | short_id = base10_to_base36(new_id)
40 | self.client.hset(URL_HASH_SHORT_SOURCE_KEY, short_id, source_url)
41 | return short_id
42 |
43 | def restore(self, short_id: str) -> str:
44 | """根据短网址ID,返回对应的源网址"""
45 | return self.client.hget(URL_HASH_SHORT_SOURCE_KEY, short_id)
46 |
47 |
48 | if __name__ == '__main__':
49 | redis = Redis(decode_responses=True)
50 | url_shorten = URLShorten(redis)
51 |
52 | short_id = url_shorten.shorten('https://github.com/yanglbme')
53 | print(short_id) # 100000
54 | source_url = url_shorten.restore(short_id)
55 | print(source_url) # https://github.com/yanglbme
56 |
57 | print(url_shorten.shorten('https://doocs.github.io')) # 100001
58 | ```
59 |
60 | ### Java 版本
61 |
62 | ```java
63 | import redis.clients.jedis.Jedis;
64 |
65 | public class URLShorten {
66 | private Jedis client;
67 |
68 | private final String URL_HASH_SHORT_SOURCE_KEY = "url_hash:short_source";
69 | private final String ID_COUNTER = "short_url:id_counter";
70 |
71 | /**
72 | * 将10进制数转换为36进制字符串
73 | *
74 | * @param number 10进制数
75 | * @return 36进制字符串
76 | */
77 | private String base10ToBase36(long number) {
78 | String alphabets = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
79 | char[] chars = alphabets.toCharArray();
80 | StringBuilder result = new StringBuilder();
81 | while (number != 0) {
82 | int i = (int) number % 36;
83 | number /= 36;
84 | result.insert(0, chars[i]);
85 | }
86 | return result.toString();
87 | }
88 |
89 | public URLShorten(Jedis client) {
90 | this.client = client;
91 | // 设置初始ID值,保留1-5位的短码,从6位的短码开始生成
92 | this.client.set(ID_COUNTER, String.valueOf((long) Math.pow(36, 5) - 1));
93 | }
94 |
95 | /**
96 | * 对源网址进行缩短,返回短网址ID
97 | *
98 | * @param sourceUrl 源网址
99 | * @return 短网址ID
100 | */
101 | public String shorten(String sourceUrl) {
102 | long newId = client.incr(ID_COUNTER);
103 | String shortId = base10ToBase36(newId);
104 | client.hset(URL_HASH_SHORT_SOURCE_KEY, shortId, sourceUrl);
105 | return shortId;
106 | }
107 |
108 | /**
109 | * 根据短网址ID,返回对应的源网址
110 | *
111 | * @param shortId 短网址ID
112 | * @return 源网址
113 | */
114 | public String restore(String shortId) {
115 | return client.hget(URL_HASH_SHORT_SOURCE_KEY, shortId);
116 | }
117 |
118 | public static void main(String[] args) {
119 | Jedis client = new Jedis();
120 | URLShorten urlShorten = new URLShorten(client);
121 |
122 | String shortId = urlShorten.shorten("https://github.com/yanglbme");
123 | System.out.println(shortId); // 100000
124 | String sourceUrl = urlShorten.restore(shortId);
125 | System.out.println(sourceUrl); // https://github.com/yanglbme
126 |
127 | System.out.println(urlShorten.shorten("https://doocs.github.io")); // 100001
128 |
129 | }
130 | }
131 | ```
132 |
--------------------------------------------------------------------------------
/docs/redis-hyperloglog-introduction.md:
--------------------------------------------------------------------------------
1 | # Redis HyperLogLog 结构底层原理剖析
2 |
--------------------------------------------------------------------------------
/docs/redis-interview.md:
--------------------------------------------------------------------------------
1 | # Redis 面试场景
2 |
3 | ## 1. 考察 Redis 基本数据结构
4 |
5 | - **面试官**:Redis 都有哪些数据结构?
6 |
7 | - **候选人**:(小 case,很自信地回答)Redis 有 5 种数据结构,分别是 `String`、`List`、`Hash`、`Set`、`Sorted Set`,其中 `Sorted Set` 是有序集合。
8 |
9 | - **面试官**:(没了??)就这些吗?
10 |
11 | - **候选人**:(心里突然有点慌)好像...没有其他数据结构了吧,我知道的就这些.....
12 |
13 | **分析**:这其实是一个很基础的问题,相信绝大部分同学都能回答出以上 5 种数据结构,但是,如果你对 Redis 有更加深入的了解,那么你应该回答 Redis 还有 `HyperLogLog`、`Bitmap`、`Geo`、`Pub/Sub` 等数据结构,甚至还可以回答你玩儿过 `Redis Module`,像 `BloomFilter`、`RedisSearch`、`Redis-ML`,想必面试官会眼前一亮,嗯,这小伙子还不错嘛,知道这么多。
14 |
15 | ## 2. 考察 Redis 分布式锁
16 |
17 | - **面试官**:那你有没有用过 Redis 分布式锁?
18 |
19 | - **候选人**:用过的。
20 |
21 | - **面试官**:说一下你是怎么用 Redis 来实现分布式锁的。
22 |
23 | - **候选人**:就是用 `setnx` 命令来争夺锁,如果抢到了,再用 `expire` 给锁加上一个过期时间,防止锁忘记释放。
24 |
25 | - **面试官**:那如果在 `setnx` 之后执行 `expire` 之前进程意外 `crash` 或者要重启维护了,要怎么做?
26 |
27 | - **候选人**:啊,还有这种情况吗?我就是在系统中简单用用,没有考虑那么多......
28 |
29 | - **面试官**:那就算没有发生意外 `crash`,有没有可能出现不安全的问题,比如,锁的超时?导致多个线程执行顺序错乱?
30 |
31 | - **候选人**:应该不会吧...我之前使用过,没遇到这个问题......
32 |
33 | - **面试官**:(心里觉得这小伙子不行,没遇到,也应该考虑到吧)行吧,那你回去之后好好想想。那 Redis 除了这种方式实现分布式锁,还有其他吗?
34 |
35 | - **候选人**:好像还有个 RedLock 算法,但是我只听说了,没去研究它(心里很没底)。
36 |
37 | - **面试官**:(我还能说什么呢)...
38 |
39 | **分析**:Redis 2.8 版本之后,`setnx` 和 `expire` 可以合并成一条指令执行,锁得不到释放的问题也随着迎刃而解。而超时问题,可以使用“随机值+ Lua 脚本”的方式来处理。以上的一些问题,你都应该考虑到,而不是简单的用用 Redis。而关于 RedLock 算法,也应该去好好了解它的原理和实现方式。
40 |
--------------------------------------------------------------------------------
/docs/redis-list-paginate.md:
--------------------------------------------------------------------------------
1 | # 利用 Redis 列表实现页面数据分页
2 |
3 | ## 代码实现
4 |
5 | | [Python](#Python-版本) | [Java](#Java-版本) |
6 | | ---------------------- | ------------------ |
7 |
8 | ### Python 版本
9 |
10 | ```python
11 | from redis import Redis
12 |
13 |
14 | class Paginate:
15 | def __init__(self, client: Redis, key: str):
16 | self.client = client
17 | self.key = key
18 |
19 | def add(self, item):
20 | """添加元素到分页列表中"""
21 | self.client.lpush(self.key, item)
22 |
23 | def get_page(self, page: int, per_page: int):
24 | """根据页码取出指定数量的元素"""
25 | start = (page - 1) * per_page
26 | end = page * per_page - 1
27 | return self.client.lrange(self.key, start, end)
28 |
29 | def get_size(self):
30 | """获取列表的元素数量"""
31 | return self.client.llen(self.key)
32 |
33 |
34 | if __name__ == '__main__':
35 | redis = Redis(decode_responses=True)
36 | topics = Paginate(redis, 'user-topics')
37 |
38 | for i in range(24):
39 | topics.add(i)
40 |
41 | # 取总数
42 | print(topics.get_size()) # 24
43 |
44 | # 以每页5条数据的方式取出第1页数据
45 | print(topics.get_page(1, 5)) # ['23', '22', '21', '20', '19']
46 |
47 | # 以每页10条数据的方式取出第1页数据
48 | print(topics.get_page(1, 10)) # ['23', '22', '21', '20', '19', '18', '17', '16', '15', '14']
49 |
50 | # 以每页5条数据的方式取出第5页数据
51 | print(topics.get_page(5, 5)) # ['3', '2', '1', '0']
52 |
53 | ```
54 |
55 | ### Java 版本
56 |
57 | ```java
58 | import redis.clients.jedis.Jedis;
59 |
60 | import java.util.List;
61 |
62 | public class Paginate {
63 |
64 | private Jedis client;
65 | private String key;
66 |
67 | public Paginate(Jedis client, String key) {
68 | this.client = client;
69 | this.key = key;
70 | }
71 |
72 | /**
73 | * 添加元素到分页列表中
74 | *
75 | * @param item 元素
76 | */
77 | public void add(String item) {
78 | client.lpush(key, item);
79 | }
80 |
81 | /**
82 | * 根据页码取出指定数量的元素
83 | *
84 | * @param page 页码
85 | * @param pageSize 数量
86 | * @return 元素列表
87 | */
88 | public List getPage(int page, int pageSize) {
89 | long start = (page - 1) * pageSize;
90 | long end = page * pageSize - 1;
91 | return client.lrange(key, start, end);
92 | }
93 |
94 | /**
95 | * 获取列表的元素数量
96 | *
97 | * @return 总数
98 | */
99 | public Long getTotalCount() {
100 | return client.llen(key);
101 | }
102 |
103 | public static void main(String[] args) {
104 | Jedis client = new Jedis();
105 | Paginate topics = new Paginate(client, "user-topics");
106 | for (int i = 0; i < 24; ++i) {
107 | topics.add(i + "");
108 | }
109 |
110 | // 取总数
111 | System.out.println(topics.getTotalCount()); // 24
112 |
113 | // 以每页5条数据的方式取出第1页数据
114 | System.out.println(topics.getPage(1, 5)); // [23, 22, 21, 20, 19]
115 |
116 | // 以每页10条数据的方式取出第1页数据
117 | System.out.println(topics.getPage(1, 10)); // [23, 22, 21, 20, 19, 18, 17, 16, 15, 14]
118 |
119 | // 以每页5条数据的方式取出第5页数据
120 | System.out.println(topics.getPage(5, 5)); // [3, 2, 1, 0]
121 | }
122 | }
123 | ```
124 |
--------------------------------------------------------------------------------
/docs/redis-set-introduction.md:
--------------------------------------------------------------------------------
1 | # Redis Set 结构底层原理剖析
2 |
--------------------------------------------------------------------------------
/docs/redis-set-like-and-dislike.md:
--------------------------------------------------------------------------------
1 | # 利用 Redis Set 实现点赞点踩功能
2 |
3 | ## 代码实现
4 |
5 | | [Python](#Python-版本) | [Java](#Java-版本) |
6 | | ---------------------- | ------------------ |
7 |
8 | ### Python 版本
9 |
10 | ```python
11 | from redis import Redis
12 |
13 |
14 | def get_like_key(entity_id):
15 | return 'like:{}'.format(entity_id)
16 |
17 |
18 | def get_dislike_key(entity_id):
19 | return 'dislike:{}'.format(entity_id)
20 |
21 |
22 | class Like:
23 | def __init__(self, client: Redis, entity_id: str):
24 | self.client = client
25 | self.entity_id = entity_id
26 |
27 | def like(self, user_id) -> int:
28 | """某用户点赞"""
29 | self.client.sadd(get_like_key(self.entity_id), user_id)
30 | self.client.srem(get_dislike_key(self.entity_id), user_id)
31 | return self.client.scard(get_like_key(self.entity_id))
32 |
33 | def dislike(self, user_id) -> int:
34 | """某用户点踩"""
35 | self.client.sadd(get_dislike_key(self.entity_id), user_id)
36 | self.client.srem(get_like_key(self.entity_id), user_id)
37 | return self.client.scard(get_like_key(self.entity_id))
38 |
39 | def get_like_status(self, user_id) -> int:
40 | """判断用户是否点赞或点踩了,点赞返回1,点踩返回-1,不赞不踩返回0"""
41 | if self.client.sismember(get_like_key(self.entity_id), user_id):
42 | return 1
43 | if self.client.sismember(get_dislike_key(self.entity_id), user_id):
44 | return -1
45 | return 0
46 |
47 | def get_like_count(self) -> int:
48 | """获取当前点赞数"""
49 | return self.client.scard(get_like_key(self.entity_id))
50 |
51 |
52 | if __name__ == '__main__':
53 | redis = Redis(decode_responses=True)
54 | redis.flushall()
55 | like_entity = Like(redis, 'user1')
56 | print(like_entity.get_like_count()) # 0
57 | like_entity.like('user2')
58 | like_entity.like('user3')
59 |
60 | like_entity.dislike('user4')
61 |
62 | print(like_entity.get_like_count()) # 2
63 | print(like_entity.get_like_status('user2')) # 1
64 | print(like_entity.get_like_status('user4')) # -1
65 |
66 |
67 | ```
68 |
69 | ### Java 版本
70 |
71 | ```java
72 | import redis.clients.jedis.Jedis;
73 | import utils.JedisUtils;
74 |
75 | public class LikeService {
76 | private Jedis client = JedisUtils.getClient();
77 |
78 | public LikeService() {
79 |
80 | }
81 |
82 | private String getLikeKey(String entityId) {
83 | return "like:" + entityId;
84 | }
85 |
86 | private String getDislikeKey(String entityId) {
87 | return "dislike:" + entityId;
88 | }
89 |
90 | /**
91 | * 用户点赞了某个实体
92 | *
93 | * @param userId 用户id
94 | * @param entityId 实体id
95 | * @return 实体的点赞人数
96 | */
97 | public Long like(String userId, String entityId) {
98 | client.sadd(getLikeKey(entityId), userId);
99 | client.srem(getDislikeKey(entityId), userId);
100 | return client.scard(getLikeKey(entityId));
101 | }
102 |
103 | /**
104 | * 用户点踩了某个实体
105 | *
106 | * @param userId 用户id
107 | * @param entityId 实体id
108 | * @return 实体的点赞人数
109 | */
110 | public Long dislike(String userId, String entityId) {
111 | client.sadd(getDislikeKey(entityId), userId);
112 | client.srem(getLikeKey(entityId), userId);
113 | return client.scard(getLikeKey(entityId));
114 | }
115 |
116 | /**
117 | * 用户对某个实体的赞踩状态,点赞1,点踩-1,不赞不踩0
118 | *
119 | * @param userId 用户id
120 | * @param entityId 实体id
121 | * @return 赞踩状态
122 | */
123 | public int getLikeStatus(String userId, String entityId) {
124 | if (client.sismember(getLikeKey(entityId), userId)) {
125 | return 1;
126 | }
127 | if (client.sismember(getDislikeKey(entityId), userId)) {
128 | return -1;
129 | }
130 | return 0;
131 | }
132 |
133 | /**
134 | * 获取某实体的点赞人数
135 | *
136 | * @param entityId 实体id
137 | * @return 点赞人数
138 | */
139 | public Long getLikeCount(String entityId) {
140 | return client.scard(getLikeKey(entityId));
141 | }
142 |
143 | public static void main(String[] args){
144 | LikeService likeService = new LikeService();
145 |
146 | String entityId = "user1";
147 | System.out.println(likeService.getLikeCount(entityId)); // 0
148 |
149 | likeService.like("user2", entityId);
150 | likeService.like("user3", entityId);
151 |
152 | likeService.dislike("user4", entityId);
153 |
154 | System.out.println(likeService.getLikeCount(entityId)); // 2
155 | System.out.println(likeService.getLikeStatus("user2", entityId)); // 1
156 |
157 | System.out.println(likeService.getLikeStatus("user4", entityId)); // -1
158 | }
159 | }
160 |
161 | ```
162 |
--------------------------------------------------------------------------------
/docs/redis-sorted-set-auto-complete.md:
--------------------------------------------------------------------------------
1 | # 利用 Redis Sorted Set 实现自动补全
2 |
3 | ## 代码实现
4 |
5 | | [Python](#Python-版本) | [Java](#Java-版本) |
6 | | ---------------------- | ------------------ |
7 |
8 | ### Python 版本
9 |
10 | ```python
11 | from redis import Redis
12 |
13 |
14 | class AutoComplete:
15 | def __init__(self, client: Redis):
16 | self.client = client
17 |
18 | def feed(self, keyword: str, weight):
19 | for i in range(1, len(keyword)):
20 | key = 'auto_complete:' + keyword[:i]
21 | self.client.zincrby(key, weight, keyword)
22 |
23 | def hint(self, prefix: str, count=10):
24 | key = 'auto_complete:' + prefix
25 | return self.client.zrevrange(key, 0, count - 1)
26 |
27 |
28 | if __name__ == '__main__':
29 | redis = Redis(decode_responses=True)
30 | redis.flushall()
31 | ac = AutoComplete(redis)
32 |
33 | ac.feed('张艺兴', 5000)
34 | ac.feed('张艺谋', 3000)
35 | ac.feed('张三', 500)
36 |
37 | print(ac.hint('张')) # ['张艺兴', '张艺谋', '张三']
38 | print(ac.hint('张艺')) # ['张艺兴', '张艺谋']
39 | ```
40 |
41 | ### Java 版本
42 |
43 | ```java
44 | import redis.clients.jedis.Jedis;
45 |
46 | import java.util.Set;
47 |
48 | public class AutoComplete {
49 | private Jedis client = new Jedis();
50 |
51 | public AutoComplete() {
52 |
53 | }
54 |
55 | /**
56 | * 根据关键词设置分词权重
57 | *
58 | * @param keyword 关键词
59 | * @param weight 权重
60 | */
61 | public void feed(String keyword, double weight) {
62 | int len = keyword.length();
63 | for (int i = 1; i < len; ++i) {
64 | String key = "auto_complete:" + keyword.substring(0, i);
65 | client.zincrby(key, weight, keyword);
66 | }
67 | }
68 |
69 | /**
70 | * 根据前缀获取自动补全的结果
71 | *
72 | * @param prefix 前缀
73 | * @param count 数量
74 | * @return 结果集合
75 | */
76 | public Set hint(String prefix, long count) {
77 | String key = "auto_complete:" + prefix;
78 | return client.zrevrange(key, 0, count - 1);
79 | }
80 |
81 | public static void main(String[] args) {
82 | AutoComplete ac = new AutoComplete();
83 | ac.feed("张艺兴", 5000);
84 | ac.feed("张艺谋", 3000);
85 | ac.feed("张三", 500);
86 |
87 | System.out.println(ac.hint("张", 10)); // [张艺兴, 张艺谋, 张三]
88 | System.out.println(ac.hint("张艺", 10)); // [张艺兴, 张艺谋]
89 | }
90 | }
91 | ```
92 |
--------------------------------------------------------------------------------
/docs/redis-sorted-set-introduction.md:
--------------------------------------------------------------------------------
1 | # Redis Sorted Set 结构底层原理剖析
2 |
--------------------------------------------------------------------------------
/docs/redis-sorted-set-ranking-or-trending-list.md:
--------------------------------------------------------------------------------
1 | # 利用 Redis Sorted Set 实现日/周/月排行榜
2 |
3 | ## 代码实现
4 |
5 | | [Python](#Python-版本) | [Java](#Java-版本) |
6 | | ---------------------- | ------------------ |
7 |
8 | ### Python 版本
9 |
10 | ```python
11 | import datetime
12 | import time
13 |
14 | from redis import Redis
15 |
16 |
17 | def get_day_rank_key():
18 | return 'rank:day:{}'.format(datetime.date.today().strftime('%Y%m%d'))
19 |
20 |
21 | def get_week_rank_key():
22 | return 'rank:week:{}{}'.format(datetime.date.today().strftime('%Y'), time.strftime("%W"))
23 |
24 |
25 | def get_month_rank_key():
26 | today = datetime.date.today()
27 | return 'rank:month:{}{}'.format(today.strftime('%Y'), today.strftime('%m'))
28 |
29 |
30 | class Ranking:
31 | def __init__(self, client: Redis):
32 | self.client = client
33 |
34 | def incr(self, user, score=1):
35 | self.client.zincrby(get_day_rank_key(), score, user)
36 | self.client.zincrby(get_week_rank_key(), score, user)
37 | self.client.zincrby(get_month_rank_key(), score, user)
38 |
39 | def get_today_top_n(self, n, with_scores=False):
40 | """获取日榜单topN"""
41 | return self.client.zrevrange(get_day_rank_key(), 0, n - 1, withscores=with_scores)
42 |
43 | def get_week_top_n(self, n, with_scores=False):
44 | """获取周榜单topN"""
45 | return self.client.zrevrange(get_week_rank_key(), 0, n - 1, withscores=with_scores)
46 |
47 | def get_month_top_n(self, n, with_scores=False):
48 | """获取月榜单topN"""
49 | return self.client.zrevrange(get_month_rank_key(), 0, n - 1, withscores=with_scores)
50 |
51 |
52 | if __name__ == '__main__':
53 | redis = Redis(decode_responses=True)
54 | ranking = Ranking(redis)
55 |
56 | ranking.incr('bingo', 5)
57 | ranking.incr('iris', 3)
58 | ranking.incr('lily', 4)
59 | ranking.incr('helen', 6)
60 |
61 | print(ranking.get_today_top_n(n=2, with_scores=True)) # [('helen', 6.0), ('bingo', 5.0)]
62 | print(ranking.get_week_top_n(n=2, with_scores=True)) # [('helen', 6.0), ('bingo', 5.0)]
63 | print(ranking.get_month_top_n(n=3, with_scores=True)) # [('helen', 6.0), ('bingo', 5.0), ('lily', 4.0)]
64 | ```
65 |
66 | ### Java 版本
67 |
68 | ```java
69 | import redis.clients.jedis.Jedis;
70 | import redis.clients.jedis.Tuple;
71 |
72 | import java.util.Calendar;
73 | import java.util.Set;
74 |
75 | /**
76 | * @author bingoyang
77 | * @date 2019/9/7
78 | */
79 | public class Ranking {
80 | private Jedis client = new Jedis();
81 | private Calendar calendar = Calendar.getInstance();
82 |
83 | public Ranking() {
84 | calendar.setFirstDayOfWeek(Calendar.MONDAY);
85 | }
86 |
87 | private String getDayRankKey() {
88 | return String.format("rank:day:%s%s%s", calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF_MONTH));
89 | }
90 |
91 | private String getWeekRankKey() {
92 | return String.format("rank:week:%s%s", calendar.get(Calendar.YEAR), calendar.get(Calendar.WEEK_OF_YEAR));
93 | }
94 |
95 | private String getMonthRankKey() {
96 | return String.format("rank:month:%s%s", calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH));
97 | }
98 |
99 | public void incr(String user, double score) {
100 | client.zincrby(getDayRankKey(), score, user);
101 | client.zincrby(getWeekRankKey(), score, user);
102 | client.zincrby(getMonthRankKey(), score, user);
103 | }
104 |
105 | /**
106 | * 获取日榜单topN
107 | *
108 | * @param n 前n位
109 | * @return 结果集合
110 | */
111 | public Set getTodayTopNWithScores(long n) {
112 | return client.zrevrangeWithScores(getDayRankKey(), 0, n - 1);
113 | }
114 |
115 | /**
116 | * 获取周榜单topN
117 | *
118 | * @param n 前n位
119 | * @return 结果集合
120 | */
121 | public Set getWeekTopNWithScores(long n) {
122 | return client.zrevrangeWithScores(getWeekRankKey(), 0, n - 1);
123 | }
124 |
125 | /**
126 | * 获取月榜单topN
127 | *
128 | * @param n 前n位
129 | * @return 结果集合
130 | */
131 | public Set getMonthTopNWithScores(long n) {
132 | return client.zrevrangeWithScores(getMonthRankKey(), 0, n - 1);
133 | }
134 |
135 | public static void main(String[] args) {
136 | Ranking ranking = new Ranking();
137 |
138 | ranking.incr("bingo", 5);
139 | ranking.incr("iris", 3);
140 | ranking.incr("lily", 4);
141 | ranking.incr("helen", 6);
142 |
143 | System.out.println(ranking.getTodayTopNWithScores(2)); // [[helen,6.0], [bingo,5.0]]
144 | System.out.println(ranking.getWeekTopNWithScores(2)); // [[helen,6.0], [bingo,5.0]]
145 | System.out.println(ranking.getMonthTopNWithScores(3)); // [[helen,6.0], [bingo,5.0], [lily,4.0]]
146 |
147 | }
148 | }
149 | ```
150 |
--------------------------------------------------------------------------------
/docs/redis-sorted-set-sns-follow.md:
--------------------------------------------------------------------------------
1 | # 利用 Redis Sorted Set 实现粉丝关注功能
2 |
3 | 为什么选用 Sorted Set?其点有三:
4 |
5 | - Sorted Set 有序,系统可以根据用户关注实体的时间倒序排列,获取最新的关注列表;
6 | - Sorted Set 去重,用户不能重复关注同一个实体;
7 | - Sorted Set 可以获取两用户之间的共同关注。
8 |
9 | ## 代码实现
10 |
11 | | [Python](#Python-版本) | [Java](#Java-版本) |
12 | | ---------------------- | ------------------ |
13 |
14 | ### Python 版本
15 |
16 | ```python
17 | from time import time
18 |
19 | from redis import Redis
20 |
21 |
22 | def following_key(user):
23 | return 'following:' + user
24 |
25 |
26 | def followers_key(user):
27 | return 'followers:' + user
28 |
29 |
30 | def common_following_key(user1, user2):
31 | return 'common:following:{}:{}'.format(user1, user2)
32 |
33 |
34 | class SocialRelationship:
35 | def __init__(self, client: Redis, user):
36 | self.client = client
37 | self.user = user
38 |
39 | def follow(self, target):
40 | """关注目标用户"""
41 | self.client.zadd(following_key(self.user), {target: time()})
42 | self.client.zadd(followers_key(target), {self.user: time()})
43 |
44 | def unfollow(self, target):
45 | """取关目标用户"""
46 | self.client.zrem(following_key(self.user), target)
47 | self.client.zrem(followers_key(target), self.user)
48 |
49 | def is_following(self, target):
50 | """判断是否关注着目标用户"""
51 | return self.client.zrank(following_key(self.user), target) is not None
52 |
53 | def get_all_following(self):
54 | """获取当前用户关注的所有人,并按最近关注时间倒序排列"""
55 | return self.client.zrevrange(following_key(self.user), 0, -1)
56 |
57 | def get_all_followers(self):
58 | """获取当前用户的所有粉丝,并按最近关注时间倒序排列"""
59 | return self.client.zrevrange(followers_key(self.user), 0, -1)
60 |
61 | def count_following(self):
62 | """获取当前用户关注的人数"""
63 | return self.client.zcard(following_key(self.user))
64 |
65 | def count_followers(self):
66 | """获取当前用户的粉丝数"""
67 | return self.client.zcard(followers_key(self.user))
68 |
69 | def get_common_following(self, one):
70 | """获取与某用户的共同关注"""
71 | common_key = common_following_key(self.user, one)
72 | self.client.zinterstore(common_key, (following_key(self.user), following_key(one)))
73 | return self.client.zrevrange(common_key, 0, -1)
74 |
75 |
76 | if __name__ == '__main__':
77 | redis = Redis(decode_responses=True)
78 | bingo = SocialRelationship(redis, 'Bingo')
79 | iris = SocialRelationship(redis, 'Iris')
80 | bingo.follow('Iris')
81 | bingo.follow('GitHub')
82 | bingo.follow('Apple')
83 | iris.follow('Bingo')
84 | iris.follow('GitHub')
85 |
86 | print(bingo.is_following('Iris')) # True
87 | print(bingo.get_all_following()) # ['Apple', 'GitHub', 'Iris']
88 | print(iris.is_following('Bingo')) # True
89 | print(bingo.count_following()) # 3
90 |
91 | print(bingo.get_common_following('Iris')) # ['GitHub']
92 | ```
93 |
94 | ### Java 版本
95 |
96 | - JedisUtils.java
97 |
98 | ```java
99 | import redis.clients.jedis.Jedis;
100 |
101 | public class JedisUtils {
102 |
103 | public static Jedis getClient() {
104 | return new Jedis();
105 | }
106 |
107 | public static String getFollowingKey(String user) {
108 | return "following:" + user;
109 | }
110 |
111 | public static String getFollowersKey(String user) {
112 | return "followers:" + user;
113 | }
114 |
115 | public static String getCommonFollowingKey(String user1, String user2) {
116 | return String.format("common:following:%s:%s", user1, user2);
117 | }
118 | }
119 | ```
120 |
121 | - SocialRelationship.java
122 |
123 | ```java
124 | import redis.clients.jedis.Jedis;
125 |
126 | import java.util.Set;
127 |
128 | public class SocialRelationship {
129 | private Jedis client;
130 | private String user;
131 |
132 | public SocialRelationship(Jedis client, String user) {
133 | this.client = client;
134 | this.user = user;
135 | }
136 |
137 | /**
138 | * 关注目标用户
139 | *
140 | * @param target 用户
141 | */
142 | public void follow(String target) {
143 | String followingKey = JedisUtils.getFollowingKey(user);
144 | String followersKey = JedisUtils.getFollowersKey(target);
145 | long now = System.currentTimeMillis();
146 | client.zadd(followingKey, now, target);
147 | client.zadd(followersKey, now, user);
148 | }
149 |
150 | /**
151 | * 取关目标用户
152 | *
153 | * @param target 用户
154 | */
155 | public void unfollow(String target) {
156 | String followingKey = JedisUtils.getFollowingKey(user);
157 | String followersKey = JedisUtils.getFollowersKey(target);
158 | client.zrem(followingKey, target);
159 | client.zrem(followersKey, user);
160 | }
161 |
162 | /**
163 | * 判断是否关注着目标用户
164 | *
165 | * @param target 用户
166 | * @return 是否关注
167 | */
168 | public boolean isFollowing(String target) {
169 | String followingKey = JedisUtils.getFollowingKey(user);
170 | return client.zrank(followingKey, target) != null;
171 |
172 | }
173 |
174 | /**
175 | * 获取当前用户关注的所有人,并按最近关注时间倒序排列
176 | *
177 | * @return 关注人集合
178 | */
179 | public Set getAllFollowing() {
180 | String followingKey = JedisUtils.getFollowingKey(user);
181 | return client.zrevrange(followingKey, 0, -1);
182 | }
183 |
184 | /**
185 | * 获取当前用户的所有粉丝,并按最近关注时间倒序排列
186 | *
187 | * @return 粉丝集合
188 | */
189 | public Set getAllFollowers() {
190 | String followersKey = JedisUtils.getFollowersKey(user);
191 | return client.zrevrange(followersKey, 0, -1);
192 | }
193 |
194 | /**
195 | * 获取当前用户关注的人数
196 | *
197 | * @return 人数
198 | */
199 | public Long countFollowing() {
200 | String followingKey = JedisUtils.getFollowingKey(user);
201 | return client.zcard(followingKey);
202 | }
203 |
204 | /**
205 | * 获取当前用户的粉丝数
206 | *
207 | * @return 人数
208 | */
209 | public Long countFollowers() {
210 | String followersKey = JedisUtils.getFollowersKey(user);
211 | return client.zcard(followersKey);
212 | }
213 |
214 | /**
215 | * 获取与某用户的共同关注
216 | *
217 | * @param one 用户
218 | * @return 共同关注的用户集合
219 | */
220 | public Set getCommonFollowing(String one) {
221 | String commonKey = JedisUtils.getCommonFollowingKey(user, one);
222 | client.zinterstore(commonKey, JedisUtils.getFollowingKey(user), JedisUtils.getFollowingKey(one));
223 | return client.zrevrange(commonKey, 0, -1);
224 | }
225 |
226 | public static void main(String[] args) {
227 | Jedis client = JedisUtils.getClient();
228 | SocialRelationship bingo = new SocialRelationship(client, "Bingo");
229 | SocialRelationship iris = new SocialRelationship(client, "Iris");
230 | bingo.follow("Iris");
231 | bingo.follow("GitHub");
232 | bingo.follow("Apple");
233 | iris.follow("Bingo");
234 | iris.follow("GitHub");
235 |
236 | System.out.println(bingo.isFollowing("Iris")); // true
237 | System.out.println(bingo.getAllFollowing()); // [Apple, GitHub, Iris]
238 | System.out.println(iris.isFollowing("Bingo")); // true
239 | System.out.println(bingo.countFollowing()); // 3
240 |
241 | System.out.println(bingo.getCommonFollowing("Iris")); // [GitHub]
242 | }
243 | }
244 | ```
245 |
--------------------------------------------------------------------------------
/docs/redis-string-introduction.md:
--------------------------------------------------------------------------------
1 | # Redis String 结构底层原理剖析
2 |
--------------------------------------------------------------------------------
/images/doocs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yanglbme/redis-multi-programming-language-practice/2425ee7e1b1f193f7b5e57ad57e3b59f9adfd329/images/doocs.png
--------------------------------------------------------------------------------
/images/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yanglbme/redis-multi-programming-language-practice/2425ee7e1b1f193f7b5e57ad57e3b59f9adfd329/images/favicon-16x16.png
--------------------------------------------------------------------------------
/images/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yanglbme/redis-multi-programming-language-practice/2425ee7e1b1f193f7b5e57ad57e3b59f9adfd329/images/favicon-32x32.png
--------------------------------------------------------------------------------
/images/owner-favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yanglbme/redis-multi-programming-language-practice/2425ee7e1b1f193f7b5e57ad57e3b59f9adfd329/images/owner-favicon-16x16.png
--------------------------------------------------------------------------------
/images/owner-favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yanglbme/redis-multi-programming-language-practice/2425ee7e1b1f193f7b5e57ad57e3b59f9adfd329/images/owner-favicon-32x32.png
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Redis 底层原理分析与多语言应用实践
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
18 |
19 |
20 | Redis 底层原理分析与多语言应用实践
21 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/java/README.md:
--------------------------------------------------------------------------------
1 | # Java 文件目录树
2 | ```bash
3 | ├─main
4 | │ ├─java
5 | │ │ │ AutoComplete.java
6 | │ │ │ DistributedLock.java
7 | │ │ │ LoginSession.java
8 | │ │ │ Paginate.java
9 | │ │ │ Ranking.java
10 | │ │ │ SocialRelationship.java
11 | │ │ │ URLShorten.java
12 | │ │ │
13 | │ │ └─utils
14 | │ │ JedisUtils.java
15 | │ │
16 | │ └─resources
17 | └─test
18 | ├─java
19 | │ AutoCompleteTest.java
20 | │ DistributedLockTest.java
21 | │ LoginSessionTest.java
22 | │ PaginateTest.java
23 | │ RankingTest.java
24 | │ SocialRelationshipTest.java
25 | │ URLShortenTest.java
26 | │
27 | └─resources
28 |
29 | ```
--------------------------------------------------------------------------------
/java/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'java'
3 | }
4 |
5 | version '1.0-SNAPSHOT'
6 |
7 | sourceCompatibility = 1.8
8 |
9 | repositories {
10 | mavenCentral()
11 | }
12 |
13 | dependencies {
14 | testCompile group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.5.2'
15 | compile group: 'redis.clients', name: 'jedis', version: '3.1.0'
16 | compile group: 'commons-codec', name: 'commons-codec', version: '1.13'
17 | compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.9'
18 | }
19 |
--------------------------------------------------------------------------------
/java/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yanglbme/redis-multi-programming-language-practice/2425ee7e1b1f193f7b5e57ad57e3b59f9adfd329/java/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/java/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.8-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/java/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/java/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/java/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = 'java'
2 |
3 |
--------------------------------------------------------------------------------
/java/src/main/java/AutoComplete.java:
--------------------------------------------------------------------------------
1 | import redis.clients.jedis.Jedis;
2 | import utils.JedisUtils;
3 |
4 | import java.util.Set;
5 |
6 | public class AutoComplete {
7 | private Jedis client = JedisUtils.getClient();
8 |
9 | public AutoComplete() {
10 | }
11 |
12 | /**
13 | * 根据关键词设置分词权重
14 | *
15 | * @param keyword 关键词
16 | * @param weight 权重
17 | */
18 | public void feed(String keyword, double weight) {
19 | int len = keyword.length();
20 | for (int i = 1; i < len; ++i) {
21 | String key = "auto_complete:" + keyword.substring(0, i);
22 | client.zincrby(key, weight, keyword);
23 | }
24 | }
25 |
26 | /**
27 | * 根据前缀获取自动补全的结果
28 | *
29 | * @param prefix 前缀
30 | * @param count 数量
31 | * @return 结果集合
32 | */
33 | public Set hint(String prefix, long count) {
34 | String key = "auto_complete:" + prefix;
35 | return client.zrevrange(key, 0, count - 1);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/java/src/main/java/DistributedLock.java:
--------------------------------------------------------------------------------
1 | import redis.clients.jedis.Jedis;
2 | import redis.clients.jedis.params.SetParams;
3 | import utils.JedisUtils;
4 |
5 | import java.util.UUID;
6 |
7 | public class DistributedLock {
8 |
9 | private Jedis client = JedisUtils.getClient();
10 | private String key;
11 |
12 | public DistributedLock(String key) {
13 | this.key = key;
14 | }
15 |
16 | /**
17 | * 获取随机字符串值
18 | *
19 | * @return 随机字符串
20 | */
21 | private String generateRandomValue() {
22 | return UUID.randomUUID().toString().replaceAll("-", "");
23 | }
24 |
25 | /**
26 | * 尝试获取锁
27 | *
28 | * @param timeout 过期时长
29 | * @return 是否获取成功
30 | */
31 | public boolean acquire(int timeout) {
32 | SetParams params = new SetParams().ex(timeout).nx();
33 | return client.set(key, generateRandomValue(), params) != null;
34 | }
35 |
36 | public boolean acquire() {
37 | int timeout = 10;
38 | return acquire(timeout);
39 | }
40 |
41 | /**
42 | * 尝试释放锁
43 | *
44 | * @return 是否成功释放锁
45 | */
46 | public boolean release() {
47 | return client.del(key) == 1;
48 | }
49 | }
--------------------------------------------------------------------------------
/java/src/main/java/LikeService.java:
--------------------------------------------------------------------------------
1 | import redis.clients.jedis.Jedis;
2 | import utils.JedisUtils;
3 |
4 | public class LikeService {
5 | private Jedis client = JedisUtils.getClient();
6 |
7 | public LikeService() {
8 |
9 | }
10 |
11 | private String getLikeKey(String entityId) {
12 | return "like:" + entityId;
13 | }
14 |
15 | private String getDislikeKey(String entityId) {
16 | return "dislike:" + entityId;
17 | }
18 |
19 | /**
20 | * 用户点赞了某个实体
21 | *
22 | * @param userId 用户id
23 | * @param entityId 实体id
24 | * @return 实体的点赞人数
25 | */
26 | public Long like(String userId, String entityId) {
27 | client.sadd(getLikeKey(entityId), userId);
28 | client.srem(getDislikeKey(entityId), userId);
29 | return client.scard(getLikeKey(entityId));
30 | }
31 |
32 | /**
33 | * 用户点踩了某个实体
34 | *
35 | * @param userId 用户id
36 | * @param entityId 实体id
37 | * @return 实体的点赞人数
38 | */
39 | public Long dislike(String userId, String entityId) {
40 | client.sadd(getDislikeKey(entityId), userId);
41 | client.srem(getLikeKey(entityId), userId);
42 | return client.scard(getLikeKey(entityId));
43 | }
44 |
45 | /**
46 | * 用户对某个实体的赞踩状态,点赞1,点踩-1,不赞不踩0
47 | *
48 | * @param userId 用户id
49 | * @param entityId 实体id
50 | * @return 赞踩状态
51 | */
52 | public int getLikeStatus(String userId, String entityId) {
53 | if (client.sismember(getLikeKey(entityId), userId)) {
54 | return 1;
55 | }
56 | if (client.sismember(getDislikeKey(entityId), userId)) {
57 | return -1;
58 | }
59 | return 0;
60 | }
61 |
62 | /**
63 | * 获取某实体的点赞人数
64 | *
65 | * @param entityId 实体id
66 | * @return 点赞人数
67 | */
68 | public Long getLikeCount(String entityId) {
69 | return client.scard(getLikeKey(entityId));
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/java/src/main/java/LoginSession.java:
--------------------------------------------------------------------------------
1 | import org.apache.commons.codec.binary.Hex;
2 | import redis.clients.jedis.Jedis;
3 | import utils.JedisUtils;
4 |
5 | import java.security.MessageDigest;
6 | import java.security.NoSuchAlgorithmException;
7 | import java.time.Instant;
8 | import java.util.Random;
9 |
10 | public class LoginSession {
11 |
12 | private final String SESSION_TOKEN_KEY = "SESSION:TOKEN";
13 | private final String SESSION_EXPIRE_TS_KEY = "SESSION:EXPIRE";
14 |
15 | private final String SESSION_NOT_LOGIN = "SESSION_NOT_LOGIN";
16 | private final String SESSION_EXPIRE = "SESSION_EXPIRE";
17 | private final String SESSION_TOKEN_CORRECT = "SESSION_TOKEN_CORRECT";
18 | private final String SESSION_TOKEN_INCORRECT = "SESSION_TOKEN_INCORRECT";
19 |
20 | private Jedis client = JedisUtils.getClient();
21 | private String userId;
22 |
23 | public LoginSession(String userId) {
24 | this.userId = userId;
25 | }
26 |
27 | /**
28 | * 生成随机token
29 | *
30 | * @return token
31 | */
32 | private String generateToken() {
33 | byte[] b = new byte[256];
34 | new Random().nextBytes(b);
35 |
36 | MessageDigest messageDigest;
37 |
38 | String sessionToken = "";
39 | try {
40 | messageDigest = MessageDigest.getInstance("SHA-256");
41 | byte[] hash = messageDigest.digest(b);
42 | sessionToken = Hex.encodeHexString(hash);
43 | } catch (NoSuchAlgorithmException e) {
44 | e.printStackTrace();
45 | }
46 | return sessionToken;
47 | }
48 |
49 | /**
50 | * 创建会话,并返回token
51 | *
52 | * @param timeout 过期时长
53 | * @return token
54 | */
55 | public String create(int timeout) {
56 | String token = generateToken();
57 | long expireTime = Instant.now().getEpochSecond() + timeout;
58 | client.hset(SESSION_TOKEN_KEY, userId, token);
59 | client.hset(SESSION_EXPIRE_TS_KEY, userId, String.valueOf(expireTime));
60 | return token;
61 | }
62 |
63 | public String create() {
64 | // 设置默认过期时长
65 | int defaultTimeout = 30 * 24 * 3600;
66 | return create(defaultTimeout);
67 | }
68 |
69 | /**
70 | * 校验token
71 | *
72 | * @param token 输入的token
73 | * @return 校验结果
74 | */
75 | public String validate(String token) {
76 | String sessionToken = client.hget(SESSION_TOKEN_KEY, userId);
77 | String expireTimeStr = client.hget(SESSION_EXPIRE_TS_KEY, userId);
78 |
79 | if (sessionToken == null || expireTimeStr == null) {
80 | return SESSION_NOT_LOGIN;
81 | }
82 |
83 | Long expireTime = Long.parseLong(expireTimeStr);
84 | if (Instant.now().getEpochSecond() > expireTime) {
85 | return SESSION_EXPIRE;
86 | }
87 |
88 | if (sessionToken.equals(token)) {
89 | return SESSION_TOKEN_CORRECT;
90 | }
91 | return SESSION_TOKEN_INCORRECT;
92 | }
93 |
94 | /**
95 | * 销毁会话
96 | */
97 | public void destroy() {
98 | client.hdel(SESSION_TOKEN_KEY, userId);
99 | client.hdel(SESSION_EXPIRE_TS_KEY, userId);
100 | }
101 | }
--------------------------------------------------------------------------------
/java/src/main/java/Paginate.java:
--------------------------------------------------------------------------------
1 | import redis.clients.jedis.Jedis;
2 | import utils.JedisUtils;
3 |
4 | import java.util.List;
5 |
6 | public class Paginate {
7 |
8 | private Jedis client = JedisUtils.getClient();
9 | private String key;
10 |
11 | public Paginate(String key) {
12 | this.key = key;
13 | }
14 |
15 | /**
16 | * 添加元素到分页列表中
17 | *
18 | * @param item 元素
19 | */
20 | public void add(String item) {
21 | client.lpush(key, item);
22 | }
23 |
24 | /**
25 | * 根据页码取出指定数量的元素
26 | *
27 | * @param page 页码
28 | * @param pageSize 数量
29 | * @return 元素列表
30 | */
31 | public List getPage(int page, int pageSize) {
32 | long start = (page - 1) * pageSize;
33 | long end = page * pageSize - 1;
34 | return client.lrange(key, start, end);
35 | }
36 |
37 | /**
38 | * 获取列表的元素数量
39 | *
40 | * @return 总数
41 | */
42 | public Long getTotalCount() {
43 | return client.llen(key);
44 | }
45 | }
--------------------------------------------------------------------------------
/java/src/main/java/Ranking.java:
--------------------------------------------------------------------------------
1 | import redis.clients.jedis.Jedis;
2 | import redis.clients.jedis.Tuple;
3 | import utils.JedisUtils;
4 |
5 | import java.util.Calendar;
6 | import java.util.Set;
7 |
8 | public class Ranking {
9 | private Jedis client = JedisUtils.getClient();
10 | private Calendar calendar = Calendar.getInstance();
11 |
12 | public Ranking() {
13 | calendar.setFirstDayOfWeek(Calendar.MONDAY);
14 | }
15 |
16 | private String getDayRankKey() {
17 | return String.format("rank:day:%s%s%s", calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF_MONTH));
18 | }
19 |
20 | private String getWeekRankKey() {
21 | return String.format("rank:week:%s%s", calendar.get(Calendar.YEAR), calendar.get(Calendar.WEEK_OF_YEAR));
22 | }
23 |
24 | private String getMonthRankKey() {
25 | return String.format("rank:month:%s%s", calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH));
26 | }
27 |
28 | public void incr(String user, double score) {
29 | client.zincrby(getDayRankKey(), score, user);
30 | client.zincrby(getWeekRankKey(), score, user);
31 | client.zincrby(getMonthRankKey(), score, user);
32 | }
33 |
34 | /**
35 | * 获取日榜单topN
36 | *
37 | * @param n 前n位
38 | * @return 结果集合
39 | */
40 | public Set getTodayTopNWithScores(long n) {
41 | return client.zrevrangeWithScores(getDayRankKey(), 0, n - 1);
42 | }
43 |
44 | /**
45 | * 获取周榜单topN
46 | *
47 | * @param n 前n位
48 | * @return 结果集合
49 | */
50 | public Set getWeekTopNWithScores(long n) {
51 | return client.zrevrangeWithScores(getWeekRankKey(), 0, n - 1);
52 | }
53 |
54 | /**
55 | * 获取月榜单topN
56 | *
57 | * @param n 前n位
58 | * @return 结果集合
59 | */
60 | public Set getMonthTopNWithScores(long n) {
61 | return client.zrevrangeWithScores(getMonthRankKey(), 0, n - 1);
62 | }
63 | }
--------------------------------------------------------------------------------
/java/src/main/java/SocialRelationship.java:
--------------------------------------------------------------------------------
1 | import redis.clients.jedis.Jedis;
2 | import utils.JedisUtils;
3 |
4 | import java.util.Set;
5 |
6 | public class SocialRelationship {
7 | private Jedis client = JedisUtils.getClient();
8 | private String user;
9 |
10 | public SocialRelationship(String user) {
11 | this.user = user;
12 | }
13 |
14 | /**
15 | * 关注目标用户
16 | *
17 | * @param target 用户
18 | */
19 | public void follow(String target) {
20 | String followingKey = JedisUtils.getFollowingKey(user);
21 | String followersKey = JedisUtils.getFollowersKey(target);
22 | long now = System.currentTimeMillis();
23 | client.zadd(followingKey, now, target);
24 | client.zadd(followersKey, now, user);
25 | }
26 |
27 | /**
28 | * 取关目标用户
29 | *
30 | * @param target 用户
31 | */
32 | public void unfollow(String target) {
33 | String followingKey = JedisUtils.getFollowingKey(user);
34 | String followersKey = JedisUtils.getFollowersKey(target);
35 | client.zrem(followingKey, target);
36 | client.zrem(followersKey, user);
37 | }
38 |
39 | /**
40 | * 判断是否关注着目标用户
41 | *
42 | * @param target 用户
43 | * @return 是否关注
44 | */
45 | public boolean isFollowing(String target) {
46 | String followingKey = JedisUtils.getFollowingKey(user);
47 | return client.zrank(followingKey, target) != null;
48 |
49 | }
50 |
51 | /**
52 | * 获取当前用户关注的所有人,并按最近关注时间倒序排列
53 | *
54 | * @return 关注人集合
55 | */
56 | public Set getAllFollowing() {
57 | String followingKey = JedisUtils.getFollowingKey(user);
58 | return client.zrevrange(followingKey, 0, -1);
59 | }
60 |
61 | /**
62 | * 获取当前用户的所有粉丝,并按最近关注时间倒序排列
63 | *
64 | * @return 粉丝集合
65 | */
66 | public Set getAllFollowers() {
67 | String followersKey = JedisUtils.getFollowersKey(user);
68 | return client.zrevrange(followersKey, 0, -1);
69 | }
70 |
71 | /**
72 | * 获取当前用户关注的人数
73 | *
74 | * @return 人数
75 | */
76 | public Long countFollowing() {
77 | String followingKey = JedisUtils.getFollowingKey(user);
78 | return client.zcard(followingKey);
79 | }
80 |
81 | /**
82 | * 获取当前用户的粉丝数
83 | *
84 | * @return 人数
85 | */
86 | public Long countFollowers() {
87 | String followersKey = JedisUtils.getFollowersKey(user);
88 | return client.zcard(followersKey);
89 | }
90 |
91 | /**
92 | * 获取与某用户的共同关注
93 | *
94 | * @param one 用户
95 | * @return 共同关注的用户集合
96 | */
97 | public Set getCommonFollowing(String one) {
98 | String commonKey = JedisUtils.getCommonFollowingKey(user, one);
99 | client.zinterstore(commonKey, JedisUtils.getFollowingKey(user), JedisUtils.getFollowingKey(one));
100 | return client.zrevrange(commonKey, 0, -1);
101 | }
102 | }
--------------------------------------------------------------------------------
/java/src/main/java/URLShorten.java:
--------------------------------------------------------------------------------
1 | import redis.clients.jedis.Jedis;
2 | import utils.JedisUtils;
3 |
4 | public class URLShorten {
5 | private Jedis client = JedisUtils.getClient();
6 |
7 | private final String URL_HASH_SHORT_SOURCE_KEY = "url_hash:short_source";
8 | private final String ID_COUNTER = "short_url:id_counter";
9 |
10 | /**
11 | * 将10进制数转换为36进制字符串
12 | *
13 | * @param number 10进制数
14 | * @return 36进制字符串
15 | */
16 | private String base10ToBase36(long number) {
17 | String alphabets = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
18 | char[] chars = alphabets.toCharArray();
19 | StringBuilder result = new StringBuilder();
20 | while (number != 0) {
21 | int i = (int) number % 36;
22 | number /= 36;
23 | result.insert(0, chars[i]);
24 | }
25 | return result.toString();
26 | }
27 |
28 | public URLShorten() {
29 | // 设置初始ID值,保留1-5位的短码,从6位的短码开始生成
30 | this.client.set(ID_COUNTER, String.valueOf((long) Math.pow(36, 5) - 1));
31 | }
32 |
33 | /**
34 | * 对源网址进行缩短,返回短网址ID
35 | *
36 | * @param sourceUrl 源网址
37 | * @return 短网址ID
38 | */
39 | public String shorten(String sourceUrl) {
40 | long newId = client.incr(ID_COUNTER);
41 | String shortId = base10ToBase36(newId);
42 | client.hset(URL_HASH_SHORT_SOURCE_KEY, shortId, sourceUrl);
43 | return shortId;
44 | }
45 |
46 | /**
47 | * 根据短网址ID,返回对应的源网址
48 | *
49 | * @param shortId 短网址ID
50 | * @return 源网址
51 | */
52 | public String restore(String shortId) {
53 | return client.hget(URL_HASH_SHORT_SOURCE_KEY, shortId);
54 | }
55 | }
--------------------------------------------------------------------------------
/java/src/main/java/utils/HLLUtils.java:
--------------------------------------------------------------------------------
1 | package utils;
2 |
3 | import org.apache.commons.lang3.time.DateUtils;
4 |
5 | import java.text.SimpleDateFormat;
6 | import java.util.*;
7 | import java.util.stream.Collectors;
8 |
9 |
10 | public class HLLUtils {
11 | private static String TIME_FORMAT_MONTH_DAY = "MMdd";
12 | private static String TIME_FORMAT_DAY_MINUTES = "MMddHHmm";
13 | private static String TIME_FORMAT_DAY_HOURS = "MMddHH";
14 | private static SimpleDateFormat FORMAT_MONTH_DAY = new SimpleDateFormat(TIME_FORMAT_MONTH_DAY);
15 | private static SimpleDateFormat FORMAT_DAY_HOURS = new SimpleDateFormat(TIME_FORMAT_DAY_HOURS);
16 | private static SimpleDateFormat FORMAT_DAY_MINUTES = new SimpleDateFormat(TIME_FORMAT_DAY_MINUTES);
17 |
18 | /**
19 | * 获取两个日期之间的键
20 | *
21 | * @param d1 日期1
22 | * @param d2 日期2
23 | * @return 键列表
24 | */
25 | public static List parse(Date d1, Date d2) {
26 | List list = new ArrayList<>();
27 | if (d1.compareTo(d2) == 0) {
28 | return list;
29 | }
30 |
31 | // 若时间差小于 1 小时
32 | long delta = d2.getTime() - d1.getTime();
33 | if (delta == 0) {
34 | return list;
35 | }
36 | if (delta < DateUtils.MILLIS_PER_HOUR) {
37 | int minutesDiff = (int) (delta / DateUtils.MILLIS_PER_MINUTE);
38 | Date date1Increment = d1;
39 | while (d2.compareTo(date1Increment) > 0 && minutesDiff > 0) {
40 | list.add(FORMAT_DAY_MINUTES.format(date1Increment));
41 | date1Increment = DateUtils.addMinutes(date1Increment, 1);
42 | }
43 | // 若时间差小于 1 天
44 | } else if (delta < DateUtils.MILLIS_PER_DAY) {
45 | Date dateLastPortionHour = DateUtils.truncate(d2, Calendar.HOUR_OF_DAY);
46 | list.addAll(parse(dateLastPortionHour, d2));
47 | long delta2 = dateLastPortionHour.getTime() - d1.getTime();
48 | int hoursDiff = (int) (delta2 / DateUtils.MILLIS_PER_HOUR);
49 | Date date1Increment = DateUtils.addHours(dateLastPortionHour, -1 * hoursDiff);
50 | while (dateLastPortionHour.compareTo(date1Increment) > 0 && hoursDiff > 0) {
51 | list.add(FORMAT_DAY_HOURS.format(date1Increment));
52 | date1Increment = DateUtils.addHours(date1Increment, 1);
53 | }
54 | list.addAll(parse(d1, DateUtils.addHours(dateLastPortionHour, -1 * hoursDiff)));
55 | } else {
56 | Date dateLastPortionDay = DateUtils.truncate(d2, Calendar.DAY_OF_MONTH);
57 | list.addAll(parse(dateLastPortionDay, d2));
58 | long delta2 = dateLastPortionDay.getTime() - d1.getTime();
59 | // 若时间差小于 1 个月
60 | int daysDiff = (int) (delta2 / DateUtils.MILLIS_PER_DAY);
61 | Date date1Increment = DateUtils.addDays(dateLastPortionDay, -1 * daysDiff);
62 | while (dateLastPortionDay.compareTo(date1Increment) > 0 && daysDiff > 0) {
63 | list.add(FORMAT_MONTH_DAY.format(date1Increment));
64 | date1Increment = DateUtils.addDays(date1Increment, 1);
65 | }
66 | list.addAll(parse(d1, DateUtils.addDays(dateLastPortionDay, -1 * daysDiff)));
67 | }
68 | return list;
69 | }
70 |
71 | /**
72 | * 获取从 date 往前推 minutes 分钟的键列表
73 | *
74 | * @param date 特定日期
75 | * @param minutes 分钟数
76 | * @return 键列表
77 | */
78 | public static List getLastMinutes(Date date, int minutes) {
79 | return parse(DateUtils.addMinutes(date, -1 * minutes), date);
80 | }
81 |
82 | /**
83 | * 获取从 date 往前推 hours 个小时的键列表
84 | *
85 | * @param date 特定日期
86 | * @param hours 小时数
87 | * @return 键列表
88 | */
89 | public static List getLastHours(Date date, int hours) {
90 | return parse(DateUtils.addHours(date, -1 * hours), date);
91 | }
92 |
93 | /**
94 | * 获取从 date 开始往前推 days 天的键列表
95 | *
96 | * @param date 特定日期
97 | * @param days 天数
98 | * @return 键列表
99 | */
100 | public static List getLastDays(Date date, int days) {
101 | return parse(DateUtils.addDays(date, -1 * days), date);
102 | }
103 |
104 | /**
105 | * 为keys列表添加前缀
106 | *
107 | * @param keys 键列表
108 | * @param prefix 前缀符号
109 | * @return 添加了前缀的键列表
110 | */
111 | public static List addPrefix(List keys, String prefix) {
112 | return keys.stream().map(key -> prefix + key).collect(Collectors.toList());
113 | }
114 |
115 | public static void main(String[] args) {
116 | Date d1 = new Date();
117 | d1.setTime((long) (System.currentTimeMillis() - 3600 * 1000 * 24 * 1.2));
118 | Date d2 = new Date();
119 | List res = parse(d1, d2);
120 | res.forEach(System.out::println);
121 | List newRes = addPrefix(res, "USER:LOGIN:");
122 | System.out.println("-------");
123 | newRes.forEach(System.out::println);
124 |
125 | List lastThreeDays = getLastDays(new Date(), 3);
126 | System.out.println("-------");
127 | lastThreeDays.forEach(System.out::println);
128 |
129 | List lastTwoHours = getLastHours(new Date(), 2);
130 | System.out.println("-------");
131 | lastTwoHours.forEach(System.out::println);
132 |
133 | List lastFourMinutes = getLastMinutes(new Date(), 4);
134 | System.out.println("-------");
135 | lastFourMinutes.forEach(System.out::println);
136 | }
137 | }
--------------------------------------------------------------------------------
/java/src/main/java/utils/JedisUtils.java:
--------------------------------------------------------------------------------
1 | package utils;
2 |
3 | import redis.clients.jedis.Jedis;
4 |
5 | public class JedisUtils {
6 |
7 | public static Jedis getClient() {
8 | Jedis client = new Jedis();
9 | client.flushAll();
10 | return client;
11 | }
12 |
13 | public static String getFollowingKey(String user) {
14 | return "following:" + user;
15 | }
16 |
17 | public static String getFollowersKey(String user) {
18 | return "followers:" + user;
19 | }
20 |
21 | public static String getCommonFollowingKey(String user1, String user2) {
22 | return String.format("common:following:%s:%s", user1, user2);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/java/src/test/java/AutoCompleteTest.java:
--------------------------------------------------------------------------------
1 | import org.junit.jupiter.api.Test;
2 |
3 | import java.util.LinkedHashSet;
4 |
5 | import static org.junit.jupiter.api.Assertions.assertEquals;
6 |
7 | public class AutoCompleteTest {
8 |
9 | @Test
10 | public void testHint() {
11 | AutoComplete ac = new AutoComplete();
12 | ac.feed("张艺兴", 5000);
13 | ac.feed("张艺谋", 3000);
14 | ac.feed("张三", 500);
15 | assertEquals(ac.hint("张", 10),
16 | new LinkedHashSet() {{
17 | add("张艺兴");
18 | add("张艺谋");
19 | add("张三");
20 | }});
21 |
22 | assertEquals(ac.hint("张艺", 10),
23 | new LinkedHashSet() {{
24 | add("张艺兴");
25 | add("张艺谋");
26 | }});
27 | }
28 | }
--------------------------------------------------------------------------------
/java/src/test/java/DistributedLockTest.java:
--------------------------------------------------------------------------------
1 | import org.junit.jupiter.api.Test;
2 |
3 | import java.util.concurrent.TimeUnit;
4 |
5 | import static org.junit.jupiter.api.Assertions.assertFalse;
6 | import static org.junit.jupiter.api.Assertions.assertTrue;
7 |
8 | public class DistributedLockTest {
9 |
10 | @Test
11 | public void testDistributedLock() throws InterruptedException {
12 | DistributedLock lock = new DistributedLock("bingo_distributed_lock");
13 | assertTrue(lock.acquire(10));
14 | assertFalse(lock.acquire(10));
15 |
16 | TimeUnit.SECONDS.sleep(10);
17 |
18 | assertTrue(lock.acquire());
19 | assertTrue(lock.release());
20 |
21 | assertTrue(lock.acquire());
22 | }
23 | }
--------------------------------------------------------------------------------
/java/src/test/java/LikeServiceTest.java:
--------------------------------------------------------------------------------
1 | import org.junit.jupiter.api.Test;
2 |
3 | import static org.junit.jupiter.api.Assertions.assertEquals;
4 |
5 | public class LikeServiceTest {
6 |
7 | @Test
8 | public void testLikeService() {
9 | LikeService likeService = new LikeService();
10 | String entityId = "user1";
11 | assertEquals(0, likeService.getLikeCount(entityId));
12 |
13 | likeService.like("user2", entityId);
14 | likeService.like("user3", entityId);
15 |
16 | likeService.dislike("user4", entityId);
17 |
18 | assertEquals(2, likeService.getLikeCount(entityId));
19 | assertEquals(1, likeService.getLikeStatus("user2", entityId));
20 |
21 | assertEquals(-1, likeService.getLikeStatus("user4", entityId));
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/java/src/test/java/LoginSessionTest.java:
--------------------------------------------------------------------------------
1 | import org.junit.jupiter.api.Test;
2 |
3 | import static org.junit.jupiter.api.Assertions.assertEquals;
4 |
5 | public class LoginSessionTest {
6 |
7 | @Test
8 | public void testLoginSession() {
9 | LoginSession session = new LoginSession("bingo");
10 |
11 | String token = session.create();
12 |
13 | String res = session.validate("this is a wrong token");
14 | assertEquals("SESSION_TOKEN_INCORRECT", res);
15 |
16 | res = session.validate(token);
17 | assertEquals("SESSION_TOKEN_CORRECT", res);
18 |
19 | session.destroy();
20 | res = session.validate(token);
21 | assertEquals("SESSION_NOT_LOGIN", res);
22 | }
23 | }
--------------------------------------------------------------------------------
/java/src/test/java/PaginateTest.java:
--------------------------------------------------------------------------------
1 | import org.junit.jupiter.api.Test;
2 |
3 | import java.util.Arrays;
4 |
5 | import static org.junit.jupiter.api.Assertions.assertEquals;
6 |
7 | public class PaginateTest {
8 |
9 | @Test
10 | public void testPaginate() {
11 | Paginate topics = new Paginate("user-topics");
12 | for (int i = 0; i < 24; ++i) {
13 | topics.add(i + "");
14 | }
15 |
16 | // 取总数
17 | assertEquals(24, topics.getTotalCount());
18 |
19 | // 以每页5条数据的方式取出第1页数据
20 | assertEquals(Arrays.asList("23", "22", "21", "20", "19"),
21 | topics.getPage(1, 5));
22 |
23 | // 以每页10条数据的方式取出第1页数据
24 | assertEquals(Arrays.asList("23", "22", "21", "20", "19", "18", "17", "16", "15", "14"),
25 | topics.getPage(1, 10));
26 |
27 | // 以每页5条数据的方式取出第5页数据
28 | assertEquals(Arrays.asList("3", "2", "1", "0"),
29 | topics.getPage(5, 5));
30 |
31 | }
32 | }
--------------------------------------------------------------------------------
/java/src/test/java/RankingTest.java:
--------------------------------------------------------------------------------
1 | import org.junit.jupiter.api.Test;
2 | import redis.clients.jedis.Tuple;
3 |
4 | import java.util.HashSet;
5 | import java.util.Set;
6 |
7 | import static org.junit.jupiter.api.Assertions.assertEquals;
8 |
9 | public class RankingTest {
10 |
11 | @Test
12 | public void testRanking() {
13 | Ranking ranking = new Ranking();
14 |
15 | ranking.incr("bingo", 5);
16 | ranking.incr("iris", 3);
17 | ranking.incr("lily", 4);
18 | ranking.incr("helen", 6);
19 |
20 | Set expectResult = new HashSet() {{
21 | add(new Tuple("helen", 6.0));
22 | add(new Tuple("bingo", 5.0));
23 | }};
24 | assertEquals(expectResult, ranking.getTodayTopNWithScores(2));
25 | assertEquals(expectResult, ranking.getWeekTopNWithScores(2));
26 |
27 | expectResult.add(new Tuple("lily", 4.0));
28 | assertEquals(expectResult, ranking.getMonthTopNWithScores(3));
29 | }
30 | }
--------------------------------------------------------------------------------
/java/src/test/java/SocialRelationshipTest.java:
--------------------------------------------------------------------------------
1 | import org.junit.jupiter.api.Test;
2 |
3 | import java.util.HashSet;
4 |
5 | import static org.junit.jupiter.api.Assertions.assertEquals;
6 | import static org.junit.jupiter.api.Assertions.assertTrue;
7 |
8 | public class SocialRelationshipTest {
9 |
10 | @Test
11 | public void testSNS() {
12 | SocialRelationship bingo = new SocialRelationship("Bingo");
13 | SocialRelationship iris = new SocialRelationship("Iris");
14 | bingo.follow("Iris");
15 | bingo.follow("GitHub");
16 | bingo.follow("Apple");
17 | iris.follow("Bingo");
18 | iris.follow("GitHub");
19 |
20 | assertTrue(bingo.isFollowing("Iris"));
21 | assertEquals(new HashSet() {{
22 | add("Apple");
23 | add("GitHub");
24 | add("Iris");
25 | }},
26 | bingo.getAllFollowing());
27 |
28 | assertTrue(iris.isFollowing("Bingo"));
29 |
30 | assertEquals(3, bingo.countFollowing());
31 | assertEquals(new HashSet() {{
32 | add("GitHub");
33 | }},
34 | bingo.getCommonFollowing("Iris"));
35 |
36 | }
37 | }
--------------------------------------------------------------------------------
/java/src/test/java/URLShortenTest.java:
--------------------------------------------------------------------------------
1 | import org.junit.jupiter.api.Test;
2 |
3 | import static org.junit.jupiter.api.Assertions.assertEquals;
4 |
5 |
6 | public class URLShortenTest {
7 |
8 | @Test
9 | public void testURLShorten() {
10 | URLShorten urlShorten = new URLShorten();
11 |
12 | String shortId = urlShorten.shorten("https://github.com/yanglbme");
13 |
14 | assertEquals("100000", shortId);
15 | String sourceUrl = urlShorten.restore(shortId);
16 | assertEquals("https://github.com/yanglbme", sourceUrl);
17 |
18 | shortId = urlShorten.shorten("https://doocs.github.io");
19 | assertEquals("100001", shortId);
20 | }
21 | }
--------------------------------------------------------------------------------
/python/README.md:
--------------------------------------------------------------------------------
1 | # Python 文件目录树
2 | ```bash
3 | ├─main
4 | │ auto_complete.py
5 | │ distributed_lock.py
6 | │ paginate.py
7 | │ ranking_list.py
8 | │ session_token.py
9 | │ shorten_url.py
10 | │ social_relationship.py
11 | │ __init__.py
12 | │
13 | └─test
14 | test_auto_complete.py
15 | test_distributed_lock.py
16 | test_paginate.py
17 | test_ranking_list.py
18 | test_session_token.py
19 | test_shorten_url.py
20 | test_social_relationship.py
21 | __init__.py
22 |
23 | ```
--------------------------------------------------------------------------------
/python/src/main/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yanglbme/redis-multi-programming-language-practice/2425ee7e1b1f193f7b5e57ad57e3b59f9adfd329/python/src/main/__init__.py
--------------------------------------------------------------------------------
/python/src/main/auto_complete.py:
--------------------------------------------------------------------------------
1 | from redis import Redis
2 |
3 |
4 | class AutoComplete:
5 | def __init__(self, client: Redis):
6 | self.client = client
7 |
8 | def feed(self, keyword: str, weight):
9 | for i in range(1, len(keyword)):
10 | key = 'auto_complete:' + keyword[:i]
11 | self.client.zincrby(key, weight, keyword)
12 |
13 | def hint(self, prefix: str, count=10):
14 | key = 'auto_complete:' + prefix
15 | return self.client.zrevrange(key, 0, count - 1)
16 |
--------------------------------------------------------------------------------
/python/src/main/distributed_lock.py:
--------------------------------------------------------------------------------
1 | import random
2 | import string
3 | import time
4 |
5 | from redis import Redis
6 |
7 |
8 | def generate_random_value():
9 | return ''.join(random.sample(string.ascii_letters + string.digits, 32))
10 |
11 |
12 | # 设定默认过期时长为 10s
13 | DEFAULT_TIMEOUT = 10
14 |
15 |
16 | class DistributedLock:
17 | def __init__(self, client: Redis, key: str):
18 | self.client = client
19 | self.key = key
20 |
21 | def acquire(self, timeout=DEFAULT_TIMEOUT) -> bool:
22 | """尝试获取锁"""
23 | res = self.client.set(self.key, generate_random_value(), ex=timeout, nx=True) # ex 表示过期时长秒数,nx 表示 key 不存在时才设置
24 | return res is True
25 |
26 | def release(self) -> bool:
27 | """尝试释放锁"""
28 | return self.client.delete(self.key) == 1
29 |
--------------------------------------------------------------------------------
/python/src/main/like_dislike.py:
--------------------------------------------------------------------------------
1 | from redis import Redis
2 |
3 |
4 | def get_like_key(entity_id):
5 | return 'like:{}'.format(entity_id)
6 |
7 |
8 | def get_dislike_key(entity_id):
9 | return 'dislike:{}'.format(entity_id)
10 |
11 |
12 | class Like:
13 | def __init__(self, client: Redis, entity_id: str):
14 | self.client = client
15 | self.entity_id = entity_id
16 |
17 | def like(self, user_id) -> int:
18 | """某用户点赞"""
19 | self.client.sadd(get_like_key(self.entity_id), user_id)
20 | self.client.srem(get_dislike_key(self.entity_id), user_id)
21 | return self.client.scard(get_like_key(self.entity_id))
22 |
23 | def dislike(self, user_id) -> int:
24 | """某用户点踩"""
25 | self.client.sadd(get_dislike_key(self.entity_id), user_id)
26 | self.client.srem(get_like_key(self.entity_id), user_id)
27 | return self.client.scard(get_like_key(self.entity_id))
28 |
29 | def get_like_status(self, user_id) -> int:
30 | """判断用户是否点赞或点踩了,点赞返回1,点踩返回-1,不赞不踩返回0"""
31 | if self.client.sismember(get_like_key(self.entity_id), user_id):
32 | return 1
33 | if self.client.sismember(get_dislike_key(self.entity_id), user_id):
34 | return -1
35 | return 0
36 |
37 | def get_like_count(self) -> int:
38 | """获取当前点赞数"""
39 | return self.client.scard(get_like_key(self.entity_id))
40 |
--------------------------------------------------------------------------------
/python/src/main/paginate.py:
--------------------------------------------------------------------------------
1 | from redis import Redis
2 |
3 |
4 | class Paginate:
5 | def __init__(self, client: Redis, key: str):
6 | self.client = client
7 | self.key = key
8 |
9 | def add(self, item):
10 | """添加元素到分页列表中"""
11 | self.client.lpush(self.key, item)
12 |
13 | def get_page(self, page: int, per_page: int):
14 | """根据页码取出指定数量的元素"""
15 | start = (page - 1) * per_page
16 | end = page * per_page - 1
17 | return self.client.lrange(self.key, start, end)
18 |
19 | def get_size(self):
20 | """获取列表的元素数量"""
21 | return self.client.llen(self.key)
22 |
--------------------------------------------------------------------------------
/python/src/main/ranking_list.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import time
3 |
4 | from redis import Redis
5 |
6 |
7 | def get_day_rank_key():
8 | return 'rank:day:{}'.format(datetime.date.today().strftime('%Y%m%d'))
9 |
10 |
11 | def get_week_rank_key():
12 | return 'rank:week:{}{}'.format(datetime.date.today().strftime('%Y'), time.strftime("%W"))
13 |
14 |
15 | def get_month_rank_key():
16 | today = datetime.date.today()
17 | return 'rank:month:{}{}'.format(today.strftime('%Y'), today.strftime('%m'))
18 |
19 |
20 | class Ranking:
21 | def __init__(self, client: Redis):
22 | self.client = client
23 |
24 | def incr(self, user, score=1):
25 | self.client.zincrby(get_day_rank_key(), score, user)
26 | self.client.zincrby(get_week_rank_key(), score, user)
27 | self.client.zincrby(get_month_rank_key(), score, user)
28 |
29 | def get_today_top_n(self, n, with_scores=False):
30 | """获取日榜单topN"""
31 | return self.client.zrevrange(get_day_rank_key(), 0, n - 1, withscores=with_scores)
32 |
33 | def get_week_top_n(self, n, with_scores=False):
34 | """获取周榜单topN"""
35 | return self.client.zrevrange(get_week_rank_key(), 0, n - 1, withscores=with_scores)
36 |
37 | def get_month_top_n(self, n, with_scores=False):
38 | """获取月榜单topN"""
39 | return self.client.zrevrange(get_month_rank_key(), 0, n - 1, withscores=with_scores)
40 |
--------------------------------------------------------------------------------
/python/src/main/session_token.py:
--------------------------------------------------------------------------------
1 | import os
2 | from _sha256 import sha256
3 | from time import time
4 |
5 | from redis import Redis
6 |
7 | # 会话默认过期时间,一个月
8 | DEFAULT_TIMEOUT = 30 * 24 * 3600
9 |
10 | # 会话 token 及过期时间的 key
11 | SESSION_TOKEN_KEY = 'SESSION:TOKEN'
12 | SESSION_EXPIRE_TS_KEY = 'SESSION:EXPIRE'
13 |
14 | # 会话状态
15 | SESSION_NOT_LOGIN = 'SESSION_NOT_LOGIN'
16 | SESSION_EXPIRE = 'SESSION_EXPIRE'
17 | SESSION_TOKEN_CORRECT = 'SESSION_TOKEN_CORRECT'
18 | SESSION_TOKEN_INCORRECT = 'SESSION_TOKEN_INCORRECT'
19 |
20 |
21 | def generate_token():
22 | """生成一个随机的会话令牌"""
23 | return sha256(os.urandom(256)).hexdigest()
24 |
25 |
26 | class LoginSession:
27 | def __init__(self, client: Redis, user_id: str):
28 | self.client = client
29 | self.user_id = user_id
30 |
31 | def create(self, timeout=DEFAULT_TIMEOUT) -> str:
32 | """创建新的会话,并返回会话token"""
33 | session_token = generate_token()
34 |
35 | # 设置过期时间
36 | expire_time = time() + timeout
37 | self.client.hset(SESSION_TOKEN_KEY, self.user_id, session_token)
38 | self.client.hset(SESSION_EXPIRE_TS_KEY, self.user_id, expire_time)
39 | return session_token
40 |
41 | def validate(self, token) -> str:
42 | """校验token"""
43 | session_token = self.client.hget(SESSION_TOKEN_KEY, self.user_id)
44 | expire_time = self.client.hget(SESSION_EXPIRE_TS_KEY, self.user_id)
45 |
46 | if (session_token is None) or (expire_time is None):
47 | return SESSION_NOT_LOGIN
48 |
49 | # 将字符串类型的时间转换为浮点数类型
50 | if time() > float(expire_time):
51 | return SESSION_EXPIRE
52 |
53 | if session_token == token:
54 | return SESSION_TOKEN_CORRECT
55 |
56 | return SESSION_TOKEN_INCORRECT
57 |
58 | def destroy(self):
59 | """销毁会话"""
60 | self.client.hdel(SESSION_TOKEN_KEY, self.user_id)
61 | self.client.hdel(SESSION_EXPIRE_TS_KEY, self.user_id)
62 |
--------------------------------------------------------------------------------
/python/src/main/shorten_url.py:
--------------------------------------------------------------------------------
1 | from redis import Redis
2 |
3 |
4 | # 10进制数转换为36进制字符串
5 | def base10_to_base36(number: int) -> str:
6 | alphabets = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
7 | result = ''
8 | while number != 0:
9 | number, i = divmod(number, 36)
10 | result = alphabets[i] + result
11 |
12 | return result or alphabets[0]
13 |
14 |
15 | URL_HASH_SHORT_SOURCE_KEY = 'url_hash:short_source'
16 | ID_COUNTER = 'short_url:id_counter'
17 |
18 |
19 | class URLShorten:
20 |
21 | def __init__(self, client: Redis):
22 | self.client = client
23 | # 设置初始ID值,保留1-5位的短码,从6位的短码开始生成
24 | self.client.set(ID_COUNTER, 36 ** 5 - 1)
25 |
26 | def shorten(self, source_url: str) -> str:
27 | """对源网址进行缩短,返回短网址ID"""
28 | new_id = self.client.incr(ID_COUNTER)
29 | short_id = base10_to_base36(new_id)
30 | self.client.hset(URL_HASH_SHORT_SOURCE_KEY, short_id, source_url)
31 | return short_id
32 |
33 | def restore(self, short_id: str) -> str:
34 | """根据短网址ID,返回对应的源网址"""
35 | return self.client.hget(URL_HASH_SHORT_SOURCE_KEY, short_id)
36 |
--------------------------------------------------------------------------------
/python/src/main/social_relationship.py:
--------------------------------------------------------------------------------
1 | from time import time
2 |
3 | from redis import Redis
4 |
5 |
6 | def following_key(user):
7 | return 'following:' + user
8 |
9 |
10 | def followers_key(user):
11 | return 'followers:' + user
12 |
13 |
14 | def common_following_key(user1, user2):
15 | return 'common:following:{}:{}'.format(user1, user2)
16 |
17 |
18 | class SocialRelationship:
19 | def __init__(self, client: Redis, user):
20 | self.client = client
21 | self.user = user
22 |
23 | def follow(self, target):
24 | """关注目标用户"""
25 | self.client.zadd(following_key(self.user), {target: time()})
26 | self.client.zadd(followers_key(target), {self.user: time()})
27 |
28 | def unfollow(self, target):
29 | """取关目标用户"""
30 | self.client.zrem(following_key(self.user), target)
31 | self.client.zrem(followers_key(target), self.user)
32 |
33 | def is_following(self, target):
34 | """判断是否关注着目标用户"""
35 | return self.client.zrank(following_key(self.user), target) is not None
36 |
37 | def get_all_following(self):
38 | """获取当前用户关注的所有人,并按最近关注时间倒序排列"""
39 | return self.client.zrevrange(following_key(self.user), 0, -1)
40 |
41 | def get_all_followers(self):
42 | """获取当前用户的所有粉丝,并按最近关注时间倒序排列"""
43 | return self.client.zrevrange(followers_key(self.user), 0, -1)
44 |
45 | def count_following(self):
46 | """获取当前用户关注的人数"""
47 | return self.client.zcard(following_key(self.user))
48 |
49 | def count_followers(self):
50 | """获取当前用户的粉丝数"""
51 | return self.client.zcard(followers_key(self.user))
52 |
53 | def get_common_following(self, one):
54 | """获取与某用户的共同关注"""
55 | common_key = common_following_key(self.user, one)
56 | self.client.zinterstore(
57 | common_key, (following_key(self.user), following_key(one)))
58 | return self.client.zrevrange(common_key, 0, -1)
59 |
--------------------------------------------------------------------------------
/python/src/test/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yanglbme/redis-multi-programming-language-practice/2425ee7e1b1f193f7b5e57ad57e3b59f9adfd329/python/src/test/__init__.py
--------------------------------------------------------------------------------
/python/src/test/test_auto_complete.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 |
3 | from redis import Redis
4 |
5 | from main.auto_complete import AutoComplete
6 |
7 |
8 | def test_auto_complete():
9 | redis = Redis(decode_responses=True)
10 | redis.flushall()
11 | test_case = TestCase()
12 |
13 | ac = AutoComplete(redis)
14 | ac.feed('张艺兴', 5000)
15 | ac.feed('张艺谋', 3000)
16 | ac.feed('张三', 500)
17 |
18 | test_case.assertListEqual(['张艺兴', '张艺谋', '张三'], ac.hint('张'))
19 | test_case.assertListEqual(['张艺兴', '张艺谋'], ac.hint('张艺'))
20 |
--------------------------------------------------------------------------------
/python/src/test/test_distributed_lock.py:
--------------------------------------------------------------------------------
1 | import time
2 |
3 | from redis import Redis
4 |
5 | from main.distributed_lock import DistributedLock
6 |
7 |
8 | def test_distributed_lock():
9 | redis = Redis(decode_responses=True)
10 | redis.flushall()
11 | lock = DistributedLock(redis, 'bingo_distributed_lock')
12 |
13 | assert lock.acquire(10) is True
14 | assert lock.acquire(10) is False
15 |
16 | time.sleep(10)
17 |
18 | assert lock.acquire() is True
19 | assert lock.release() is True
20 |
21 | assert lock.acquire() is True
22 |
--------------------------------------------------------------------------------
/python/src/test/test_like_dislike.py:
--------------------------------------------------------------------------------
1 | from redis import Redis
2 |
3 | from main.like_dislike import Like
4 |
5 |
6 | def test_like_dislike():
7 | redis = Redis(decode_responses=True)
8 | redis.flushall()
9 | like_entity = Like(redis, 'user1')
10 | assert like_entity.get_like_count() == 0
11 | like_entity.like('user2')
12 | like_entity.like('user3')
13 |
14 | like_entity.dislike('user4')
15 |
16 | assert like_entity.get_like_count() == 2
17 | assert like_entity.get_like_status('user2') == 1
18 | assert like_entity.get_like_status('user4') == -1
19 |
20 |
--------------------------------------------------------------------------------
/python/src/test/test_paginate.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 |
3 | from redis import Redis
4 |
5 | from main.paginate import Paginate
6 |
7 |
8 | def test_paginate():
9 | test_case = TestCase()
10 | redis = Redis(decode_responses=True)
11 | redis.flushall()
12 | topics = Paginate(redis, 'user-topics')
13 |
14 | for i in range(24):
15 | topics.add(i)
16 |
17 | # 取总数
18 | assert topics.get_size() == 24
19 |
20 | # 以每页5条数据的方式取出第1页数据
21 | test_case.assertListEqual(['23', '22', '21', '20', '19'], topics.get_page(1, 5))
22 |
23 | # 以每页10条数据的方式取出第1页数据
24 | test_case.assertListEqual(['23', '22', '21', '20', '19', '18', '17', '16', '15', '14'], topics.get_page(1, 10))
25 |
26 | # 以每页5条数据的方式取出第5页数据
27 | test_case.assertListEqual(['3', '2', '1', '0'], topics.get_page(5, 5))
28 |
--------------------------------------------------------------------------------
/python/src/test/test_ranking_list.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 |
3 | from redis import Redis
4 |
5 | from main.ranking_list import Ranking
6 |
7 |
8 | def test_ranking():
9 | test_case = TestCase()
10 | redis = Redis(decode_responses=True)
11 | redis.flushall()
12 | ranking = Ranking(redis)
13 |
14 | ranking.incr('bingo', 5)
15 | ranking.incr('iris', 3)
16 | ranking.incr('lily', 4)
17 | ranking.incr('helen', 6)
18 |
19 | test_case.assertListEqual([('helen', 6.0), ('bingo', 5.0)], ranking.get_today_top_n(n=2, with_scores=True))
20 | test_case.assertListEqual([('helen', 6.0), ('bingo', 5.0)], ranking.get_week_top_n(n=2, with_scores=True))
21 | test_case.assertListEqual([('helen', 6.0), ('bingo', 5.0), ('lily', 4.0)], ranking.get_month_top_n(n=3, with_scores=True))
22 |
--------------------------------------------------------------------------------
/python/src/test/test_session_token.py:
--------------------------------------------------------------------------------
1 | from redis import Redis
2 |
3 | from main.session_token import LoginSession
4 |
5 |
6 | def test_session_token():
7 | redis = Redis(decode_responses=True)
8 | redis.flushall()
9 | session = LoginSession(redis, 'bingo')
10 |
11 | user_token = session.create()
12 |
13 | res = session.validate('this is a wrong token')
14 | assert res == 'SESSION_TOKEN_INCORRECT'
15 |
16 | res = session.validate(user_token)
17 | assert res == 'SESSION_TOKEN_CORRECT'
18 |
19 | session.destroy()
20 | res = session.validate(user_token)
21 | assert res == 'SESSION_NOT_LOGIN'
22 |
--------------------------------------------------------------------------------
/python/src/test/test_shorten_url.py:
--------------------------------------------------------------------------------
1 | from redis import Redis
2 |
3 | from main.shorten_url import URLShorten
4 |
5 |
6 | def test_shorten_url():
7 | redis = Redis(decode_responses=True)
8 | redis.flushall()
9 | url_shorten = URLShorten(redis)
10 |
11 | short_id = url_shorten.shorten('https://github.com/yanglbme')
12 | assert short_id == '100000'
13 |
14 | source_url = url_shorten.restore(short_id)
15 | assert source_url == 'https://github.com/yanglbme'
16 |
17 | assert url_shorten.shorten('https://doocs.github.io') == '100001'
18 |
--------------------------------------------------------------------------------
/python/src/test/test_social_relationship.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 |
3 | from redis import Redis
4 |
5 | from main.social_relationship import SocialRelationship
6 |
7 |
8 | def test_sns():
9 | test_case = TestCase()
10 | redis = Redis(decode_responses=True)
11 | redis.flushall()
12 |
13 | bingo = SocialRelationship(redis, 'Bingo')
14 | iris = SocialRelationship(redis, 'Iris')
15 | bingo.follow('Iris')
16 | bingo.follow('GitHub')
17 | bingo.follow('Apple')
18 | iris.follow('Bingo')
19 | iris.follow('GitHub')
20 |
21 | assert bingo.is_following('Iris') is True
22 | assert iris.is_following('Bingo') is True
23 | assert bingo.count_following() == 3
24 | test_case.assertListEqual(['GitHub'], bingo.get_common_following('Iris'))
25 |
--------------------------------------------------------------------------------