Redis实战:第一章-初识Redis案例-文章投票

redis全称REmote DIctionary Server,即远程字典服务,是一个由Salvatore Sanfilippo写的key-value存储系统。Redis是一个开源的使用ANSI C语言编写、遵守BSD协议、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。它通常被称为数据结构服务器,因为值(value)可以是 字符串(String), 哈希(Map), 列表(list), 集合(sets) 和 有序集合(sorted sets)等类型。
redis是NoSQL数据库,相对于其他数据库所不同的是,redis并不使用表结构,即并不会提前设计或建立表,而是在使用的时候,直接附带存储结构名。(一般都是字符串,在之后的几章中,为了方便描述,暂时称这种名也为表)

redis特点和应用场景参考博客:六尺帐篷
redis参考文档:http://redisdoc.com/index.html
我理解的优势:

  • 快速存取:相对于一般的关系型数据库,redis数据表之间没有关联,查找和存取都异常的块
  • 由于数据存量少,大多数时候是直接内存操作
  • 相对于同属nosql的memcached,redis有两个主要优势:
    • 有两种写入硬盘的方式:1、快照-直接将所有数据写入硬盘;2、追加-追加一条数据到硬盘
    • 支持5中数据格式,更加适应多种不同的业务需求,更加灵活
  • redis既可以用作主数据库,也可以用作辅助数据库,一般用来作为缓存数据库使用。常见于电商系统,登录系统等
  • redis使用主从数据库配置,所以可以有效避免单台服务器的内存访问压力,这个逻辑相当于分布式服务器。

redis初始案例:文章投票(同类如:新闻点赞,网站发帖等)

  • 文章投票的逻辑:对于一个网站,当用户访问时,我们希望用户看到的文章尽量是最近发表的或者是点赞最多的,这里就需要对文章进行评分排序;另外对于文章发布时间,我们也需要进行保存,以便提供根据发布时间来查看文章顺序,这两样都是需要redis的有序集合结构。
  • 为了防止用户对文章进行多次投票,需要记录一个文章投票用户,使用简单地集合set
  • 为了节约内存,一般情况下,文章发不超过一周,将不能进行投票,这里需要使用一个过期时间,或者直接定义时间进行计算。这个例子并不适用于这里,但是可以想象其他场景,比如一个跳蚤平台,当买家发布一周后,此时的交易很可能已经完成或者基本不可能完成了,也就没必要再占用网站页面,就让他自动死去。
  • 对于删除文章或者添加文章,虽然redis对没单个操作提供原子性,但是多个操作在多线程情况下,可能会发生错误,所以需要使用redis事务处理,由于本章只是提供案例,所以暂时不使用事务逻辑。

