Redis从入门到入土

一. Redis简介

1.1. Redis是什么

Remote Dictionary Server,即远程字典服务。
是c语言编写的key-value数据库,提供多种语言API。
Redis会周期性把更新的数据写入磁盘或把修改操作写入追加的记录文件,并且在此基础上是实现了master-slave主从同步。

1.2. Redis能干什么?

  1. 内存存储、持久化(RDB & AOF)
  2. 高速缓存
  3. 发布订阅系统(简单消息队列)
  4. 地图信息分析
  5. 计时器、计数器
  6. 分布式锁
  7. 排序

1.3. Redis 特性

  1. 数据类型多
  2. 持久化
  3. 集群 哨兵
  4. 事务
  5. Lua脚本

二. 命令以及数据结构

参考阅读材料:

  1. https://github.com/huangz1990/redis-3.0-annotated redis3.0源码
  2. redis6.25源码
    书籍:
  3. 《Redis深度历险》
  4. 《Redis设计与实现》

2.1. 基础命令

如果key特别多,可以考虑用scan通过游标cursor分步进行,并设置limit和offset,注意,这里的limit指的是遍历的槽位

遍历的顺序是高位加法,从左往右加,是为了扩容和缩容的时候,不会产生重复遍历。

scan也可扫描四种基础数据结构

127.0.0.1:6379> set name hzh
OK
127.0.0.1:6379> get name
"hzh"

// getset 返回get的值
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> getset name hello
(nil)
127.0.0.1:6379> getset name hello1
"hello"

// set with expire
127.0.0.1:6379> setex name 10 hello
OK

// set if not exists
// 这个命令在分布式锁的时候经常用到:和expire配合,通过LUA脚本或者后来更新的融合命令可以解决死锁问题
127.0.0.1:6379> setnx name world
(integer) 1
127.0.0.1:6379> get name
"world"
127.0.0.1:6379> setnx name world
(integer) 0

// 批量设置与获取
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3
OK
127.0.0.1:6379> keys *
1) "name"
2) "k2"
3) "k1"
4) "k3"
127.0.0.1:6379> mget k1 k2 k3
1) "v1"
2) "v2"
3) "v3"
// 原子性操作 multiset if not exists ,一个失败就不执行
127.0.0.1:6379> msetnx k1 v1 k4 v3
(integer) 0

// 切换到数据库1(一共有16个,0~15)
127.0.0.1:6379> select 1
OK
// 获取所有的key
127.0.0.1:6379[1]> keys *
(empty array)
127.0.0.1:6379[1]> select 0
OK

127.0.0.1:6379> del name
(integer) 1
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> set name hello
OK

// 将name转移到数据库1
127.0.0.1:6379> move name 1
(integer) 1
127.0.0.1:6379> select 1
OK
127.0.0.1:6379[1]> keys *
1) "name"

// 设置name5秒过期
127.0.0.1:6379[1]> expire name 5
(integer) 1
127.0.0.1:6379[1]> ttl name
(integer) 3
127.0.0.1:6379[1]> ttl name
(integer) 1

// -2代表不存在, -1代表没设置过期时间
127.0.0.1:6379[1]> ttl name
(integer) -2
127.0.0.1:6379[1]> keys *
(empty array)

// 查看类型,none为空
127.0.0.1:6379[1]> type name
none
127.0.0.1:6379[1]> set name hello
OK
127.0.0.1:6379[1]> type name
string

// 清空所有
127.0.0.1:6379[1]> flushall
OK

// 清空单个数据库
127.0.0.1:6379[1]> flushdb
OK

2.2. 字符串

动态字符串SDS,simple dynamic string
在redis中除了无需对字符串值进行修改的地方,比如打印日志,其他所有的字符串底层都是由SDS实现。
SDS遵循了c语言字符串的特点,末尾添加了’\0’,以此来复用c语言的一部分<string.h>库函数。

先来看一下sds3.0的结构:

/*
 * 保存字符串对象的结构
 */
struct sdshdr {
    
    // buf 中已占用空间的长度
    int len;

    // buf 中剩余可用空间的长度
    int free;

    // 数据空间
    char buf[];
};

可以看到内部还是由字符数组来存储数据的,所以打印函数就是直接printf %s即可;
这个字符数组往往被称作字节数组。因为Redis是用它来保存二进制数组的,也就是可以存储图像音频等二进制数据,允许字符串中包含’\0’(因为是通过len来判断是否字符串的结束),所有sds的api都是二进制安全的。

这里设立了len和free两个参数,是为了降低获取字符串长度的时间复杂度为O(1)并解决c字符串容易造成缓冲区溢出的弊端。SDS不需要手动编辑len与free,作者将其全部封装好了,长度变化的时候,len会自动变化,free不足的时候会自动扩容。

/*
 * 最大预分配长度
 */
#define SDS_MAX_PREALLOC (1024*1024)

/*
 * 对 sds 中 buf 的长度进行扩展,确保在函数执行之后,
 * buf 至少会有 addlen + 1 长度的空余空间
 * (额外的 1 字节是为 \0 准备的)
 *
 * 返回值
 *  sds :扩展成功返回扩展后的 sds
 *        扩展失败返回 NULL
 *
 * 复杂度
 *  T = O(N)
 */
