update redis docs and examples

pull/1/head
Zhang Peng 2020-02-26 23:05:48 +08:00
parent 67291cb54f
commit 5c99163520
13 changed files with 2354 additions and 1829 deletions

View File

@ -29,10 +29,13 @@
- [Nosql 技术选型](docs/nosql/nosql-selection.md)
- [Redis](docs/nosql/redis) 📚
- [Redis 入门指南](docs/nosql/redis/redis-quickstart.md) ⚡
- [Redis 数据类型](docs/nosql/redis/redis-datatype.md)
- [Redis 持久化](docs/nosql/redis/redis-persistence.md)
- [Redis 复制](docs/nosql/redis/redis-replication.md)
- [Redis 哨兵](docs/nosql/redis/redis-sentinel.md)
- [Redis 集群](docs/nosql/redis/redis-cluster.md)
- [Redis 事务](docs/nosql/redis/redis-transaction.md)
- [Redis 发布与订阅](docs/nosql/redis/redis-pub-sub.md)
- [Redis 运维](docs/nosql/redis/redis-ops.md) 🔨
## 中间件

Binary file not shown.

Binary file not shown.

View File

@ -55,7 +55,7 @@
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-csv</artifactId>
<version>1.5</version>
<version>1.8</version>
</dependency>
<dependency>
<groupId>org.javatuples</groupId>

View File

