Redis與Java
標籤 : Java與NoSQL
Redis(REmote DIctionary Server) is an open source (BSD licensed), in-memory data structure store, used as database, cache and message broker. It supports data structures such as strings, hashes, lists, sets, sorted sets with range queries, bitmaps, hyperloglogs and geospatial indexes with radius queries. Redis has built-in replication, Lua scripting, LRU eviction, transactions and different levels of on-disk persistence, and provides high availability via Redis Sentinel and automatic partitioning with Redis Cluster.
Redis是一個開源、高性能、基於內存數據結構的Key-Value緩存/存儲系統. 提供多種鍵值數據類型(String, Hash, List, Set, Sorted Set)來適應不同場景下的存儲需求.同時Redis的諸多高級功能可以勝任消息隊列 、任務隊列、數據庫等不同的角色(主頁: redis.io, 中文: redis.cn, 命令: redisfans.com ).
Install
Redis沒有其他外部依賴, 編譯安裝過程非常簡單.
- 編譯安裝
wget http://download.redis.io/releases/redis-3.0.5.tar.gz
make
(32位機器:make 32bit
)make test
make PREFIX=${redis-path} install
安裝完成後,在${redis-path}/bin/
下生成如下二進制文件:
工具 | 描述 |
---|---|
redis-server | 服務端 |
redis-cli | 客戶端 |
redis-benchmark | Redis性能測試工具 |
redis-check-aof | AOF文件修復工具 |
redis-check-dump | RDB文件檢測工具 |
redis-sentinel | Sentinel服務器(僅在2.8之後) |
配置
cp ${redis-3.0.5}/redis.conf ${redis-path}
注: 使Redis以後臺進程的形式運行:
編輯redis.conf配置文件,設置daemonize yes
.啓動
${redis-path}/bin/redis-server ./redis.conf
連接
${redis-path}/bin/redis-cli
連接服務器- h
: 指定server地址- p
: 指定server端口
基礎命令
查詢
KEYS pattern
查詢key
Redis支持通配符格式:*, ? ,[]
:
* |
通配任意多個字符 |
---|---|
? |
通配單個字符 |
[] |
通配括號內的某1個字符 |
\x |
轉意符 |
RANDOMKEY
返回一個隨機存在的keyEXISTS key
判斷key是否存在TYPE key
返回key存儲類型
更新
SET key value
設置一對key-valueDEL key [key...]
刪除key注: 返回真正刪除的key數量, 且
DEL
並不支持通配符.RENAME[NX] key new_key
重命名NX: not exists
new_key
不存在纔對key重命名.move key DB
移動key
到另外一個DB一個Redis進程默認打開16個DB,編號0~15(可在redis.conf中配置,默認爲0),使用
SELECT n
可在多個DB間跳轉.
有效期
TTL/PTTL key
查詢key有效期(以秒/毫秒爲單位,默認-1永久有效)
對於不存在的key,返回-2; 對於已過期/永久有效的key,都返回-1
EXPIRE/PEXPIRE key n
設置key有效期PERSIST key
指定永久有效
Strings
字符串
Strings
是Redis最基本的數據類型,它能存儲任何形式的字符串,如用戶郵箱/JSON化的對象甚至是一張圖片(二進制數據).一個字符串允許存儲的最大容量爲512MB.
字符串類型也是其他4種數據類型的基礎,其他數據類型和字符串的區別從某種角度來說只是組織字符串的形式不同.
常用命令
1. 存/取
SEX key value [EX/PX] [NX/XX]
GET key
# EX/PX: 設置有效時間 [秒/毫秒].
# NX/XX: key存在與否
2. 增/減
INCR key # 指定的key的值加1,並返回加1後的值
DECR key
## 1: 當key不存在時, 新建`<key, 0>`再執行`INCR`;
## 2: INCR/DECR的範圍爲64位有符號整數;
## 3: Redis包括`INCR`在內的所有命令保證是原子操作(可以不用考慮競態條件).
實踐
存儲文章(使用用Jedis)
我們使用**Jedis**客戶端連接Redis並存儲文章數據(關於本篇博客實踐部分的詳細場景講解,可以參考[Redis入門指南][5]一書,在此就不再贅述,下同).
- 使用Jedis需要在pom.xml中添加如下依賴:
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>${jedis.version}</version>
</dependency>
- applicationContext.xml
使用Spring來管理Reids的連接.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- 批量掃描@Component -->
<context:component-scan base-package="com.fq.redis"/>
<bean id="jedis" class="redis.clients.jedis.Jedis">
<constructor-arg name="host" value="aliyun"/>
</bean>
</beans>
- DO: Articles文章
/**
* @author jifang
* @since 16/3/4 下午5:38.
*/
@Message
public class Articles {
private String title;
private String content;
private String author;
private Date time;
// ...
}
- DAO
public interface ArticlesDAO {
/* 文章 */
Long putArticles(Articles articles);
Articles getArticles(Long postID);
}
@Repository
public class ArticlesDAOImpl implements ArticlesDAO {
private static final String POSTS_ID = "posts:id";
private static final String POSTS_DATA = "posts:%s:data";
@Autowired
private Jedis jedis;
@Override
public Long putArticles(Articles articles) {
Long id = jedis.incr(POSTS_ID);
String key = String.format(POSTS_DATA, id);
// 序列化value
MessagePack pack = new MessagePack();
byte[] value;
try {
value = pack.write(articles);
} catch (IOException e) {
value = new byte[0];
}
String result = jedis.set(key.getBytes(), value);
if (!result.equals("OK")) {
id = -1L;
jedis.decr(POSTS_ID);
}
return id;
}
@Override
public Articles getArticles(Long id) {
String key = String.format(POSTS_DATA, id);
byte[] value = jedis.get(key.getBytes());
// 反序列化
MessagePack message = new MessagePack();
try {
return message.read(value, Articles.class);
} catch (IOException e) {
return new Articles();
}
}
}
上面代碼使用了Spring與MessagePack的部分功能,因此需要在pom.xml中添加如下依賴:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.msgpack</groupId>
<artifactId>msgpack</artifactId>
<version>${msgpack.version}</version>
</dependency>
拓展
功能 | 關鍵詞 |
---|---|
增/減指定整數 | INCREBY/DECY key number |
增加指定浮點數 | INCREBYFLOAT key number |
尾部追加 | APPEND key value |
獲取字符串長度 | STRLEN key |
同時設置多個鍵值 | MSET key value [key value ...] |
同時獲得多個鍵值 | MGET key [key ...] |
返回舊值並設置新值 | GETSET key value |
位操作 | GETBIT /SETBIT /BITCOUNT /BITOP |
Hash
散列
Hash
類型的鍵值是一種字典結構, 其存儲了字段(filed)和字段值(value)的映射.
但value只能是字符串,不支持其他數據類型, 且一個Hash
類型Key鍵可以包含至多232-1個字段.
常用命令
1. 存取
HSET key field value
HGET key field
HMSET key field value [field value ...]
HMGET key field [value ...]
HGETALL key
HSET
不區分插入還是更新,當key不存在時,HSET
會自動建立並插入.插入返回1, 更新返回0.
2. 更新
HEXISTS key field # 判斷key下的filed是否存在
HSETNX key field value # 當field不存在時賦值
HINCRBY key field number # 增加數字
HDEL key field [field] # 刪除field.
實踐
添加存儲文章縮略詞
前面使用String存儲整篇文章實際上有一個弊端, 如只需要更新文章標題,需要將篇文章都做更新然後存入Redis,費時費力.因此我們更推薦使用Hash
來存儲文章數據:
這樣即使需要爲文章新添加字段, 也只需爲該Hash
再添加一新key即可, 比如<slug, 文章縮略名>
.
- DAO
@Repository
public class ArticlesDAOImpl implements ArticlesDAO {
private static final String POSTS_ID = "posts:id";
private static final String POSTS_DATA = "posts:%s";
@Autowired
private Jedis jedis;
@Override
public Long putArticles(Articles articles) {
Long id = jedis.incr(POSTS_ID);
String key = String.format(POSTS_DATA, id);
Map<String, String> map = new HashMap<>();
map.put("title", articles.getTitle());
map.put("content", articles.getContent());
map.put("author", articles.getAuthor());
map.put("time", String.valueOf(articles.getTime().getTime()));
String result = jedis.hmset(key, map);
if (!result.equals("OK")) {
id = -1L;
jedis.decr(POSTS_ID);
}
return id;
}
@Override
public Articles getArticles(Long id) {
String key = String.format(POSTS_DATA, id);
Map<String, String> map = jedis.hgetAll(key);
Date time = new Date(Long.valueOf(map.get("time")));
return new Articles(map.get("title"), map.get("content"), map.get("author"), time);
}
}
拓展
功能 | 關鍵詞 |
---|---|
值獲取字段名 | HKEYS key |
只獲取字段值 | HVALS key |
獲取字段數量 | HLEN key |
注: 除了
Hash
, Redis的其他數據類型同樣不支持類型嵌套, 如集合類型的每個元素只能是字符串, 不能是另一個集合或Hash
等.
List
列表
List
可以存儲一個有序的字符串列表, 其內部使用雙向鏈表實現, 所以向列表兩端插入/刪除元素的時間複雜度爲O(1)
,而且越接近兩端的元素速度就越快.
不過使用鏈表的代價是通過索引訪問元素較慢(詳細可參考博客雙向循環鏈表的設計與實現). 一個列表類型最多能容納232-1個元素.
常用命令
1. 兩端壓入/彈出
LPUSH/LPUSHX key value [key value ...]
LPOP key
RPUSH/RPUSHX key value [key value ...]
RPOP key
2. 查詢
LLEN key
LRANGE key start stop
LLEN命令的時間複雜度爲O(1): Reids會保存鏈表長度, 不必每次遍歷統計.
3. 刪除
LREM key count value # count>0:表頭刪除; count<0:表尾刪除; count=0:全部刪除
LTRIM key start stop # 只保留[start,stop)內值
實踐
存儲文章評論列表
考慮到評論時需要存儲評論的全部數據(姓名/聯繫方式/內容/時間等),所以適合將一條評論的各個元素序列化爲String之後作爲列表的元素存儲:
- DO: Comment
@Message
public class Comment {
private String author;
private String email;
private Date time;
private String content;
// ...
}
- DAO
@Repository
public class CommentDAOImpl implements CommentDAO {
@Autowired
private Jedis jedis;
private static final String POSTS_COMMENTS = "posts:%s:comments";
@Override
public void addComment(Long id, Comment comment) {
MessagePack pack = new MessagePack();
String key = String.format(POSTS_COMMENTS, id);
byte[] value;
try {
value = pack.write(comment);
} catch (IOException e) {
value = new byte[0];
}
jedis.lpush(key.getBytes(), value);
}
@Override
public List<Comment> getComments(Long id) {
String key = String.format(POSTS_COMMENTS, id);
List<byte[]> list = jedis.lrange(key.getBytes(), 0, -1);
List<Comment> comments = new ArrayList<>(list.size());
MessagePack pack = new MessagePack();
for (byte[] item : list) {
try {
comments.add(pack.read(item, Comment.class));
} catch (IOException ignored) {
}
}
return comments;
}
}
拓展
功能 | 關鍵詞 |
---|---|
獲得指定索引元素值 | LINDEX key index |
設置指定索引元素值 | LSET key index value |
插入元素 | LINSERT key BEFORE|AFTER pivoit value |
將元素從一個列表轉入另一個列表 | RPOPLPUSH source destination |
等待[彈出/轉移][頭/尾]元素 | BLPOP /BRPOP /BRPOPLPUSH |
RPOPLPUSH
是一個很有意思的命令: 先執行RPOP
, 再執行LPUSH
, 先從source列表右邊中彈出一個元素, 然後將其加入destination左邊, 並返回這個元素值, 整個過程是原子的.
根據這一特性可將List作爲循環隊列使用:source與destination相同,
RPOPLPUSH
不斷地將隊尾的元素移到隊首.好處在於在執行過程中仍可不斷向隊列中加入新元素,且允許多個客戶端同時處理隊列.
Set
集合
Set
內的元素是無序且唯一的,一個集合最多可以存儲232-1個字符串.集合類型的常用操作是插入/刪除/判斷是否存在, 由於集合在Redis內部是使用值爲空的HashTable實現, 所以這些操作的時間複雜度爲O(1)
, 另外, Set最方便的還是多個集合之間還可以進行並/交/差的運算.
常用命令
1. 增/刪
SADD key member [member ...] #同一個member只會保存第一個
SREM key member [member ...]
2. 查找
SMEMBERS key # 獲得集合中所有的元素
SISMEMBER key # 判斷是否在集合中
3. 集合間運算
SDIF key [key ...] # 差集
SINTER key [key ...] # 交集
SUNION key [key ...] # 並集
實踐
1. 存儲文章標籤
考慮到一個文章的所有標籤都是互不相同的, 且對標籤的保存順序並沒有特殊的要求, 因此Set
比較適用:
@Repository
public class TagDAOImpl implements TagDAO {
private static final String POSTS_TAGS = "posts:%s:tags";
@Autowired
private Jedis jedis;
@Override
public void addTag(Long id, String... tags) {
String key = String.format(POSTS_TAGS, id);
jedis.sadd(key, tags);
}
@Override
public void rmTag(Long id, String... tags) {
String key = String.format(POSTS_TAGS, id);
jedis.srem(key, tags);
}
@Override
public Set<String> getTags(Long id) {
String key = String.format(POSTS_TAGS, id);
return jedis.smembers(key);
}
}
2. 通過標籤搜索文章: 列出某個(或同屬於某幾個)標籤下的所有文章.
在提出這樣的需求之後, 前面的posts:[ID]:tags
文章維度的存儲結構就不適用了, 因此借鑑索引倒排的思想, 我們使用tags:[tag]:posts
這種標籤維度的數據結構:
在這種結構下, 根據標籤搜索文章就變得不費吹灰之力, 而Set
自帶交/並/補的支持, 使得多標籤文章搜索有也變得十分簡單:
@Repository
public class TagDAOImpl implements TagDAO {
private static final String POSTS_TAGS = "posts:%s:tags";
private static final String TAGS_POSTS = "tags:%s:posts";
@Autowired
private Jedis jedis;
@Autowired
private ArticlesDAO aDAO;
@Override
public void addTag(Long id, String... tags) {
String key = String.format(POSTS_TAGS, id);
if (jedis.sadd(key, tags) != 0L) {
// 倒排插入
for (String tag : tags) {
String rKey = String.format(TAGS_POSTS, tag);
jedis.sadd(rKey, String.valueOf(id));
}
}
}
@Override
public void rmTag(Long id, String... tags) {
String key = String.format(POSTS_TAGS, id);
if (jedis.srem(key, tags) != 0L) {
// 倒排刪除
for (String tag : tags) {
String rKey = String.format(TAGS_POSTS, tag);
jedis.srem(rKey, String.valueOf(id));
}
}
}
@Override
public Set<String> getTags(Long id) {
String key = String.format(POSTS_TAGS, id);
return jedis.smembers(key);
}
@Override
public List<Articles> getArticlesByTag(String tag) {
// 需要首先由 tags:%s:posts 查出文章ID 列表
String rKey = String.format(TAGS_POSTS, tag);
Set<String> ids = jedis.smembers(rKey);
return idToArticles(ids);
}
@Override
public List<Articles> getArticlesByTagInter(String... tags) {
String[] keys = new String[tags.length];
for (int i = 0; i < tags.length; ++i) {
keys[i] = String.format(TAGS_POSTS, tags[i]);
}
Set<String> ids = jedis.sinter(keys);
return idToArticles(ids);
}
@Override
public List<Articles> getArticlesByTagUnion(String... tags) {
String[] keys = new String[tags.length];
for (int i = 0; i < tags.length; ++i) {
keys[i] = String.format(TAGS_POSTS, tags[i]);
}
Set<String> ids = jedis.sunion(keys);
return idToArticles(ids);
}
private List<Articles> idToArticles(Set<String> ids) {
List<Articles> articles = new ArrayList<>();
for (String id : ids) {
articles.add(aDAO.getArticles(Long.valueOf(id)));
}
return articles;
}
}
拓展
功能 | 關鍵詞 |
---|---|
獲得集合中元素數 | SCARD key |
集合運算並將結果存儲 | SDIFFSTORE/SINTERSTORE/SUNIONSTORE destination key [key ...] |
隨機獲得集合中的元素 | SRANDMEMBER key [count] |
隨機彈出集合中的一個元素 | SPOP key |
Sorted-Sets
有序集合
Sorted-Sets
在Set
基礎上爲每個元素都關聯了一個分數[score
],這使得我們不僅可以完成插入/刪除和判斷元素是否存在等操作,還能夠獲得與score
有關的操作(如score
最高/最低的前N個元素、指定score
範圍內的元素).Sorted-Sets
具有以下特點:
1) 雖然集合中元素唯一, 但score
可以相同.
2) 內部基於HashTable
與SkipList
實現,因此即使讀取中間部分的數據速度也很快(O(log(N))
).
3) 可以通過更改元素score
值來元素順序(與List不同).
常用命令
1. 增/刪/改
ZADD key score member [score member ...]
# score還可以是雙精度浮點數(+inf/-inf分別代表正無窮/負無窮), 相同元素會覆蓋前面的score.
ZREM key member [member ...]
ZREMRANGEBYRANK key start stop
# 按排名範圍刪除[start, stop]範圍內元素.
ZREMRANGEBYSCORE key start stop
# 按分數範圍刪除
ZINCRBY key increment member
# 增加某個元素的score
2. 查詢
ZSCORE key member #獲得元素分數
ZRANGE key start stop [WITHSCORES] #獲得排名在[start, stop]範圍內的元素列表(從小到大, 從0開始)
ZREVRANGE key start stop [WITHSCORES] # (從大到小)
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]
# 獲得指定分數範圍內的元素(如果不希望包含端點值, 可在分數前加'(').
ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]
# 分數從大到小, 且注意min/max顛倒.
實踐
實現文章按點擊量排序
要按照文章的點擊量排序, 就必須再額外使用一個Sorted-Set
類型來實現, 文章ID爲元素,以該文章點擊量爲元素分數.
@Repository
public class BrowseDAOImpl implements BrowseDAO {
private static final String POSTS_BROWSE = "posts:page.browse";
@Autowired
private Jedis jedis;
@Autowired
private ArticlesDAO aDAO;
@Override
public void addABrowse(Long id) {
long score = 1L;
jedis.zincrby(POSTS_BROWSE, score, String.valueOf(id));
}
@Override
public List<Articles> getArticlesByBrowseOrder(Long start, Long end, boolean reverse) {
Set<String> ids;
if (!reverse) {
ids = jedis.zrange(POSTS_BROWSE, start, end);
} else {
ids = jedis.zrevrange(POSTS_BROWSE, start, end);
}
return idToArticles(ids);
}
private List<Articles> idToArticles(Set<String> ids) {
List<Articles> articles = new ArrayList<>();
for (String id : ids) {
articles.add(aDAO.getArticles(Long.valueOf(id)));
}
return articles;
}
}
拓展
功能 | 關鍵詞 |
---|---|
獲得集合中的元素數目 | ZCARD key |
獲得指定分數範圍內的元素個數 | ZCOUNT key min max |
獲得元素排名 | ZRANK/ZREVRANK key member |
ZINTERSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX]
ZINTERSTORE
用來計算多個Sorted-Set
的交集並將結果存儲在destination
, 返回值爲destination
中的元素個數.
AGGREGATE
:
destination
中元素的分數由AGGREGATE
參數決定:SUM
(和/默認),MIN
(最小值),MAX
(最大值)WEIGHTS
通過WEIGHTS
參數設置每個集合的權重,在參與運算時元素的分數會乘上該集合的權重.
ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX]
用法類似
實戰微博
Key設計技巧
參考以往RDBMS的設計經驗:
1. 將表名轉換爲key前綴, 如user:
.
2. 第2段放置用於區分key的字段, 對應於RDBMS中的主鍵, 如user:[uid]:
.
3. 第3段放置要存儲的列名, 如user:[uid]:email
.
需求
微博MiBlog要實現的功能需求:
1. 用戶模塊: 註冊、登錄、新用戶列表;
2. 關係模塊: 關注、取消關注、已關注列表、粉絲列表、共同關注列表;
3. 微博模塊: 發微博、刪微博、已發微博列表、已關注人的微博列表、微博動態流(所有微博列表).
設計與實現
1. 用戶模塊
用戶模塊數據分3個Key存儲: 用戶ID由
user:count
自增生成(String
), 用戶email與id映射關係由user:email.to.id
存儲(Hash
), 用戶真實數據由user:[id]:data
存儲(Hash
):
- User(domain)
public class User {
private Long id;
private String email;
private String nickname;
private String password;
private Long time;
// ...
}
- UserDAO
@Repository
public class UserDAOImpl implements UserDAO {
@Autowired
private Jedis redis;
@Override
public long register(User user) {
long id = -1;
// 當前email沒有註冊過
if (!redis.hexists(Constant.USER_EMAIL_TO_ID, user.getEmail())) {
// 爲用戶生成id
id = redis.incr(Constant.USER_COUNT);
// 插入email -> id 對應關係
redis.hset(Constant.USER_EMAIL_TO_ID, user.getEmail(), String.valueOf(id));
Map<String, String> map = new HashMap<>();
map.put(Constant.EMAIL, user.getEmail());
map.put(Constant.PASSWORD, PasswordUtil.encode(user.getPassword()));
map.put(Constant.NICKNAME, user.getNickname());
map.put(Constant.REGIST_TIME, String.valueOf(System.currentTimeMillis()));
// 寫入user:[id]:data
String key = String.format(Constant.USER_ID_DATA, id);
redis.hmset(key, map);
}
return id;
}
@Override
public boolean login(String email, String password) {
String id = redis.hget(Constant.USER_EMAIL_TO_ID, email);
if (!Strings.isNullOrEmpty(id)) {
String key = String.format(Constant.USER_ID_DATA, id);
Map<String, String> map = redis.hgetAll(key);
return PasswordUtil.checkEqual(password, map.get(Constant.PASSWORD));
}
return false;
}
@Override
public long getUserId(String email) {
String id = redis.hget(Constant.USER_EMAIL_TO_ID, email);
if (!Strings.isNullOrEmpty(id)) {
return Long.valueOf(id);
}
return -1;
}
@Override
public User getUser(long id) {
String key = String.format(Constant.USER_ID_DATA, id);
Map<String, String> map = redis.hgetAll(key);
return Util.mapToSimpleObject(map, User.class);
}
@Override
public List<Long> newUserList(int limit) {
Long maxId = Long.valueOf((redis.get(Constant.USER_COUNT)));
Long minId = maxId - (limit - 1);
if (minId < 1) {
minId = 1L;
}
List<Long> ids = new ArrayList<>((int) (maxId - minId + 1));
for (Long i = maxId; i >= minId; --i) {
ids.add(i);
}
return ids;
}
}
- UserService
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDAO dao;
@Override
public long register(String email, String nickname, String password) {
return dao.register(new User(null, email, nickname, password, null));
}
@Override
public boolean login(String email, String password) {
return dao.login(email, password);
}
@Override
public List<User> newUserList(int limit) {
List<Long> ids = dao.newUserList(limit);
List<User> users = new ArrayList<>(ids.size());
for (Long id : ids) {
users.add(dao.getUser(id));
}
return users;
}
}
2. 關係模塊
關係模塊數據由2個Key存儲: 關注由
relation:following:[id]
存儲(Set
), 被關注由relation:follower:[id]
存儲(Set
):
這樣存的優勢是既可以快速的查詢關注列表, 也可以快速的查詢粉絲列表, 而且還可以基於Redis對Set
的支持, 做共同關注功能.
- Relation(domain)
public class Relation {
private long from;
private long to;
// ...
}
- RelationDAO
@Repository
public class RelationDAOImpl implements RelationDAO {
@Autowired
private Jedis redis;
@Override
public boolean follow(Relation relation) {
if (relation.getFrom() != relation.getTo()) {
// 主動關注
String following = String.format(Constant.RELATION_FOLLOWING, relation.getFrom());
Long result1 = redis.sadd(following, String.valueOf(relation.getTo()));
// 被動被關注
String follower = String.format(Constant.RELATION_FOLLOWER, relation.getTo());
Long result2 = redis.sadd(follower, String.valueOf(relation.getFrom()));
return result1 == 1L && result2 == 1L;
}
return false;
}
@Override
public boolean unfollow(Relation relation) {
if (relation.getFrom() != relation.getTo()) {
// 取消主動關注
String following = String.format(Constant.RELATION_FOLLOWING, relation.getFrom());
Long result1 = redis.srem(following, String.valueOf(relation.getTo()));
// 取消被動關注
String follower = String.format(Constant.RELATION_FOLLOWER, relation.getTo());
Long result2 = redis.srem(follower, String.valueOf(relation.getFrom()));
return result1 == 1L && result2 == 1L;
}
return false;
}
@Override
public List<Long> getFollowings(long id) {
String following = String.format(Constant.RELATION_FOLLOWING, id);
Set<String> members = redis.smembers(following);
return stringToLong(members);
}
@Override
public List<Long> getFollowers(long id) {
String following = String.format(Constant.RELATION_FOLLOWER, id);
Set<String> members = redis.smembers(following);
return stringToLong(members);
}
@Override
public List<Long> withFollowings(long... ids) {
String[] keys = new String[ids.length];
for (int i = 0; i < ids.length; ++i) {
keys[i] = String.format(Constant.RELATION_FOLLOWING, ids[i]);
}
Set<String> sids = redis.sinter(keys);
return stringToLong(sids);
}
private List<Long> stringToLong(Set<String> sets) {
List<Long> list = new ArrayList<>(sets.size());
for (String set : sets) {
list.add(Long.valueOf(set));
}
return list;
}
}
- RelationService
@Service
public class RelationServiceImpl implements RelationService {
@Autowired
private RelationDAO rDAO;
@Autowired
private UserDAO uDAO;
@Override
public boolean follow(long from, long to) {
return rDAO.follow(new Relation(from, to));
}
@Override
public boolean unfollow(long from, long to) {
return rDAO.unfollow(new Relation(from, to));
}
@Override
public List<User> getFollowings(long id) {
List<Long> ids = rDAO.getFollowings(id);
return idToUser(ids);
}
@Override
public List<User> getFollowers(long id) {
List<Long> ids = rDAO.getFollowers(id);
return idToUser(ids);
}
@Override
public List<User> withFollowings(long... ids) {
return idToUser(rDAO.withFollowings(ids));
}
private List<User> idToUser(List<Long> ids) {
List<User> users = new ArrayList<>();
for (Long id : ids) {
users.add(uDAO.getUser(id));
}
return users;
}
}
3. 微博模塊
發微博功能我們採用推模式實現: 爲每個用戶建立一個信箱
List
, 存儲關注的人發的微博, 因此每個用戶在發微博時都需要獲取自己的粉絲列表, 然後爲每個粉絲推送一條微博數據(考慮到一個用戶關注的人過多, 因此實際開發中只存最新1000條即可).
由此微博模塊數據由4個Key存儲: 微博ID由miblog:count
自增生成(String
), 微博真實數據由miblog:[id]:data
存儲(Hash
), 自己發的微博由miblog:[uid]:my
存儲(List
), 推送給粉絲的微博由miblog:[uid]:flow
存儲(List
):
採用推模式的優勢是用戶在查看微博時響應迅速, 而且還可實現針對不同用戶做定向推薦, 但帶來的成本是部分數據冗餘以及用戶發微博邏輯較複雜導致時間開銷較大.因此還可以考慮使用拉模式實現,拉模式節省了發微博的時間陳本, 但用戶讀取微博的速度會降低, 而且很難做定向推薦.因此在實際開發中最好推拉相結合(詳細可參考微博feed系統的推(push)模式和拉(pull)模式和時間分區拉模式架構探討).
- MiBlog(domain)
public class MiBlog {
private Long author;
private String content;
private Long time;
// ...
}
- MiBlogDAO
@Repository
public class MiBlogDAOImpl implements MiBlogDAO {
@Autowired
private Jedis redis;
@Autowired
private RelationDAO relationDAO;
@Override
public long publish(MiBlog miBlog) {
// 獲得微博ID
long id = redis.incr(Constant.MI_BLOG_COUNT);
// 插入微博數據
Map<String, String> map = new HashMap<>();
map.put(Constant.AUTHOR, String.valueOf(miBlog.getAuthor()));
map.put(Constant.TIME, String.valueOf(System.currentTimeMillis()));
map.put(Constant.CONTENT, miBlog.getContent());
String dataKey = String.format(Constant.MI_BLOG_DATA, id);
redis.hmset(dataKey, map);
// 插入到當前用戶已發表微博
String myKey = String.format(Constant.MI_BLOG_MY, miBlog.getAuthor());
redis.lpush(myKey, String.valueOf(id));
// 爲每一個自己的粉絲推送微博消息
// 獲得所有粉絲
List<Long> followers = relationDAO.getFollowers(miBlog.getAuthor());
for (Long follower : followers) {
String key = String.format(Constant.MI_BLOG_FLOW, follower);
redis.lpush(key, String.valueOf(id));
}
return id;
}
@Override
public boolean unpublish(long uid, long id) {
String sId = String.valueOf(id);
String myKey = String.format(Constant.MI_BLOG_MY, uid);
// 確實是uid發佈的微博
if (redis.lrem(myKey, 1L, sId) == 1L) {
// 刪除所有粉絲微博
List<Long> followers = relationDAO.getFollowers(uid);
for (Long follower : followers) {
String flowKey = String.format(Constant.MI_BLOG_FLOW, follower);
redis.lrem(flowKey, 1L, sId);
}
// 刪除微博數據
String dataKey = String.format(Constant.MI_BLOG_DATA, id);
redis.del(dataKey);
return true;
}
return false;
}
@Override
public MiBlog getBlog(long id) {
String key = String.format(Constant.MI_BLOG_DATA, id);
Map<String, String> map = redis.hgetAll(key);
return Util.mapToSimpleObject(map, MiBlog.class);
}
@Override
public List<Long> getMyBlog(long uid) {
String key = String.format(Constant.MI_BLOG_MY, uid);
List<String> sids = redis.lrange(key, 0, -1);
return CollectionUtil.stringToLong(sids);
}
@Override
public List<Long> getFollowingBlog(long uid) {
String key = String.format(Constant.MI_BLOG_FLOW, uid);
List<String> sids = redis.lrange(key, 0, -1);
return CollectionUtil.stringToLong(sids);
}
@Override
public List<Long> getBlogFlow(long uid) {
List<Long> myList = this.getMyBlog(uid);
List<Long> flowList = this.getFollowingBlog(uid);
int myEndIndex = 0;
for (; myEndIndex < myList.size(); ++myEndIndex) {
Long my = myList.get(myEndIndex);
boolean isEnd = true;
for (int i = 0; i < flowList.size(); ++i) {
long flow = flowList.get(i);
if (my > flow) {
flowList.add(i, my);
isEnd = false;
break;
}
}
if (isEnd)
break;
}
// 將所有my < flow的元素填充
flowList.addAll(myList.subList(myEndIndex, myList.size()));
return flowList;
}
}
- MiBlogService
@Service
public class MiBlogServiceImpl implements MiBlogService {
@Autowired
private MiBlogDAO miBlogDAO;
@Override
public long publish(long author, String content) {
return miBlogDAO.publish(new MiBlog(author, content, null));
}
@Override
public boolean unpublish(long uid, long id) {
return miBlogDAO.unpublish(uid, id);
}
@Override
public List<MiBlog> getMyBlog(long uid) {
List<Long> ids = miBlogDAO.getMyBlog(uid);
return idToBlog(ids);
}
@Override
public List<MiBlog> getFollowingBlog(long uid) {
List<Long> ids = miBlogDAO.getFollowingBlog(uid);
return idToBlog(ids);
}
@Override
public List<MiBlog> getBlogFlow(long uid) {
List<Long> ids = miBlogDAO.getBlogFlow(uid);
return idToBlog(ids);
}
private List<MiBlog> idToBlog(List<Long> ids) {
List<MiBlog> blogs = new ArrayList<>();
for (Long id : ids) {
blogs.add(miBlogDAO.getBlog(id));
}
return blogs;
}
}
限於篇幅, 在這兒只列出了最核心的代碼, 詳細代碼可參考Git: 翡青/MiBlog
- 參考&擴展
- Twitter如何使用Redis提高可伸縮性
- 爲什麼不能用memcached存儲Session
- Redis Geo: Redis新增位置查詢功能
- Redis開源文檔《Redis設計與實現》發佈