sds sdsMakeRoomFor(sds s, size_t addlen) {

    struct sdshdr *sh, *newsh;

    // 获取 s 目前的空余空间长度
    size_t free = sdsavail(s);

    size_t len, newlen;

    // s 目前的空余空间已经足够,无须再进行扩展,直接返回
    if (free >= addlen) return s;

    // 获取 s 目前已占用空间的长度
    len = sdslen(s);
    sh = (void*) (s-(sizeof(struct sdshdr)));

    // s 最少需要的长度
    newlen = (len+addlen);

    // 根据新长度,为 s 分配新空间所需的大小
    if (newlen < SDS_MAX_PREALLOC)
        // 如果新长度小于 SDS_MAX_PREALLOC 
        // 那么为它分配两倍于所需长度的空间
        newlen *= 2;
    else
        // 否则,分配长度为目前长度加上 SDS_MAX_PREALLOC
        newlen += SDS_MAX_PREALLOC;
    // T = O(N)
    newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);

    // 内存不足,分配失败,返回
    if (newsh == NULL) return NULL;

    // 更新 sds 的空余长度
    newsh->free = newlen - len;

    // 返回 sds
    return newsh->buf;
}

扩容策略:1MB以下翻倍,1MB以上增加1MB,上限512MB。
缩容策略:惰性空间释放,即不释放多余的空间。

但看3.2之后的SDS代码,我们发现它产生了巨大的变化,最基本的字符串里加了一个特殊标识符,并且有五种大小不同的字符串的类型。除此之外,在长度小于等于44字节的时候,会以embstr的形式存储;大于44字节会以raw形式存储。embstr存储形式是将RedisObject对象头结构和SDS连续存储在一起,而raw是分两次malloc的,内存地址一般不连续。

typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
                            * LFU data (least significant 8 bits frequency
                            * and most significant 16 bits access time). */
    int refcount;
    void *ptr;
} robj;

下面是6.25的sds

struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* used */
    uint8_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; /* used */
    uint32_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* used */
    uint64_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};

#define SDS_TYPE_5  0
#define SDS_TYPE_8  1
#define SDS_TYPE_16 2
#define SDS_TYPE_32 3
#define SDS_TYPE_64 4

内存分配器jemalloc、tcmalloc的分配内存单位为2/4/8/16/32/64B,可以看到redisObject中有4b + 4b + 24b+ 4B +8B = 16B
还有SDS中的3B,留给content的最大空间只有64B-3B-16B=45B,还有留给字符串终结符‘\0’的一个B,所以字符串内容最多只有44B

127.0.0.1:6379> set name v1
OK
// 如果不存在,相当于set
127.0.0.1:6379> append name hello
(integer) 7
127.0.0.1:6379> get name
"v1hello"

127.0.0.1:6379> strlen name
(integer) 7

127.0.0.1:6379> set views 0
OK
127.0.0.1:6379> get views
"0"
// 自增自减
127.0.0.1:6379> incr views
(integer) 1
127.0.0.1:6379> decr views
(integer) 0
// 步长
127.0.0.1:6379> incrby views 2
(integer) 2
127.0.0.1:6379> decrby views 2
(integer) 0

// substr
127.0.0.1:6379> SET name hello
OK
127.0.0.1:6379> GETRANGE name 0 1
"he"
127.0.0.1:6379> GETRANGE name 0 -1
"hello"

// replace
127.0.0.1:6379> SETRANGE name 1 oly
(integer) 5
127.0.0.1:6379> get name
"holyo"

2.3. 列表

用链表实现,所以尽量避免索引定位;
可充当栈和队列;
老版本的list存储策略是:在列表元素较少的时候,会使用一块连续的内存存储,即ziplist,压缩列表;当元素个数超过512或者任意元素长度超过64,就会使用linkedlist。
而新版本的list:则用ziplist和链表组合成quicklist取代了之前的方案,在满足快速插入删除的基础上,减少了大量指针带来的空间冗余。

typedef struct listNode {
    struct listNode *prev;
    struct listNode *next;
    void *value;
} listNode;

typedef struct listIter {
    listNode *next;
    int direction;
} listIter;

typedef struct list {
    listNode *head;
    listNode *tail;
    void *(*dup)(void *ptr);
    void (*free)(void *ptr);
    int (*match)(void *ptr, void *key);
    unsigned long len;
} list;
// 插入列表头部
127.0.0.1:6379> lpush list one
(integer) 1
127.0.0.1:6379> lpush list two
(integer) 2
127.0.0.1:6379> lpush list three
(integer) 3
127.0.0.1:6379> lrange list  0 -1
1) "three"
2) "two"
3) "one"
127.0.0.1:6379> Llen list
(integer) 3

// 插入列表尾部
127.0.0.1:6379> rpush list zero
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
3) "one"
4) "zero"

// 弹出列表头部
127.0.0.1:6379> lpop list
"three"
// 弹出列表尾部
127.0.0.1:6379> rpop list
"zero"
// 通过下标获得值
127.0.0.1:6379> lindex list 0
"two"

