Redis與Java - 數據結構

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 ).

爲什麼使用 Redis及其產品定位


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 返回一個隨機存在的key
  • EXISTS key 判斷key是否存在
  • TYPE key 返回key存儲類型

更新

  • SET key value 設置一對key-value
  • DEL 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();
        }
    }
}

上面代碼使用了SpringMessagePack的部分功能,因此需要在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作爲循環隊列使用:sourcedestination相同,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-SetsSet基礎上爲每個元素都關聯了一個分數[score],這使得我們不僅可以完成插入/刪除和判斷元素是否存在等操作,還能夠獲得與score有關的操作(如score最高/最低的前N個元素、指定score範圍內的元素).Sorted-Sets具有以下特點:
1) 雖然集合中元素唯一, 但score可以相同.
2) 內部基於HashTableSkipList實現,因此即使讀取中間部分的數據速度也很快(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


參考&擴展

微博關係服務與Redis的故事

Twitter如何使用Redis提高可伸縮性
爲什麼不能用memcached存儲Session
Redis Geo: Redis新增位置查詢功能
Redis開源文檔《Redis設計與實現》發佈

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章