Redis介绍

169 阅读20分钟
  • Redis简介
  • Redis的IO模型
  • Redis的5种基本数据结构介绍及使用场景举例
  • Redis高级数据结构的举例说明
  • Redis两种持久化方式(RDB和AOF)
  • Redis的简单主从模式、哨兵模式

Redis介绍

Redis(Remote Dictionary Server)是一款优秀、高性能、单线程的开源内存型key-value数据库。由于其高性能、多数据类型支持、文档完善性高、社区活跃等特点,使它被广泛的运用在面向互联网的应用系统中,可以说是互联网应用的必备中间件,所以我们了解和学习它是非常用必要的。

Redis为什么单线程还是这么快

基于现代的计算机架构内存的数据存取操作比磁盘操作会高出几个数量级,而它的单线程并不是严格意义上的单线程一些后台也会在执行。

阻塞IO和非阻塞IO模型

阻塞模型 Client发起一个socket.write()会将需要发送的内容写入到发送缓存区,由操作系统将内容真正通过网卡发送出去。当分配给sokect的发送缓存区未满时write一般不会阻塞,若发生阻塞则须等待缓冲区空闲。发送完成后会调用类似的sokect.read(100),如果还没读取到100字节的,该线程会一直阻塞在这儿,直到从接收缓存区读取到100字节的内容或者连接断开为止。

非阻塞模型(待完善) 。

基于事件驱动的多路IO复用模型

其中重要的角色为selector,新来的连接会将其通道注册到其中,利用一个线程去轮询所有注册在其上的连接,当有通道有新的事件响应(accept、read、write等)时交给相应的线程去处理。这样的模型好处是:只需要一个线程就能管理一组连接,当连接无事件时不会有线程阻塞问题。

Redis快原因总结:

  1. 高效的多路复用IO模型
  2. 纯内存的读取和写入操作效率极高

Redis的五种常用数据结构

string 字符串

字符串类型(string)是Redis中最基本的数据结构,其底层类似于维护了一个像ArrayList一样的容器,所以它实际是一个可以动态扩容的一个字符串。值得注意的是value上限512M

简单kv操作

# 单个操作和多个操作
> set user:1:name blue   # set key value 设置单个kv对
OK
> get user:1:name # get key 获取单个key的值
"blue"
> mset user:2:name watermelon user:3:name nightwish # mset [key value..] 同时设置多个kv对
OK
> MGET user:2:name user:3:name # mget [key...]同时获取多个key的值,返回列表
1) "watermelon"
2) "nightwish"
> del user:1:name user:2:name # del [key...]移除一个或多个key。 
(integer) 2

对key做过期时间设置,使其到点后自动删除,作为缓存时可设置失效时间。同时可以使用下述的指令实现分布式锁。

# 过期时间设置
> set user:4:name name4 ex 12 # 方式1:set key value ex seconds 设置一个kv对并设置过期时间为12秒
OK
> ttl user:4:name  # ttl key 指令可以查看当前key剩余的过期时间
(integer) 5
> ttl user:4:name # 过期后返回-2
(integer) -2   
> get user:4:name # 过期后key被移除返回空
(nil)
> SETEX user:7:name 10 name7 # 方式2:setex key expire value 10秒过期 
OK
> set user:5:name name5
OK
> ttl user:5:name  # -1表示永不过期
(integer) -1
> expire user:5:name 10  # 方式3: 直接使用expire指令对key设置过期时间单位:秒

判断性设置

> SETNX user:6:name name6 # setnx key value如果不存在key则设置kv对,返回1
(integer) 1
> SETNX user:6:name name6 # 如果存在key则返回0
(integer) 0

当value为整数时可对value进行简单的自增自减等操作,范围是有符号的long类型最大最小值,超出后报错。

> set user:count 1 # 设置一个value为整数的kv对
OK
> INCR user:count # 对value进行自增
(integer) 2
> DECR user:count # 对value自减
(integer) 1
> INCRBY user:count 3 # incrby key 使用指定步长自增
(integer) 4
> DECRBY user:count 2 # decrby key 使用指定步长自减
(integer) 2

字符串的实际使用场景,实现一个简单的分布式锁

需求:实现每下载一次证明打印次数加1。

问题分析:看似简单的加一,但是在同一证明使用并发下载时打印次数的值是可能会出现错误的。

原因:考虑这样一个事实库中存在A证明且打印次数为0,在并发情况下线程t1读取了A证明并准备更新打印次数为1,t1更新操作未完成时t2线程也读取了A证明因为t1更新还没完成呀,所以t2读取到的打印次数还是0那t2也更新为1吧。两个线程结束后A证明的打印次数为1。显然这个结果是错误的,应该为2。所以在对同一资源进行并发的写操作是会操作问题的。