// 删除指定个数的value
127.0.0.1:6379> lrem list 1 two
(integer) 1
127.0.0.1:6379> lrange list 0 -1
1) "one"

127.0.0.1:6379> lpush mylist hello
(integer) 1
127.0.0.1:6379> lpush mylist hello12
(integer) 2
127.0.0.1:6379> lpush mylist hello34
(integer) 3
//删掉start到end以外的全部元素
127.0.0.1:6379> ltrim mylist 0 1
OK
127.0.0.1:6379> lrange mylist 0 -1
1) "hello34"
2) "hello12"

// 组合命令
127.0.0.1:6379> rpoplpush mylist list2
"hello12"
127.0.0.1:6379> lrange mylist 0 -1
1) "hello34"

// 更新列表中下标对应的值
127.0.0.1:6379> lset mylist 0 now
OK
127.0.0.1:6379> lrange mylist 0 -1
1) "now"

// 往前插入
127.0.0.1:6379> linsert mylist before now before
(integer) 2
127.0.0.1:6379> lrange mylist 0 -1
1) "before"
2) "now"
127.0.0.1:6379> linsert mylist after now afterElement
(integer) 3
127.0.0.1:6379> lrange mylist 0 -1
1) "before"
2) "now"
3) "afterElement"

// 顺序排序
127.0.0.1:6379> LPUSH list 7 1 8 9 3 0 10
(integer) 7
127.0.0.1:6379> SORT list
1) "0"
2) "1"
3) "3"
4) "7"
5) "8"
6) "9"
7) "10"
// 逆序排序
127.0.0.1:6379> SORT list desc
1) "10"
2) "9"
3) "8"
4) "7"
5) "3"
6) "1"
7) "0"

2.4. Set

value恒为空的hashmap
元素都是整数,并且个数不多的set会使用intset这种紧凑的整数数组结构来存储,
如果set里存储的是字符串或者整数元素个数超过512,sadd立即升级为hashtable

127.0.0.1:6379> sadd myset hello
(integer) 1
127.0.0.1:6379> sadd myset hello1
(integer) 1
127.0.0.1:6379> sadd myset hello2
(integer) 1
127.0.0.1:6379> sadd myset hello
(integer) 0

// show
127.0.0.1:6379> smembers myset
1) "hello1"
2) "hello"
3) "hello2"

// 判断是否在set中
127.0.0.1:6379> sismember myset hello
(integer) 1

// getLen
127.0.0.1:6379> scard myset
(integer) 3

// 移除set中的指定元素
127.0.0.1:6379> srem myset hello
(integer) 1
127.0.0.1:6379> scard myset
(integer) 2

// 随机抽出指定个数元素
127.0.0.1:6379> SRANDMEMBER myset
"hello2"
127.0.0.1:6379> SRANDMEMBER myset
"hello1"
127.0.0.1:6379> SRANDMEMBER myset 2
1) "hello1"
2) "hello2"

// 随机弹出一个元素
127.0.0.1:6379> spop myset
"hello2"

// 移动一个元素到另一个set中,可以移到空的set中
127.0.0.1:6379> smove myset myset2 well
(integer) 1

// 差交并集
127.0.0.1:6379> sadd set1 a
(integer) 1
127.0.0.1:6379> sadd set1 b
(integer) 1
127.0.0.1:6379> sadd set1 c
(integer) 1
127.0.0.1:6379> sadd set2 c
(integer) 1
127.0.0.1:6379> sadd set2 d
(integer) 1
127.0.0.1:6379> sadd set2 e
(integer) 1
127.0.0.1:6379> sdiff set1 set2
1) "b"
2) "a"
127.0.0.1:6379> sinter set1 set2
1) "c"
127.0.0.1:6379> sunion set1 set2
1) "e"
2) "a"
3) "c"
4) "b"
5) "d"

2.5. Hash

jdk1.8之前的hashmap,数组+链表,存储消耗高于字符串;

当数据结构很小的时候,会使用ziplist去存储,key和value会作为两个entry被相邻存储;当元素个数朝超过512或者任意key/value长度超过64,必须使用标准结构存储。

rehash策略与hashmap不同,为了高性能,采用渐进式rehash策略:在hash的过程中,保留新旧两个hash结构,查询的时候,会同时查询两个hash结构,然后在后续的定时任务一级hash操作指令中,循序渐进地将旧hash的内容迁移到新hash中,迁移完成后,旧hash被自动删除回收。

在遍历hash的过程中,会删除过期的key,指针的操作十分复杂;
选择安全的迭代器,需要对遍历的元素打标记,不会出现重复遍历元素的现象;选择不安全的迭代器,则会出现重复遍历元素的现象。

hash可以来存储用户信息,对用户结构中的每个字段单独存储,可以选择需要的信息获取,节省网络流量。

127.0.0.1:6379> hset hash1 name hzh
(integer) 1
127.0.0.1:6379> hget hash1 name
"hzh"

// 设置多个
127.0.0.1:6379> hmset myhash name hzh1 age 18
OK

// 被弃用
127.0.0.1:6379> hmget myhash name age
1) "hzh1"
2) "18"

// 长度
127.0.0.1:6379> hlen myhash
(integer) 2