@ -10,7 +10,7 @@ import java.util.*;
public class Chapter02 {
public static final void main(String[] args) throws InterruptedException {
public static void main(String[] args) throws InterruptedException {
new Chapter02().run();
}
@ -163,7 +163,7 @@ public class Chapter02 {
}
/**
* 2-1
* 2-1 -
*/
public String checkToken(Jedis conn, String token) {
// 尝试获取并返回令牌对应的用户。
@ -171,7 +171,7 @@ public class Chapter02 {
}
/**
* 2-2 2-9
* 2-22-9 -
*/
public void updateToken(Jedis conn, String token, String user, String item) {
// 获取当前时间戳。
@ -189,6 +189,62 @@ public class Chapter02 {
}
}
/**
* 2-3 -
*/
public static class CleanSessionsThread extends Thread {
private Jedis conn;
private int limit;
private volatile boolean quit;
public CleanSessionsThread(int limit) {
this.conn = new Jedis("localhost");
this.conn.select(15);
this.limit = limit;
}
public void quit() {
quit = true;
}
@Override
public void run() {
while (!quit) {
// 找出目前已有令牌的数量。
long size = conn.zcard("recent:");
// 令牌数量未超过限制,休眠并在之后重新检查。
if (size <= limit) {
try {
sleep(1000);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
continue;
}
// 获取需要移除的令牌ID。
long endIndex = Math.min(size - limit, 100);
Set<String> tokenSet = conn.zrange("recent:", 0, endIndex - 1);
String[] tokens = tokenSet.toArray(new String[tokenSet.size()]);
// 为那些将要被删除的令牌构建键名。
ArrayList<String> sessionKeys = new ArrayList<String>();
for (String token : tokens) {
sessionKeys.add("viewed:" + token);
}
// 移除最旧的那些令牌。
conn.del(sessionKeys.toArray(new String[sessionKeys.size()]));
conn.hdel("login:", tokens);
conn.zrem("recent:", tokens);
}
}
}
/**
* 2-4
*/
@ -203,17 +259,60 @@ public class Chapter02 {
}
/**
* 2-7
* 2-5
*/
public void scheduleRowCache(Jedis conn, String rowId, int delay) {
// 先设置数据行的延迟值。
conn.zadd("delay:", delay, rowId);
// 立即缓存数据行。
conn.zadd("schedule:", System.currentTimeMillis() / 1000, rowId);
public static class CleanFullSessionsThread extends Thread {
private Jedis conn;
private int limit;
private boolean quit;
public CleanFullSessionsThread(int limit) {
this.conn = new Jedis("localhost");
this.conn.select(15);
this.limit = limit;
}
public void quit() {
quit = true;
}
@Override
public void run() {
while (!quit) {
long size = conn.zcard("recent:");
if (size <= limit) {
try {
sleep(1000);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
continue;
}
long endIndex = Math.min(size - limit, 100);
Set<String> sessionSet = conn.zrange("recent:", 0, endIndex - 1);
String[] sessions = sessionSet.toArray(new String[sessionSet.size()]);
ArrayList<String> sessionKeys = new ArrayList<String>();
for (String sess : sessions) {
sessionKeys.add("viewed:" + sess);
// 新增加的这行代码用于删除旧会话对应用户的购物车。
sessionKeys.add("cart:" + sess);
}
conn.del(sessionKeys.toArray(new String[sessionKeys.size()]));
conn.hdel("login:", sessions);
conn.zrem("recent:", sessions);
}
}
}
/**
* 2-6
* 2-6
*/
public String cacheRequest(Jedis conn, String request, Callback callback) {
// 对于不能被缓存的请求,直接调用回调函数。
@ -237,6 +336,74 @@ public class Chapter02 {
return content;
}
/**
* 2-7 -
*/
public void scheduleRowCache(Jedis conn, String rowId, int delay) {
// 先设置数据行的延迟值。
conn.zadd("delay:", delay, rowId);
// 立即缓存数据行。
conn.zadd("schedule:", System.currentTimeMillis() / 1000, rowId);
}
/**
* 2-8 -
*/
public static class CacheRowsThread extends Thread {
private Jedis conn;
private boolean quit;
public CacheRowsThread() {
this.conn = new Jedis("localhost");
this.conn.select(15);
}
public void quit() {
quit = true;
}
@Override
public void run() {
Gson gson = new Gson();
while (!quit) {
// 尝试获取下一个需要被缓存的数据行以及该行的调度时间戳,
// 命令会返回一个包含零个或一个元组tuple的列表。
Set<Tuple> range = conn.zrangeWithScores("schedule:", 0, 0);
Tuple next = range.size() > 0 ? range.iterator().next() : null;
long now = System.currentTimeMillis() / 1000;
if (next == null || next.getScore() > now) {
try {
// 暂时没有行需要被缓存休眠50毫秒后重试。
sleep(50);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
continue;
}
String rowId = next.getElement();
// 获取下一次调度前的延迟时间。
double delay = conn.zscore("delay:", rowId);
if (delay <= 0) {
// 不必再缓存这个行,将它从缓存中移除。
conn.zrem("delay:", rowId);
conn.zrem("schedule:", rowId);
conn.del("inv:" + rowId);
continue;
}
// 读取数据行。
Inventory row = Inventory.get(rowId);
// 更新调度时间并设置缓存值。
conn.zadd("schedule:", now + delay, rowId);
conn.set("inv:" + rowId, gson.toJson(row));
}
}
}
/**
* 2-11
*/
@ -304,171 +471,4 @@ public class Chapter02 {
}
/**
* 2-3
*/
public static class CleanSessionsThread extends Thread {
private Jedis conn;
private int limit;
private boolean quit;
public CleanSessionsThread(int limit) {
this.conn = new Jedis("localhost");
this.conn.select(15);
this.limit = limit;
}
public void quit() {
quit = true;
}
@Override
public void run() {
while (!quit) {
// 找出目前已有令牌的数量。
long size = conn.zcard("recent:");
// 令牌数量未超过限制,休眠并在之后重新检查。
if (size <= limit) {
try {
sleep(1000);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
continue;
}
// 获取需要移除的令牌ID。
long endIndex = Math.min(size - limit, 100);
Set<String> tokenSet = conn.zrange("recent:", 0, endIndex - 1);
String[] tokens = tokenSet.toArray(new String[tokenSet.size()]);
// 为那些将要被删除的令牌构建键名。
ArrayList<String> sessionKeys = new ArrayList<String>();
for (String token : tokens) {
sessionKeys.add("viewed:" + token);
}
// 移除最旧的那些令牌。
conn.del(sessionKeys.toArray(new String[sessionKeys.size()]));
conn.hdel("login:", tokens);
conn.zrem("recent:", tokens);
}
}
}
/**
* 2-5
*/
public class CleanFullSessionsThread extends Thread {
private Jedis conn;
private int limit;
private boolean quit;
public CleanFullSessionsThread(int limit) {
this.conn = new Jedis("localhost");
this.conn.select(15);
this.limit = limit;
}
public void quit() {
quit = true;
}
@Override
public void run() {
while (!quit) {
long size = conn.zcard("recent:");
if (size <= limit) {
try {
sleep(1000);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
continue;
}
long endIndex = Math.min(size - limit, 100);
Set<String> sessionSet = conn.zrange("recent:", 0, endIndex - 1);
String[] sessions = sessionSet.toArray(new String[sessionSet.size()]);
ArrayList<String> sessionKeys = new ArrayList<String>();
for (String sess : sessions) {
sessionKeys.add("viewed:" + sess);
// 新增加的这行代码用于删除旧会话对应用户的购物车。
sessionKeys.add("cart:" + sess);
}
conn.del(sessionKeys.toArray(new String[sessionKeys.size()]));
conn.hdel("login:", sessions);
conn.zrem("recent:", sessions);
}
}
}
/**
* 2-8
*/
public class CacheRowsThread extends Thread {
private Jedis conn;
private boolean quit;
public CacheRowsThread() {
this.conn = new Jedis("localhost");
this.conn.select(15);
}
public void quit() {
quit = true;
}
@Override
public void run() {
Gson gson = new Gson();
while (!quit) {
// 尝试获取下一个需要被缓存的数据行以及该行的调度时间戳,
// 命令会返回一个包含零个或一个元组tuple的列表。
Set<Tuple> range = conn.zrangeWithScores("schedule:", 0, 0);
Tuple next = range.size() > 0 ? range.iterator().next() : null;
long now = System.currentTimeMillis() / 1000;
if (next == null || next.getScore() > now) {
try {
// 暂时没有行需要被缓存休眠50毫秒后重试。
sleep(50);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
continue;
}
String rowId = next.getElement();
// 获取下一次调度前的延迟时间。
double delay = conn.zscore("delay:", rowId);
if (delay <= 0) {
// 不必再缓存这个行,将它从缓存中移除。
conn.zrem("delay:", rowId);
conn.zrem("schedule:", rowId);
conn.del("inv:" + rowId);
continue;
}
// 读取数据行。
Inventory row = Inventory.get(rowId);
// 更新调度时间并设置缓存值。
conn.zadd("schedule:", now + delay, rowId);
conn.set("inv:" + rowId, gson.toJson(row));
}
}
}
}

View File

@ -699,9 +699,9 @@ public class Chapter06 {
}
public class TestCallback implements Callback {
public static class TestCallback implements Callback {
public List<Integer> counts = new ArrayList<Integer>();
public List<Integer> counts = new ArrayList<>();
private int index;
@ -719,7 +719,7 @@ public class Chapter06 {
}
public class RedisInputStream extends InputStream {
public static class RedisInputStream extends InputStream {
private Jedis conn;
@ -733,13 +733,13 @@ public class Chapter06 {
}
@Override
public int available() throws IOException {
public int available() {
long len = conn.strlen(key);
return (int) (len - pos);
}
@Override
public int read() throws IOException {
public int read() {
byte[] block = conn.substr(key.getBytes(), pos, pos);
if (block == null || block.length == 0) {
return -1;
@ -749,7 +749,7 @@ public class Chapter06 {
}
@Override
public int read(byte[] buf, int off, int len) throws IOException {
public int read(byte[] buf, int off, int len) {
byte[] block = conn.substr(key.getBytes(), pos, pos + (len - off - 1));
if (block == null || block.length == 0) {
return -1;
@ -766,7 +766,7 @@ public class Chapter06 {
}
public class ChatMessages {
public static class ChatMessages {
public String chatId;
@ -860,6 +860,7 @@ public class Chapter06 {
this.limit = limit;
}
@Override
public void run() {
Deque<File> waiting = new ArrayDeque<File>();
long bytesInRedis = 0;
@ -869,11 +870,7 @@ public class Chapter06 {
recipients.add(String.valueOf(i));
}
createChat(conn, "source", recipients, "", channel);
File[] logFiles = path.listFiles(new FilenameFilter() {
public boolean accept(File dir, String name) {
return name.startsWith("temp_redis");
}
});
File[] logFiles = path.listFiles((dir, name) -> name.startsWith("temp_redis"));
Arrays.sort(logFiles);
for (File logFile : logFiles) {
long fsize = logFile.length();

View File

@ -0,0 +1,7 @@
/**
* Redis Java
*
* @author <a href="mailto:forbreak@163.com">Zhang Peng</a>
* @since 2020-02-26
*/
package io.github.dunwu.db.redis;

View File

@ -4,6 +4,10 @@
### [Redis 入门指南 ⚡](redis-quickstart.md)
### [Redis 数据类型](redis-datatype.md)
![Redis 数据类型](https://raw.githubusercontent.com/dunwu/images/master/snap/20200226113813.png)
### [Redis 持久化](redis-persistence.md)
![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200224214047.png)

View File

@ -4,7 +4,9 @@
>
> 使用 Redis ,不仅要了解其数据类型的特性,还需要根据业务场景,灵活的、高效的使用其数据类型来建模。
## Redis 数据类型简介
## Redis 基本数据类型
![Redis 数据类型](https://raw.githubusercontent.com/dunwu/images/master/snap/20200226113813.png)
| 数据类型 | 可以存储的值 | 操作 |
| -------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------- |
@ -229,7 +231,79 @@ OK
2) "982"
```
## Redis 数据类型应用
## Redis 数据类型通用命令
### 排序
Redis 的 `SORT` 命令可以对 `LIST`、`SET`、`ZSET` 进行排序。
| 命令 | 描述 |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `SORT` | `SORT source-key [BY pattern] [LIMIT offset count] [GET pattern [GET pattern ...]] [ASC|DESC] [ALPHA] [STORE dest-key]`—根据给定选项,对输入 `LIST`、`SET`、`ZSET` 进行排序,然后返回或存储排序的结果。 |
示例:
```shell
127.0.0.1:6379[15]> RPUSH 'sort-input' 23 15 110 7
(integer) 4
127.0.0.1:6379[15]> SORT 'sort-input'
1) "7"
2) "15"
3) "23"
4) "110"
127.0.0.1:6379[15]> SORT 'sort-input' alpha
1) "110"
2) "15"
3) "23"
4) "7"
127.0.0.1:6379[15]> HSET 'd-7' 'field' 5
(integer) 1
127.0.0.1:6379[15]> HSET 'd-15' 'field' 1
(integer) 1
127.0.0.1:6379[15]> HSET 'd-23' 'field' 9
(integer) 1
127.0.0.1:6379[15]> HSET 'd-110' 'field' 3
(integer) 1
127.0.0.1:6379[15]> SORT 'sort-input' by 'd-*->field'
1) "15"
2) "110"
3) "7"
4) "23"
127.0.0.1:6379[15]> SORT 'sort-input' by 'd-*->field' get 'd-*->field'
1) "1"
2) "3"
3) "5"
4) "9"
```
### 键的过期时间
Redis 的 `EXPIRE` 命令可以指定一个键的过期时间当达到过期时间后Redis 会自动删除该键。
| 命令 | 描述 |
| ----------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `PERSIST` | `PERSIST key-name`—移除键的过期时间 |
| `TTL` | `TTL key-name`—查看给定键距离过期还有多少秒 |
| `EXPIRE` | `EXPIRE key-name seconds`—让给定键在指定的秒数之后过期 |
| `EXPIREAT` | `EXPIREAT key-name timestamp`—将给定键的过期时间设置为给定的 UNIX 时间戳 |
| `PTTL` | `PTTL key-name`—查看给定键距离过期时间还有多少毫秒(这个命令在 Redis 2.6 或以上版本可用) |
| `PEXPIRE` | `PEXPIRE key-name milliseconds`—让给定键在指定的毫秒数之后过期(这个命令在 Redis 2.6 或以上版本可用) |
| `PEXPIREAT` | `PEXPIREAT key-name timestamp-milliseconds`—将一个毫秒级精度的 UNIX 时间戳设置为给定键的过期时间(这个命令在 Redis 2.6 或以上版本可用) |
示例:
```shell
127.0.0.1:6379[15]> SET key value
OK
127.0.0.1:6379[15]> GET key
"value"
127.0.0.1:6379[15]> EXPIRE key 2
(integer) 1
127.0.0.1:6379[15]> GET key
(nil)
```
## Redis 数据建模
### 案例-最受欢迎文章
@ -457,35 +531,58 @@ OK
比如:最多允许存储 1000 万条令牌信息,周期性检查,一旦发现记录数超出 1000 万条,将 ZSET 从新到老排序,将超出 1000 万条的记录清除。
```java
while (!quit) {
// 找出目前已有令牌的数量。
long size = conn.zcard("recent:");
// 令牌数量未超过限制,休眠并在之后重新检查。
if (size <= limit) {
try {
sleep(1000);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
continue;
}
public static class CleanSessionsThread extends Thread {
// 获取需要移除的令牌ID。
long endIndex = Math.min(size - limit, 100);
Set<String> tokenSet = conn.zrange("recent:", 0, endIndex - 1);
String[] tokens = tokenSet.toArray(new String[tokenSet.size()]);
private Jedis conn;
// 为那些将要被删除的令牌构建键名。
ArrayList<String> sessionKeys = new ArrayList<String>();
for (String token : tokens) {
sessionKeys.add("viewed:" + token);
}
private int limit;
// 移除最旧的那些令牌。
conn.del(sessionKeys.toArray(new String[sessionKeys.size()]));
conn.hdel("login:", tokens);
conn.zrem("recent:", tokens);
private volatile boolean quit;
public CleanSessionsThread(int limit) {
this.conn = new Jedis("localhost");
this.conn.select(15);
this.limit = limit;
}
public void quit() {
quit = true;
}
@Override
public void run() {
while (!quit) {
// 找出目前已有令牌的数量。
long size = conn.zcard("recent:");
// 令牌数量未超过限制,休眠并在之后重新检查。
if (size <= limit) {
try {
sleep(1000);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
continue;
}
// 获取需要移除的令牌ID。
long endIndex = Math.min(size - limit, 100);
Set<String> tokenSet = conn.zrange("recent:", 0, endIndex - 1);
String[] tokens = tokenSet.toArray(new String[tokenSet.size()]);
// 为那些将要被删除的令牌构建键名。
ArrayList<String> sessionKeys = new ArrayList<String>();
for (String token : tokens) {
sessionKeys.add("viewed:" + token);
}
// 移除最旧的那些令牌。
conn.del(sessionKeys.toArray(new String[sessionKeys.size()]));
conn.hdel("login:", tokens);
conn.zrem("recent:", tokens);
}
}
}
```
### 案例-购物车
@ -543,10 +640,331 @@ OK
### 案例-页面缓存
大部分网页内容并不会经常改变,但是访问时,后台需要动态计算,这可能耗时较多,此时可以使用 `STRING` 结构存储页面缓存,
```java
public String cacheRequest(Jedis conn, String request, Callback callback) {
// 对于不能被缓存的请求,直接调用回调函数。
if (!canCache(conn, request)) {
return callback != null ? callback.call(request) : null;
}
// 将请求转换成一个简单的字符串键,方便之后进行查找。
String pageKey = "cache:" + hashRequest(request);
// 尝试查找被缓存的页面。
String content = conn.get(pageKey);
if (content == null && callback != null) {
// 如果页面还没有被缓存,那么生成页面。
content = callback.call(request);
// 将新生成的页面放到缓存里面。
conn.setex(pageKey, 300, content);
}
// 返回页面。
return content;
}
```
SETEX page_key context 300
### 案例-数据行缓存
电商网站可能会有促销、特卖、抽奖等活动,这些活动页面只需要从数据库中加载几行数据,如:用户信息、商品信息。
可以使用 `STRING` 结构来缓存这些数据,使用 JSON 存储结构化的信息。
此外,需要有两个 `ZSET` 结构来记录更新缓存的时机:
- 第一个为调度有序集合;
- 第二个为延时有序集合。
记录缓存时机:
```java
public void scheduleRowCache(Jedis conn, String rowId, int delay) {
// 先设置数据行的延迟值。
conn.zadd("delay:", delay, rowId);
// 立即缓存数据行。
conn.zadd("schedule:", System.currentTimeMillis() / 1000, rowId);
}
```
定时更新数据行缓存:
```java
public class CacheRowsThread extends Thread {
private Jedis conn;
private boolean quit;
public CacheRowsThread() {
this.conn = new Jedis("localhost");
this.conn.select(15);
}
public void quit() {
quit = true;
}
@Override
public void run() {
Gson gson = new Gson();
while (!quit) {
// 尝试获取下一个需要被缓存的数据行以及该行的调度时间戳,
// 命令会返回一个包含零个或一个元组tuple的列表。
Set<Tuple> range = conn.zrangeWithScores("schedule:", 0, 0);
Tuple next = range.size() > 0 ? range.iterator().next() : null;
long now = System.currentTimeMillis() / 1000;
if (next == null || next.getScore() > now) {
try {
// 暂时没有行需要被缓存休眠50毫秒后重试。
sleep(50);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
continue;
}
String rowId = next.getElement();
// 获取下一次调度前的延迟时间。
double delay = conn.zscore("delay:", rowId);
if (delay <= 0) {
// 不必再缓存这个行,将它从缓存中移除。
conn.zrem("delay:", rowId);
conn.zrem("schedule:", rowId);
conn.del("inv:" + rowId);
continue;
}
// 读取数据行。
Inventory row = Inventory.get(rowId);
// 更新调度时间并设置缓存值。
conn.zadd("schedule:", now + delay, rowId);
conn.set("inv:" + rowId, gson.toJson(row));
}
}
}
```
### 案例-网页分析
网站可以采集用户的访问、交互、购买行为,再分析用户习惯、喜好,从而判断市场行情和潜在商机等。
那么,简单的,如何记录用户在一定时间内访问的商品页面呢?
参考 [更新令牌](#更新令牌) 代码示例,记录用户访问不同商品的浏览次数,并排序。
判断页面是否需要缓存,根据评分判断商品页面是否热门:
```java
public boolean canCache(Jedis conn, String request) {
try {
URL url = new URL(request);
HashMap<String, String> params = new HashMap<>();
if (url.getQuery() != null) {
for (String param : url.getQuery().split("&")) {
String[] pair = param.split("=", 2);
params.put(pair[0], pair.length == 2 ? pair[1] : null);
}
}
// 尝试从页面里面取出商品ID。
String itemId = extractItemId(params);
// 检查这个页面能否被缓存以及这个页面是否为商品页面。
if (itemId == null || isDynamic(params)) {
return false;
}
// 取得商品的浏览次数排名。
Long rank = conn.zrank("viewed:", itemId);
// 根据商品的浏览次数排名来判断是否需要缓存这个页面。
return rank != null && rank < 10000;
} catch (MalformedURLException mue) {
return false;
}
}
```
### 案例-记录日志
可用使用 `LIST` 结构存储日志数据。
```java
public void logRecent(Jedis conn, String name, String message, String severity) {
String destination = "recent:" + name + ':' + severity;
Pipeline pipe = conn.pipelined();
pipe.lpush(destination, TIMESTAMP.format(new Date()) + ' ' + message);
pipe.ltrim(destination, 0, 99);
pipe.sync();
}
```
### 案例-统计数据
更新计数器:
```java
public static final int[] PRECISION = new int[] { 1, 5, 60, 300, 3600, 18000, 86400 };
public void updateCounter(Jedis conn, String name, int count, long now) {
Transaction trans = conn.multi();
for (int prec : PRECISION) {
long pnow = (now / prec) * prec;
String hash = String.valueOf(prec) + ':' + name;
trans.zadd("known:", 0, hash);
trans.hincrBy("count:" + hash, String.valueOf(pnow), count);
}
trans.exec();
}
```
查看计数器数据:
```java
public List<Pair<Integer>> getCounter(
Jedis conn, String name, int precision) {
String hash = String.valueOf(precision) + ':' + name;
Map<String, String> data = conn.hgetAll("count:" + hash);
List<Pair<Integer>> results = new ArrayList<>();
for (Map.Entry<String, String> entry : data.entrySet()) {
results.add(new Pair<>(
entry.getKey(),
Integer.parseInt(entry.getValue())));
}
Collections.sort(results);
return results;
}
```
### 案例-查找IP所属地
Redis 实现的 IP 所属地查找比关系型数据实现方式更快。
#### 载入 IP 数据
IP 地址转为整数值:
```java
public int ipToScore(String ipAddress) {
int score = 0;
for (String v : ipAddress.split("\\.")) {
score = score * 256 + Integer.parseInt(v, 10);
}
return score;
}
```
创建 IP 地址与城市 ID 之间的映射:
```java
public void importIpsToRedis(Jedis conn, File file) {
FileReader reader = null;
try {
// 载入 csv 文件数据
reader = new FileReader(file);
CSVFormat csvFormat = CSVFormat.DEFAULT.withRecordSeparator("\n");
CSVParser csvParser = csvFormat.parse(reader);
int count = 0;
List<CSVRecord> records = csvParser.getRecords();
for (CSVRecord line : records) {
String startIp = line.get(0);
if (startIp.toLowerCase().indexOf('i') != -1) {
continue;
}
// 将 IP 地址转为整数值
int score = 0;
if (startIp.indexOf('.') != -1) {
score = ipToScore(startIp);
} else {
try {
score = Integer.parseInt(startIp, 10);
} catch (NumberFormatException nfe) {
// 略过文件的第一行以及格式不正确的条目
continue;
}
}
// 构建唯一的城市 ID
String cityId = line.get(2) + '_' + count;
// 将城市 ID 及其对应的 IP 地址整数值添加到 ZSET
conn.zadd("ip2cityid:", score, cityId);
count++;
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
try {
reader.close();
} catch (Exception e) {
// ignore
}
}
}
```
存储城市信息:
```java
public void importCitiesToRedis(Jedis conn, File file) {
Gson gson = new Gson();
FileReader reader = null;
try {
// 加载 csv 信息
reader = new FileReader(file);
CSVFormat csvFormat = CSVFormat.DEFAULT.withRecordSeparator("\n");
CSVParser parser = new CSVParser(reader, csvFormat);
// String[] line;
List<CSVRecord> records = parser.getRecords();
for (CSVRecord record : records) {
if (record.size() < 4 || !Character.isDigit(record.get(0).charAt(0))) {
continue;
}
// 将城市地理信息转为 json 结构,存入 HASH 结构中
String cityId = record.get(0);
String country = record.get(1);
String region = record.get(2);
String city = record.get(3);
String json = gson.toJson(new String[] { city, region, country });
conn.hset("cityid2city:", cityId, json);
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
try {
reader.close();
} catch (Exception e) {
// ignore
}
}
}
```
#### 查找 IP 所属城市
操作步骤:
1. 将要查找的 IP 地址转为整数值;
2. 查找所有分值小于等于要查找的 IP 地址的地址,取出其中最大分值的那个记录;
3. 用找到的记录所对应的城市 ID 去检索城市信息。
```java
public String[] findCityByIp(Jedis conn, String ipAddress) {
int score = ipToScore(ipAddress);
Set<String> results = conn.zrevrangeByScore("ip2cityid:", score, 0, 0, 1);
if (results.size() == 0) {
return null;
}
String cityId = results.iterator().next();
cityId = cityId.substring(0, cityId.indexOf('_'));
return new Gson().fromJson(conn.hget("cityid2city:", cityId), String[].class);
}
```
### 案例-服务的发现与配置
### 案例-自动补全
需求:根据用户输入,自动补全信息,如:联系人、商品名等。
@ -562,6 +980,8 @@ SETEX page_key context 300
- 将指定联系人添加到最近联系人列表的最前面。对应 `LPUSH` 命令。
- 添加操作完成后,如果联系人列表中的数量超过 100 个,进行裁剪操作。对应 `LTRIM` 命令。
### 案例-广告定向
### 案例-职位搜索
需求:在一个招聘网站上,求职者有自己的技能清单;用人公司的职位有必要的技能清单。用人公司需要查询满足自己职位要求的求职者;求职者需要查询自己可以投递简历的职位。

View File

@ -0,0 +1,15 @@
# Redis 发布与订阅
Redis 提供了 5 个发布与订阅命令:
| 命令 | 描述 |
| -------------- | ------------------------------------------------------------------- |
| `SUBSCRIBE` | `SUBSCRIBE channel [channel ...]`—订阅指定频道。 |
| `UNSUBSCRIBE` | `UNSUBSCRIBE [channel [channel ...]]`—取消订阅指定频道。 |
| `PUBLISH` | `PUBLISH channel message`—发送信息到指定的频道。 |
| `PSUBSCRIBE` | `PSUBSCRIBE pattern [pattern ...]`—订阅符合指定模式的频道。 |
| `PUNSUBSCRIBE` | `PUNSUBSCRIBE [pattern [pattern ...]]`—取消订阅符合指定模式的频道。 |
## 参考资料
- [《Redis 实战》](https://item.jd.com/11791607.html)

View File

@ -0,0 +1 @@
# Redis 事务