解决方案:单节点的情况下可以使用JDK自带的锁ReentrantLock或关键字synchronized解决。那集群模式下呢?显然使用jdk锁或synchronized是无法解决的,这时我们需要使用分布式锁来进行控制。

实现一个基于Redis的简单分布式锁

原理:

  • 在对资源操作前,先使用set lock:wszm1 1 ex 5 nx(设置一个key为lock:wszm1的string类型key-value) 指令去redis抢占锁
  • 如果抢占锁成功,则进行后续的资源操作,否则根据具体业务处理(自旋等待上一个线程释放锁、直接返回失败等)
/**
 * <pre>类名: RedisReentrantLock</pre>
 * <pre>描述: 基于redis的简单可重入锁(老钱版)</pre>
 * <pre>日期: 2020/4/18 10:15 下午</pre>
 * <pre>作者: watermleon_r</pre>
 */

public class RedisReentrantLock {

      // 维护当前线程的锁重入次数
    private ThreadLocal<Map<String, Integer>> lockers = new ThreadLocal<Map<String, Integer>>();

    private Jedis jedis;

    public RedisReentrantLock(Jedis jedis) {
        this.jedis = jedis;
    }

    /**
     * 获取当前线程某个key的锁重入数据
     * @return
     */

    private Map<String, Integer> currentLocker() {
        final Map lockCount = lockers.get();
        if (lockCount != null) {
            return lockCount;
        }
        lockers.set(new HashMap<String, Integer>());
        return lockers.get();
    }


    /**
     * @Description: 可重入锁获取
     * @author watermelon_r
     * @date 2020/4/18 10:25 上午
     * @param key
     * @return boolean
     */

    public boolean lock(String key) {
        final Map<String, Integer> locks = currentLocker();
        Integer count = locks.get(key);
        // 这个还没上锁的话
        if (count == null || count.intValue() == 0) {
            final boolean ok = this._lock(key);
            if (!ok) {
                return false;
            }
            locks.put(key, 1);
            return true;
        }
        // 重入锁+1
        locks.put(key, count + 1);
        return true;
    }
         /**
     * @Description: 上锁
     * @author watermelon_r
     * @date 2020/4/18 10:35 上午
     * @param key
     * @return boolean
     */

    private boolean _lock(String key) {
        // 构造 set key value nx ex 5
        SetParams params = new SetParams();
        params.ex(30);
        params.nx();
        return jedis.set(key, "", params) != null;
    }
    public boolean unlock(String key) {
        Map<String, Integer> locks = currentLocker();
        Integer count = locks.get(key);
        if (count == null || count.intValue() == 0) {
            return false;
        }
        if (count > 1) {
            locks.put(key, --count);
        } else {
            lockers.remove();
            this._unlock(key);
        }
        return true;
    }

    private void _unlock(String key) {
        jedis.del(key);
    }
}

list 列表

基于链表实现的list数据结构,链表这样的数据结构意味它的添加删除实践复杂度都是O(1),但定位元素速度相对较慢O(n)。

常用的list操作

127.0.0.1:6379> LPUSH company:1:no 11111 # lpush key ele 从company:1:no列表左边放入一个11111的值
(integer) 1
127.0.0.1:6379> LPUSH company:1:no 22222 
(integer) 2
127.0.0.1:6379> LPUSH company:1:no 33333
(integer) 3
127.0.0.1:6379> LRANGE company:1:no 0 2 # lrang key start stop 取列表某个索引范围的值 
1) "33333"
2) "22222"
3) "11111"
127.0.0.1:6379> LLEN company:1:no # llen key计算列表长度
(integer) 3
127.0.0.1:6379> RPOP company:1:no # rpop key从列表的右边移除一个元素并返回值
"11111"
127.0.0.1:6379> LRANGE company:1:no 0 1
1) "33333"
2) "22222"
127.0.0.1:6379> LPOP company:1:no  # lpop key从左边移除一个列表元素并返回
"33333"
127.0.0.1:6379> LRANGE company:1:no 0 0
1) "22222"
127.0.0.1:6379> LINDEX company:1:no 1 #  lindex key index取某个指定位置上的元素值(不推荐使用)
"22222"

以上操作中LPUSHRPOP两个指令实现了一个队列效果,而LPUSHLPOP则实现了一个栈的效果。

注意:lindex 指令不推荐大list使用,其O(n)的时间复杂度对于redis来说已经非常长了,因为它是单线程的需要为大局考虑。

应用:如果在没有消息队列中间件的情况下,如何实现一个简单的队列。

方案一思路:根据上面简单的list操作,我们发现LPUSH和RPOP可以实现一个队列模型,此时两个业务系统中间可以使用redis的list来构造一个简单的消息队列。