// getAll
127.0.0.1:6379> HGETALL myhash
1) "name"
2) "hzh1"
3) "age"
4) "18"

// isExists
127.0.0.1:6379> hexists myhash name
(integer) 1

// 获得键和值
127.0.0.1:6379> hkeys myhash
1) "name"
2) "age"
127.0.0.1:6379> HVALS myhash
1) "hzh1"
2) "18"

// 自增量
127.0.0.1:6379> HINCRBY myhash age 1
(integer) 19

// 如果存在,不可以设置;存在,可以设置
127.0.0.1:6379> hsetnx myhash age 18
(integer) 0

2.6. ZSet 有序Set

zset内部是跳表,对于新插入的节点,调用随机算法分配合理的层数。
跳表的效率可以和平衡树媲美,但实现比平衡树简单得多。

如果数据结构很小,会使用ziplist去存储,value和score会作为两个entry被相邻存储,
如果元素个数超过128或者任意元素长度超过64,必须用标准结构存储

127.0.0.1:6379> zadd set1 1 one
(integer) 1
127.0.0.1:6379> zadd set1 2 two 3 three
(integer) 2

注意 zRange和zRevRange是排序完成后,
根据索引(从零开始)来取值的,
给的两个参数是排序好后的起始索引以及终止索引。

而加了ByScore,则是根据score值的大小,
依序输出这个区间内的所有值,
给的两个参数是排序好后的下界和上界。


// 顺序排序 zRange
127.0.0.1:6379> zrange set1 0 -1
1) "one"
2) "two"
3) "three"

// 顺序排序 zRangeByScore
127.0.0.1:6379> ZRANGEBYSCORE set1 -inf +inf
1) "one"
2) "two"
3) "three"

// 逆序排序 zRevRange
127.0.0.1:6379> ZREVRANGE set1 0 -1
1) "three"
2) "two"
3) "one"

// 逆序排序 zRevRangeByScore
127.0.0.1:6379> ZREVRANGEBYSCORE set1 +inf -inf
1) "three"
2) "two"
3) "one"

// 排序 + key
127.0.0.1:6379> ZRANGEBYSCORE set1 -inf +inf withscores
1) "one"
2) "1"
3) "two"
4) "2"
5) "three"
6) "3"

// 删除一个value
127.0.0.1:6379> zrem set1 one
(integer) 1

// 获取set内元素数量
127.0.0.1:6379> zcard set1
(integer) 2

127.0.0.1:6379> flushdb
OK
// 获取区间内值的数量
127.0.0.1:6379> zadd myset 1 1
(integer) 1
127.0.0.1:6379> zadd myset 1 2
(integer) 1
127.0.0.1:6379> zcount myset 1 2
(integer) 2

2.7. GEO 地理位置

底层为zest,可以用zset去操作geo
两级没法添加
建议使用单个实例部署,而不是集群
如果数据量过大,可以按国家省市地区进行拆分

// 经度 纬度 城市
127.0.0.1:6379> geoadd china:city 116.405285 39.904989 beijing
(integer) 1
127.0.0.1:6379> geoadd china:city 121.472644 31.231706 shanghai
(integer) 1
127.0.0.1:6379> geoadd china:city 106.50 29.53 chongqing
(integer) 1
127.0.0.1:6379> geoadd china:city 120.16922 30.24255 hangzhou
(integer) 1

// 打印位置
127.0.0.1:6379> GEOPOS china:city beijing shanghai
1) 1) "116.40528291463851929"
   2) "39.9049884229125027"
2) 1) "121.47264629602432251"
   2) "31.23170490709807012"

// 绝对距离 默认单位为km
127.0.0.1:6379> GEODIST china:city beijing shanghai
"1067597.9668"
127.0.0.1:6379> GEODIST china:city beijing shanghai km
"1067.5980"

// 以给定点为中心,找出半径内全部的点
127.0.0.1:6379> GEORADIUS china:city 110 30 500 km
1) "chongqing"
127.0.0.1:6379> GEORADIUS china:city 110 30 1000000 km
1) "chongqing"
2) "hangzhou"
3) "shanghai"
4) "beijing"

// 以给定国家为中心,找出半径内全部的点
127.0.0.1:6379> GEORADIUSBYMEMBER china:city beijing 1400 km
1) "hangzhou"
2) "shanghai"
3) "beijing"

// 将指定国家经纬度hash为11位的字符串
127.0.0.1:6379> GEOHASH china:city beijing shanghai
1) "wx4g0b7xrt0"
2) "wtw3sjt9vg0"

// 用zset去操作
127.0.0.1:6379> ZRANGE china:city 0 -1
1) "chongqing"
2) "hangzhou"
3) "shanghai"
4) "beijing"
127.0.0.1:6379> ZREM china:city shanghai
(integer) 1

2.8. Hyperloglog

计算一个元组里不重复元素的个数,相比set省大量空间

适用场景:计算UV(user view),将uuid去重计算数量

优点:占用内存是固定的,数据量小的时候,使用稀疏矩阵,数据量大会换成稠密矩阵,12KB可以统计2^64个元素,有0.81%的错误率。

