一. Redis简介
1.1. Redis是什么
Remote Dictionary Server,即远程字典服务。
是c语言编写的key-value数据库,提供多种语言API。
Redis会周期性把更新的数据写入磁盘或把修改操作写入追加的记录文件,并且在此基础上是实现了master-slave主从同步。
1.2. Redis能干什么?
- 内存存储、持久化(RDB & AOF)
- 高速缓存
- 发布订阅系统(简单消息队列)
- 地图信息分析
- 计时器、计数器
- 分布式锁
- 排序
- …
1.3. Redis 特性
- 数据类型多
- 持久化
- 集群 哨兵
- 事务
- Lua脚本
- …
二. 命令以及数据结构
参考阅读材料:
- https://github.com/huangz1990/redis-3.0-annotated redis3.0源码
- redis6.25源码
书籍: - 《Redis深度历险》
- 《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();
}
}
配置文件
- 单位大小写不区分
# units are case insensitive so 1GB 1Gb 1gB are all the same.
- 可以使用include去导入多个配置文件
# include /path/to/local.conf
# include /path/to/other.conf
- 网络
绑定的ip bind 127.0.0.1 -::1
保护模式 protected-mode yes
端口号 port 6379 - 通用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.33.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官方的集群方案,是去中心化的。