详细逻辑代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
import redis.clients.jedis.Jedis;
import redis.clients.jedis.ZParams;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
* @author: ZouTai
* @date: 2018/6/27
* @description: 第一章:初识redis-例子-对文章进行投票
*/
public class Chapter01 {
/**
* 一周总秒数;一次支持加432分;一页包含25篇文章
*/
private static final int ONE_WEEK_IN_SECONDS = 7 * 86400;
private static final int VOTE_SCORE = 432;
private static final int ARTICLES_PER_PAGE = 25;

public static final void main(String[] args) {
new Chapter01().run();
}

public void run() {
// 切换到指定的数据库,数据库索引号 index 用数字值指定,以 0 作为起始索引值。
// 默认使用 0 号数据库。
Jedis conn = new Jedis("localhost");
conn.select(15);

// 1、发表新文章
String articleId = postArticle(
conn, "username", "A title", "http://www.google.com");
System.out.println("We posted a new article with id: " + articleId);
System.out.println("Its HASH looks like:");

// 2、输出所有文章
Map<String, String> articleData = conn.hgetAll("article:" + articleId);
for (Map.Entry<String, String> entry : articleData.entrySet()) {
System.out.println(" " + entry.getKey() + ": " + entry.getValue());
}
System.out.println();

// 3、对文章进行投票
articleVote(conn, "other_user", "article:" + articleId);
String votes = conn.hget("article:" + articleId, "votes"); // 获取文章投票数
System.out.println("We voted for the article, it now has votes: " + votes);
assert Integer.parseInt(votes) > 1;

// 4、输出所有的文章数据
System.out.println("The currently highest-scoring articles are:");
List<Map<String, String>> articles = getArticles(conn, 1);
printArticles(articles);
assert articles.size() >= 1;

// 5、文章分组
addGroups(conn, articleId, new String[]{"new-group"});
System.out.println("We added the article to a new group, other articles include:");
articles = getGroupArticles(conn, "new-group", 1);
printArticles(articles);
assert articles.size() >= 1;
}

/**
* @param conn
* @param user
* @param title
* @param link
* @return 发表新的文章
*/
public String postArticle(Jedis conn, String user, String title, String link) {
String articleId = String.valueOf(conn.incr("article:")); // 即文章id从1依次递增(文章表)
String voted = "voted:" + articleId;
conn.sadd(voted, user); // (投票表)
conn.expire(voted, ONE_WEEK_IN_SECONDS); //(设置过期时间为一周)

long now = System.currentTimeMillis() / 1000;
String article = "article:" + articleId;
HashMap<String, String> articleData = new HashMap<String, String>();
articleData.put("title", title);
articleData.put("link", link);
articleData.put("user", user);
articleData.put("now", String.valueOf(now));// (文章发表时间)
articleData.put("votes", "1");
conn.hmset(article, articleData);// (文章详细数据表)

// 分数score用于对文章进行排序,排序由发表时间和投票数两个因素决定,
// 默认一篇文章一天投票200次,则每一次投票相当于增加432秒的分数
// 初始分数为发表时的当前时间(now+432)
conn.zadd("score:", now + VOTE_SCORE, article); // (分数表-用于排序)
conn.zadd("time:", now, article); // (时间表-用于通过发表时间查找)

return articleId;
}

public void articleVote(Jedis conn, String user, String article) {
// 判断投票文章是否过期(一个星期),过期不能投票
// 判断方法:当前时间-过期时间>文章发表时间(过期)
long cutoff = (System.currentTimeMillis() / 1000) - ONE_WEEK_IN_SECONDS;
if (conn.zscore("time:", article) < cutoff) {
return;
}

String articleId = article.substring(article.indexOf(':') + 1);
if (conn.sadd("voted:" + articleId, user) == 1) { // 一个人只能投一次票,投过票会返回0
conn.zincrby("score:", VOTE_SCORE, article); // 增加分数
conn.hincrBy(article, "votes", 1); // 增加文章投票数
}
}


public List<Map<String, String>> getArticles(Jedis conn, int page) {
return getArticles(conn, page, "score:");
}

public List<Map<String, String>> getArticles(Jedis conn, int page, String order) {
int start = (page - 1) * ARTICLES_PER_PAGE;
int end = start + ARTICLES_PER_PAGE - 1;

Set<String> ids = conn.zrevrange(order, start, end);
List<Map<String, String>> articles = new ArrayList<Map<String, String>>();
for (String id : ids) {
Map<String, String> articleData = conn.hgetAll(id);
articleData.put("id", id);
articles.add(articleData);
}

return articles;
}

/**
* 文章分类/分组
*/
public void addGroups(Jedis conn, String articleId, String[] toAdd) {
String article = "article:" + articleId;
for (String group : toAdd) {
conn.sadd("group:" + group, article);
}
}

public List<Map<String, String>> getGroupArticles(Jedis conn, String group, int page) {
return getGroupArticles(conn, group, page, "score:");
}

/**
*
* @param conn
* @param group
* @param page
* @param order
* @return
* 1、zinterstore交集;
* (Article:id)与(Article:id-score)交集可以对分组文章进行排序
* 即("group:" + group, order)-->("group:new-group", "score:")
* 2、缓存:
* zinterstore太耗时,所以使用临时表,进行缓存(表key)
*/
public List<Map<String, String>> getGroupArticles(Jedis conn, String group, int page, String order) {
String key = order + group;
if (!conn.exists(key)) { // 先判断是否有缓存
ZParams params = new ZParams().aggregate(ZParams.Aggregate.MAX);
conn.zinterstore(key, params, "group:" + group, order);
conn.expire(key, 60); // 缓存1分钟,一分钟后删除表
}
return getArticles(conn, page, key);
}

private void printArticles(List<Map<String, String>> articles) {
for (Map<String, String> article : articles) {
System.out.println(" id: " + article.get("id"));
for (Map.Entry<String, String> entry : article.entrySet()) {
if (entry.getKey().equals("id")) {
continue;
}
System.out.println(" " + entry.getKey() + ": " + entry.getValue());
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
结果如下:
We posted a new article with id: 2
Its HASH looks like:
link: http://www.google.com
votes: 1
title: A title
user: username
now: 1530349369

We voted for the article, it now has votes: 2
The currently highest-scoring articles are:
id: article:2
link: http://www.google.com
votes: 2
title: A title
user: username
now: 1530349369
id: article:1
link: http://www.google.com
votes: 2
title: A title
user: username
now: 1530109319
We added the article to a new group, other articles include:
id: article:2
link: http://www.google.com
votes: 2
title: A title
user: username
now: 1530349369
id: article:1
link: http://www.google.com
votes: 2
title: A title
user: username
now: 1530109319

Process finished with exit code 0
Donate comment here