原理:太复杂了orz,简单来说就是低位连续零位的最大长度K和随机数数量N的对数之间存在线性关系,为了提升精度,采用多个BitKeeper进行加权估计,分配桶进行调和平均。因为HyperLogLog使用了16384个桶,每个桶的maxbits用了6个bit来存储,maxbits-63,总内存位为2^14*6/8=12KB

127.0.0.1:6379> pfadd key a b c d e a b c
(integer) 1
127.0.0.1:6379> PFCOUNT key
(integer) 5

127.0.0.1:6379> pfadd key2 1 2 3 4 5 a b c
(integer) 1
127.0.0.1:6379> pfcount key2
(integer) 8
// 拼接
127.0.0.1:6379> PFMERGE key key2
OK
127.0.0.1:6379> PFCOUNT key
(integer) 10

2.9. Bitmaps

位存储,用一位的信息可以存储一天是否签到

// 用bitmap记录打卡
127.0.0.1:6379> SETBIT sign 0 1
(integer) 0
127.0.0.1:6379> SETBIT sign 1 0
(integer) 0
127.0.0.1:6379> SETBIT sign 2 0
(integer) 0
127.0.0.1:6379> SETBIT sign 3 1
(integer) 0

127.0.0.1:6379> GETBIT sign 3
(integer) 1

// 打卡天数
127.0.0.1:6379> BITCOUNT sign
(integer) 2

2.10. rax

有序字典树,和zset的区别是,rax是按key排序,而zset是按score排序。
可以将公安局的居民档案看成一棵rax树,key为身份证号,通过身份证号的前几位可以快速过滤某个地区的人;
也可以将router堪称一棵rax树,过滤url。

2.11. Stream

下面消息队列有详细讲

2.12. 事务

redis单条命令保证原子性,事务整体不保证原子性,没有隔离级别
很适合与管道搭配使用

// 执行事务
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> get k2
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) OK
3) "v2"

// 取消事务
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> discard
OK

编译型异常,也就是词法分析、语法分析、语义分析等阶段错了,所有命令都不会执行
运行时异常,也就是在执行时,发现了1/0等错误,其他正确命令会执行
不设置回滚是为了保证效率

127.0.0.1:6379> watch money
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> decrby money 10
QUEUED
127.0.0.1:6379(TX)> increby out 10
(error) ERR unknown command `increby`, with args beginning with: `out`, `10`, 
127.0.0.1:6379(TX)> incrby out 10
QUEUED
127.0.0.1:6379(TX)> exec

watch充当乐观锁,监视变量,不存在ABA问题
线程1
127.0.0.1:6379> set money 199
OK
127.0.0.1:6379> watch money
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> decrby money 10
QUEUED

线程2
127.0.0.1:6379> set money 199
OK

线程1
127.0.0.1:6379(TX)> exec
(nil)

用jedis实现

import com.alibaba.fastjson.JSONObject;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;

public class Test {
    public static void main(String[] args) {
        final Jedis jedis = new Jedis("127.0.0.1", 6379);
        jedis.flushAll();
        final Transaction multi = jedis.multi();
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("name","hzh");
        jsonObject.put("age","21");

        try {
            multi.set("User1",jsonObject.toJSONString());
            multi.exec();
        }catch (Exception e) {
            multi.discard();
        }
        System.out.println(jedis.get("User1"));
        jedis.close();
    }
}

配置文件

  1. 单位大小写不区分
# units are case insensitive so 1GB 1Gb 1gB are all the same.
  1. 可以使用include去导入多个配置文件
# include /path/to/local.conf
# include /path/to/other.conf
  1. 网络
    绑定的ip bind 127.0.0.1 -::1
    保护模式 protected-mode yes
    端口号 port 6379
  2. 通用general

默认为守护进程 daemonize yes

如果以后台方式运行,需要指定pid进程文件 pidfile /var/run/redis_6379.pid

日志 :四个级别

# Specify the server verbosity level.
# This can be one of:
# debug (a lot of information, useful for development/testing)
# verbose (many rarely useful info, but not a mess like the debug level)
# notice (moderately verbose, what you want in production probably)
# warning (only very important / critical messages are logged)

日志的文件位置名:logfile “”

数据库数量:database 16

logo显示:always-show-logo yes

快照:
持久化:多少秒内进行多少次修改的话,进行持久化
save 3600 1
save 300 100
save 60 10000

持久化失败是否继续工作:
stop-writes-on-bgsave-error yes

是否压缩rdb文件:
rdbcompression yes

保存rdb文件的时候,进行校验检查:
rdbchecksum yes

rdb文件保存位置:
dir ./

主从复制:REPLICATION

安全:Security
设置密码: requirepass “abcdef” 或者用命令 config set requirepass “abcdef”
登陆命令:auth “abcdef”

客户端:Client
最大客户端数量:maxclients 10000
最大内存容量:maxmemory
内存上限后处理策略:maxmemory-policy noeviction 6种算法

AOF配置:
默认不开启,使用RDB:appendonly no
文件名:“appendonly.aof”
每秒同步一下[默认]:appendfsync everysec
每次修改都会同步:appendfsync always
不同步,操作系统来同步,速度最快:appendfsync no

三. springboot整合Redis

3.1. 导入SpringData的redis包