上图说明一个基于list的简单消息队列实现,生产者发送任务消息到list中,消费者轮询该list如果有任务则消费任务。

优点:

  • 由于redis单线程连接,所以一个消息只能被一个消费者服务
  • 实现起来简单,且消费者横向扩展比较容易

缺点:

  • 消费确认机制缺失,ack
  • 队列无数据时,消费者空轮训CPU消耗大

方案二:在方案一的基础上使消费者变成阻塞的操作(brpop key timeout, blocking right pop),当队列无数据时阻塞当前线程降低空轮训导致的cpu占用消耗

优点:解决的空轮训问题

注意:如果线程一直阻塞在那里,Redis客户端的连接就成了闲置连接,闲置过久,服务器一般会主动断开连接,减少闲置资源占用,这个时候blpop和brpop或抛出异常,此时,客户端需要重试否则将失去监听。

Hash

Hash该类型的value为一个KV键值对,类似于Java中的HashMap和Python的字典。

基本的命令使用

> hset user:1 name ljf   # hset key field value 向user:1key中设置一个属性为name值为ljf的kv对
(integer) 1
> hget user:1 name # hget key field 获取user:1 key中属性为name的值
"ljf"
> hset user:1 age 24
(integer) 1
> hgetall user:1 # hgetall key 获取一个key的所有kv对
1) "name"
2) "ljf"
3) "age"
4) "24"
> hlen user:1 # hlen key 计算一个hash key中kv对(属性)的个数
(integer) 2
> hmset user:1 address hangzhou street qingchunload # hmset key f1 v1 f2 v2设置多个kv对
OK
> hlen user:1
(integer) 4
> HINCRBY user:1 age 1 # hincrby key field step 将key中的一个可自增的属性增加 step(步长)
(integer) 25

应用举例

  • 以kv对存储对象属性
  • 以userid为key,商品id为field,数量为value实现一个简单购物车

Set 集合

集合概念和数学中的类似,具有确定性(要么元素属于这个集合要么属于)、无序性(集合元素顺序并不合放入顺序一致)、元素唯一性(一个集合中不存在重复的元素)。Redis中集合同样的能进行集合间常见的运算如交集、差集、并集等。可以运用集合计算完成一些功能

常见的相关集合命令的操作

> SADD gender male # sadd key m1 m2... 像一个集合中添加一个或多个元素 添加成功返回成功个数
(integer) 1
> SADD gender female 
(integer) 1
> SADD gender male # 添加重复元素返回0
(integer) 0
> SMEMBERS gender # smembers key 取一个集合的所有元素
1) "male"
2) "female"
> SISMEMBER gender male # sismember key ele 判断一个元素是否属于该集合
(integer) 1
> SISMEMBER gender males 
(integer) 0
> SCARD gender # scard key 计算一个集合中的元素个数
(integer) 2
> spop gender # spop key 随机从集合中弹出一个元素
"male"
> SADD gender male
(integer) 1
> SADD gender1 female
(integer) 1
> SINTER gender gender1 # sinter key1 key2 计算两个集合的交集 sinterstore des key1 key2 计算交集并存在des中
1) "female"
> SDIFF gender gender1 # sdiff key1 key2 计算两个集合的差集
1) "male"
> sdiffstore des_gender  gender gender1 # sdiffstore des key1 key2 计算两个集合的差集并存在des中
(integer) 1
> SMEMBERS des_gender
1) "male"
> SRANDMEMBER gender 1 # srandmember key count 从集合中随机返回count个元素
1) "male"
> srem gender male [member ...] # srem key ele 从集合中移除一个或多个元素
...

利用集合的特性或者运算我们能做些啥

  • 集合的元素唯一性我们可以统计记录使用应用某个功能的用户
  • 利用交集运算可以简单实现类似微博的共同关注的功能

SortedSet

Redis 有序集合和集合一样也是string类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。有序集合的成员是唯一的,但分数(score)却可以重复。集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1),类似Java的TreeSet和TreeMap。

常用的Zset命令