org.springframework.boot spring-boot-starter-data-redis 2.5.3

3.2. yml配置一下

# redis 配置
  redis:
    # 地址
    host: localhost
    # 端口,默认为6379
    port: 6379
    # 数据库索引
    database: 0
    # 密码
    password: 
    # 连接超时时间
    timeout: 10s
    lettuce:
      pool:
        # 连接池中的最小空闲连接
        min-idle: 0
        # 连接池中的最大空闲连接
        max-idle: 8
        # 连接池的最大数据库连接数
        max-active: 8
        # #连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: -1ms

3.3. 引入自动装配或者重写过的的redisTemplate

这里以若依的配置为例子

package com.ruoyi.framework.config;

import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;

/**
 * redis配置
 * 
 * @author ruoyi
 */
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport
{
    @Bean
    @SuppressWarnings(value = { "unchecked", "rawtypes" })
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
    {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);

        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        serializer.setObjectMapper(mapper);

        // 使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);

        // Hash的key也采用StringRedisSerializer的序列化方式
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);

        template.afterPropertiesSet();
        return template;
    }
}

数据类型:
字符串:redisTemplate.opsForValue()
链表:redisTemplate.opsForList() 等所有类型都有

3.4. 在redisTemplate中封装基本操作

package com.ruoyi.common.core.redis;

import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;

/**
 * spring redis 工具类
 *
 * @author ruoyi
 **/
@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
public class RedisCache
{
    @Autowired
    public RedisTemplate redisTemplate;

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key 缓存的键值
     * @param value 缓存的值
     */
    public <T> void setCacheObject(final String key, final T value)
    {
        redisTemplate.opsForValue().set(key, value);
    }

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key 缓存的键值
     * @param value 缓存的值
     * @param timeout 时间
     * @param timeUnit 时间颗粒度
     */
    public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
    {
        redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
    }

    /**
     * 设置有效时间
     *
     * @param key Redis键
     * @param timeout 超时时间
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout)
    {
        return expire(key, timeout, TimeUnit.SECONDS);
    }

    /**
     * 设置有效时间
     *
     * @param key Redis键
     * @param timeout 超时时间
     * @param unit 时间单位
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout, final TimeUnit unit)
    {
        return redisTemplate.expire(key, timeout, unit);
    }

    /**
     * 获得缓存的基本对象。
     *
     * @param key 缓存键值
     * @return 缓存键值对应的数据
     */
    public <T> T getCacheObject(final String key)
    {
        ValueOperations<String, T> operation = redisTemplate.opsForValue();
        return operation.get(key);
    }

    /**
     * 删除单个对象
     *
     * @param key
     */
    public boolean deleteObject(final String key)
    {
        return redisTemplate.delete(key);
    }

    /**
     * 删除集合对象
     *
     * @param collection 多个对象
     * @return
     */
    public long deleteObject(final Collection collection)
    {
        return redisTemplate.delete(collection);
    }

    /**
     * 缓存List数据
     *
     * @param key 缓存的键值
     * @param dataList 待缓存的List数据
     * @return 缓存的对象
     */
    public <T> long setCacheList(final String key, final List<T> dataList)
    {
        Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
        return count == null ? 0 : count;
    }

    /**
     * 获得缓存的list对象
     *
     * @param key 缓存的键值
     * @return 缓存键值对应的数据
     */
    public <T> List<T> getCacheList(final String key)
    {
        return redisTemplate.opsForList().range(key, 0, -1);
    }

    /**
     * 缓存Set
     *
     * @param key 缓存键值
     * @param dataSet 缓存的数据
     * @return 缓存数据的对象
     */
    public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet)
    {
        BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
        Iterator<T> it = dataSet.iterator();
        while (it.hasNext())
        {
            setOperation.add(it.next());
        }
        return setOperation;
    }

    /**
     * 获得缓存的set
     *
     * @param key
     * @return
     */
    public <T> Set<T> getCacheSet(final String key)
    {
        return redisTemplate.opsForSet().members(key);
    }

    /**
     * 缓存Map
     *
     * @param key
     * @param dataMap
     */
    public <T> void setCacheMap(final String key, final Map<String, T> dataMap)
    {
        if (dataMap != null) {
            redisTemplate.opsForHash().putAll(key, dataMap);
        }
    }

    /**
     * 获得缓存的Map
     *
     * @param key
     * @return
     */
    public <T> Map<String, T> getCacheMap(final String key)
    {
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * 往Hash中存入数据
     *
     * @param key Redis键
     * @param hKey Hash键
     * @param value 值
     */
    public <T> void setCacheMapValue(final String key, final String hKey, final T value)
    {
        redisTemplate.opsForHash().put(key, hKey, value);
    }

    /**
     * 获取Hash中的数据
     *
     * @param key Redis键
     * @param hKey Hash键
     * @return Hash中的对象
     */
    public <T> T getCacheMapValue(final String key, final String hKey)
    {
        HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
        return opsForHash.get(key, hKey);
    }

    /**
     * 获取多个Hash中的数据
     *
     * @param key Redis键
     * @param hKeys Hash键集合
     * @return Hash对象集合
     */
    public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys)
    {
        return redisTemplate.opsForHash().multiGet(key, hKeys);
    }

    /**
     * 获得缓存的基本对象列表
     *
     * @param pattern 字符串前缀
     * @return 对象列表
     */
    public Collection<String> keys(final String pattern)
    {
        return redisTemplate.keys(pattern);
    }
}

四. RDB

RedisDataBase
在指定的时间间隔内,将内存中的数据集快照写入磁盘,也就是snapshot快照,恢复的时候,内存读入文件。
是全量备份,是内存数据的二进制序列化形式,存储上非常紧凑,
除了配置文件内配置规则,也可以通过flushall,save命令或退出redis来保存快照dump.rdb,
使用操作系统的多进程COW(copy on write)机制来实现快照持久化,
也就是持久化的时候,会调用glibc的函数fork一个子进程,快照持久化交给子进程来处理,父进程继续处理客户端请求,
父子进程共享代码段和数据段,子进程不会修改现有内存数据结构,父进程修改数据结构时,会将被共享的页面复制一份出来修改

五. AOF

AppendOnlyFile 默认不开启
将所有操作记录,在长期运行过程中,会变得无比庞大,需要定期使用bgrewriteaof指令重写瘦身
fsync可以将文件的内容强制从内核缓存刷入磁盘;是一个磁盘IO操作,很慢;可以防止机器宕机时,日志的丢失。

补充:
Redis4.0新特性:混合持久化
将rdb文件的内容和 增量 AOF放在一起,重启的时候,先加载rdb再重放aof,就可以替代aof全量重放,提升效率。

六. 分布式锁

推荐阅读:
https://www.zhihu.com/question/452803310/answer/1931377239

为了解决分布式应用的并发问题,即多个用户同时修改同一用户状态的时候,读取用户的数据,修改数据,再持久化数据,这个过程并不是原子性的,所以需要上锁。

这里主要使用到setnx这个命令,setnx一个key,如果成功了代表抢到锁了,没成功就代表没有抢到,持久化完成后,调用del指令释放锁。

为了解决del指令执行前,程序出现问题导致没有释放锁,需要设置一个锁的过期时间放置死锁。但setnx和expire不是一条原子指令,为了解决问题,产生了很多分布式锁的library,但后来这些都被淘汰了,作者直接新增了一个合并二者操作的指令:

set lock:hzh true ex 5 nx
设置分布式锁,将setnx与ex两操作合并,避免了死锁。

del lock:hzh
手动删除锁

当然也可以用Lua脚本去让两条指令具有原子性来解决这个问题。

但问题并没有结束,redis分布式锁没法解决超时问题,即在锁的有效时间里,并未完成所有操作,所以尽量不要用于长时间的任务;也可以通过开启一个守护进程,定时去检测这个锁的失效时间,如果锁快过期了,但操作共享资源还未完成,就对锁进行续期。java可以使用redisson这个sdk客户端来完成。

除此之外,还可能出现锁被其他线程给释放了这种问题,可以给set指令设置value,这个value可以为uuid,最后释放锁的时候,检验一下value的值,当然这两步操作也得合并成lua脚本。

在集群环境下,分布式锁是不安全的。比如主节点申请了一把锁,但还没同步到从节点,主节点就挂掉,从节点取而代之,但从节点内部没有这个锁,有客户端过来申请锁,就同意了,这样会导致一把锁被两个客户端同时持有,不安全。不过这种不安全也仅在failover才会产生,而是时间极短,可以容忍。

作者提出了RedLock算法去解决这个问题,核心思想和大多数分布式算法一样,也使用“大多数机制”。想要使用redlock,至少要部署5个redis实例,只要大部分节点set成功,即视为加锁成功。

需要注意的是,redlock不具备高可用性,并且需要更多的运维成本。
在这里插入图片描述

七. 异步消息队列

只适用于对消息可靠性要求不高,一组生产者对应一组消费者的简单场合,复杂场合适合用专业的RabbitMQ、RocketMQ等。

可以使用blpop和brpop实现阻塞出入队列,不过要注意如果阻塞太久,会主动断开连接,要在异常处理里面重试。

延时队列的话可以用zset来实现,将消息序列化一个字符串作为zset的value,到期时间设置为score,然后用多个线程去轮询。

可以用publish和subscribe去实现多消费者消费同一组数据,一个消费者也可以订阅多个频道的数据,但还是存在数据丢失的可能。
缓冲区的默认配置:client-output-buffer-limit pubsub 32mb 8mb 60。
它的参数含义如下:32mb:缓冲区一旦超过 32MB,Redis 直接强制把消费者踢下线8mb + 60:缓冲区超过 8MB,并且持续 60 秒,Redis 也会把消费者踢下线。
List 中的数据可以一直积压在内存中,消费者什么时候来「拉」都可以。但 Pub/Sub 是把消息先「推」到消费者在 Redis Server 上的缓冲区中,然后等消费者再来取。

可以考虑使用最新的数据类型Stream,它弥补了上面的诸多缺陷。

  • 使用了消息ID,来避免消费者异常当机导致的消息丢失。
  • 写入RDB 和 AOF做持久化,来避免Redis当机导致的消息丢失。
  • 设定了队列的最大长度,避免消息堆积过多。(仍然会丢失掉超过最大长度的消息)