> ZADD math 98 ljf # ZADD key score1 ele1 score2 ele2.. 向有序集合添加一个或多个成员,或者更新已存在成员的分数
(integer) 1
> ZADD math 99 jeff
(integer) 1
> ZADD math 89 tom
(integer) 1
> ZCARD math  # ZCARD key 获取有序集合的成员数
(integer) 3
> ZCOUNT math 89 98 # ZCOUNT key 统计score在[89,98]中的成员个数,包含边界
(integer) 2
> ZINCRBY math 1 jeff # ZINCRBY key increment ele ,对某个成员jeff的分数增加1
"100"
> ZADD english 80 jeff 
(integer) 1
> ZINTERSTORE score 2 math english # ZINTERSTORE des numkeys key1 key2 [key..]对2个集合math和english求交集,对应的成员分数会相加。
(integer) 1
> ZRANGE score 0 -1 withscores # ZRANGE key start stop [withscores] 过索引区间返回有序集合指定间                                           内的成员,withscores选项表示带分数输出
1) "jeff"
2) "180"
> ZREVRANGE math 0 -1 withscores # ZREVRANGE key start stop [withscores] 过索引区间逆序返回有序集合指定区间成员withscores选项表示带分数输出
1) "jeff"
2) "100"
3) "ljf"
4) "98"
5) "tom"
6) "89"
> ZRANK math jeff # ZRANK key ele 在从小到大排序情况下,返回有序集合中指定成员的索引
(integer) 2
> ZREVRANK math jeff # ZREVRABK key ele 在从大到小排序情况下情况下返回有序集合中指定成员的索引
(integer) 0
> ZREM score jeff #  ZREM key ele1 ele2 [ele..] 移除一个或多个成员
(integer) 1
> ZSCORE math jeff # ZSCORE key ele 返回有序集合中成员的分数
"100"

有序集合的使用

  • 类似微博的热搜排行榜

  • 运用有序集合实现简单的接口限流策略

    需求:在一个时间窗口内,某个服务的调用次数达到一定数量时开始限流策略。

    实现:

Redis高级数据结构的举例说明(待写)

GeoHash

HyperLoglog

Redis两种持久化方式(RDB和AOF)

Redis相比与Memcache不仅是提供了出字符串意外的数据结构,其数据可持久化也是相对于memcache的一个大特性,在断电宕机等极端情况下尽可能的保证了Redis的数据可恢复。Redis提供了两种数据持久化方案 RDB(snapshotting)AOF(append-only-file)

RDB

RDB是Redis中进行数据持久化的其中一种方式,会将内存中的数据进行二进制序列化并写入磁盘结构较为紧凑,RDB是一次内存中某个时间点前的数据全量备份。

RDB的触发时机

  • 手动触发(命令行模式)
  1. bgsave命令触发RDB,Redis会fork一个子进程进行这一次的快照同步,在fork阶段是存在阻塞的,redis需要将当前进程中数据copy到子进程中,完成fork后子进程会在后台进行快照持久化,主进程继续提供服务。
  2. save命令出发RDB,该命令执行会阻塞当前的redis服务直到整个RDB过程结束,所以在存在大量key的时候是禁止使用该命令的,长时间的阻塞会影响redis提供服务导致客户端不可用。
  • 自动触发(配置文件模式)

    ​ 相关配置

    ​ 1. 在redis.confSNAPSHOTTING部分配置 save 10 100,代表的是在10秒中内,Redis写操作达到了100次则会触发bgsave

    默认配置:

    # 如果在900秒发生了至少一次写操作则触发RDB
    save 900 1  
    # 如果在300秒发生了至少10次写操作则触发RDB
    save 300 10
    # 如果在60秒发生了至少10000次写操作则触发RDB
    save 60 10000

    #
     save ""  代表不会使用RDB持久化
  1. rdbcompression yes|no ,默认为yes,是否开启对rdb的文件进行LZF压缩,需要消耗一定的cpu资源。

  2. rdbchecksum yes|no,默认yes,开启对rdb文件进行CRC64的校验。有10%的性能消耗,如果需要性能发挥的更好可以设置为no

  3. dbfilename "dump.rdb" rdb持久化文件名称

RDB的优缺点

  • 优点
  • 持久化后的数据文件紧凑,作为备份文件相当合适
  • fork出子进程对于主进程更加的友好,不会导致主进程阻塞和IO
  • 适合大数据的备份恢复,相比与AOF更快
  • 缺点
  • 有条件的触发RDB,在Redis意外宕机时会丢失最后一次RDB之后的数据
  • 每次RDB都需要fork一个子进程,这是一个非常耗时的操作,如果是大数据集时,对客户端会有几毫秒甚至1秒的停止服务时间

AOF(待写)

AOF是Redis的另一种数据持久化方式,它通过追加对Redis的每一个操作命令,最后形成一个操作日志文件。

基于Docker的Redis简单一主一从

配置简单主从步骤

  1. master节点无需过多配置直接启动(假设ip:172.18.0.2 port:6379)

  2. slave节点在redis.conf里添加配置SLAVEOF 172.18.0.2 6379

  3. 启动从节点

  4. 验证是否成功在master上使用命令info Repliation

    image-20200504171853544
    image-20200504171853544

基于Docker的Redis哨兵模式(待写)

参考资料

  • Redis 深度历险核心原理和应用实践》—— 钱文品
  • https://redis.io/documentation 官网地址