在这里插入图片描述

这个写的挺全挺好的
https://www.zhihu.com/question/43688764

八. 布隆过滤器

一个有误差的set结构,主要用作判断一个值是否存在于某一个set。当布隆过滤器说不存在,那一定不存在,但它说存在,却不一定存在。很适合推送用户没看过的新信息,因为即使少推送了几条新信息,也没事。

布隆过滤器的原理是,当一个元素被加入集合时,通过K个散列哈希函数将这个元素映射成一个位数组中的K个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在。这就是布隆过滤器的基本思想。

Bloom Filter跟单哈希函数Bit-Map不同之处在于:Bloom Filter使用了k个哈希函数,每个字符串跟k个bit对应。从而降低了冲突的概率。

Bloom Filter默认错误率为1%,可以通过设置key,error_rate,initial_size,通过增大空间,减少碰撞概率,从而减少错误率。
具体的公式如下:

  • hash函数的最佳数量 k=0.7*(1/ n)
  • 错误率 f=0.6185(1/n)
  • f=(1-0.5t)k
// 测试主要命令add和exists
127.0.0.1:6379> bf.add test user1
(integer) 1
127.0.0.1:6379> bf.add test user2
(integer) 1
127.0.0.1:6379> bf.add test user3
(integer) 1
127.0.0.1:6379> bf.add test user4
(integer) 1
127.0.0.1:6379> BF.EXISTS test user1
(integer) 1
127.0.0.1:6379> BF.EXISTS test user2
(integer) 1
127.0.0.1:6379> BF.EXISTS test user5
(integer) 0

// 批量操作
127.0.0.1:6379> BF.MADD test user2 user3 user5 user6
1) (integer) 0
2) (integer) 0
3) (integer) 1
4) (integer) 1
127.0.0.1:6379> BF.MEXISTS test user6 user7
1) (integer) 1
2) (integer) 0

应用场景:缓存穿透

有一种恶意请求,是通过查询大量的不存在用户,缓存查不到,会直接在数据库里去查,从而导致缓存甚至数据库被击穿,从而引起雪崩效应。

布隆过滤器可以直接过滤掉这些不存在的用户,如果布隆过滤器显示存在,那么再去数据库里复查。

九. 限流

9.1. 滑动窗口

用一个zset去记录用户的行为历史,一个用户的一种行为用一个zset,key为用户的行为历史,score为时间戳。然后每个行为到来的时候,去维护这个时间窗口,将时间窗口外的记录清理掉。可以使用pipeline去提升存取效率。
不适合数据量大的场景。

管道pipeline是由客户端提供的技术,通过调整读写的顺序,将 写->读->写->读->写->读 循环 替换为 写->写->写->读->读->读 ,合并多次读和多次写,从而减少IO的次数。我们需要知道,写操作只要将数据写到发送缓冲中就可以返回了,写操作只会在缓冲区满的时候,才需要等待;而读操作是需要从接受缓冲中去拉取数据,只有当缓冲中没有数据,才会等待。一般情况下,写不怎么耗时,读才是需要耗时的操作。

9.2. 漏斗限流

Redis4.0提供了一个限流Redis模块,提供了原子的限流指令。可以设置漏斗的速率以及初始容量等参数。
java可以使用lettuce定义扩展命令实现这些限流方式

十. 集群

10.1. 主从同步

Redis的主从同步采用异步复制,保证最终一致性和可用性,从节点会努力追赶主节点。
当主节点挂掉后,从节点可能没有接受完全部数据,导致数据丢失。

增量同步:主节点会将自己修改自己状态的指令记录在本地内存buffer中,然后异步将buffer同步到从节点,从节点一边同步指令,一边反馈给主节点同步到哪里了(偏移量)。这个buffer是一个环形数组,如果内容过多,头部的旧元素会被覆盖。如果发生这种情况,需要快照同步。

快照同步:十分消耗资源的操作,在主节点进行一次bgsave,然后将快照发送给从节点,从节点清空全部数据后加载快照,加载完成后,进行增量同步。从节点刚加入时,必须进行快照同步。需要注意的是,如果buffer太小的话,会陷入这两个同步的死循环。在2.8.18版本以后,Redis支持无盘复制,也就是通过套接字发送快照内容到从节点。

10.2. 哨兵Sentinel

一个高可用方案,可以在故障发生时,自动进行主从切换。可以建立一个哨兵集群,哨兵之间也互相监督。可以将Sentinel集群理解为zookeeper集群,是集群高可用的心脏,一般由3~5个节点组成。
Sentinel可以以牺牲可用性为代价,减少数据的丢失。可以在配置文件里设置,主节点中最少有多少个节点在正常复制,低于这个数,就停止对外写服务。

10.3. Codis

Redis集群方案之一,是一个代理中间件,可以将指令转发给Redis实例,再将返回结果转发给客户端。可以根据集群空间大小,动态增加Redis实例实现动态扩容。支持多个Codis代理增加QPS并起到容灾的功能。但不能支持多个Redis的事务,rename也十分危险。

10.4. Cluster

Redis官方的集群方案,是去中心化的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值