redis基础
Redis基础
简介:Redis是一个开源的key-value存储系统。
特点:
- 数据都在内存中,支持持久化,主要用作备份恢复
- 除了支持简单的key-value模式,还支持多种数据结构的存储,比如 list、set、hash、zset等。
- 一般是作为缓存数据库辅助持久化的数据库
安装
去官方下载安装包
上传安装包到/opt下
在linux下安装gcc环境:
yum install -y gcc tcl
解压 tar -zxvf redis-6.2.1.tar.gz
进入redis目录执行make命令
执行make install 安装
安装目录在/usr/local/bin下
在安装包的目录下备份配置文件
cp redis.conf redis.conf.bck
修改配置文件(最好不要直接修改原文件,复制到/etc下运行时用复制后的)
vim redis.conf
修改如下配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16# 允许访问的地址,默认是127.0.0.1,会导致只能在本地访问。修改为0.0.0.0则可以在任意IP访问,生产环境不要设置为0.0.0.0
bind 0.0.0.0
# 守护进程,修改为yes后即可后台运行logs
daemonize yes
# 密码,设置后访问Redis必须输入密码
requirepass 123321
# 监听的端口
port 6379
# 工作目录,默认是当前目录,也就是运行redis-server时的命令,日志.持久化等文件会保存在这个目录
dir .
# 数据库数量,设置为1,代表只使用1个库,默认有16个库,编号0~15
databases 1
# 设置redis能够使用的最大内存
maxmemory 512mb
# 日志文件,默认为空,不记录日志,可以指定日志文件名
logfile "redis.log"执行redis-server redis.conf 进行后台运行(redis-server是直接前台运行)
设置开机自启动
新建一个系统服务文件:
vi/etc/systemd/system/redis.service
写入内容
1
2
3
4
5
6
7
8
9
10
11[Unit]
Description=redis-server
After=network.target
[Service]
Type=forking
ExecStart=/usr/local/bin/redis-server /opt/redis-6.2.1/redis.conf
PrivateTmp=true
[Install]
WantedBy=multi-user.target重载系统服务:
systemctl daemon-reload
可以使用的命令:
1
2
3
4
5
6
7
8# 启动
systemctl start redis
# 停止
systemctl stop redis
# 重启
systemctl restart redis
# 查看状态
systemctl status redis让redis开机自启动:
systemctl enable redis
连接redis
使用redis-cli
Redis安装完成后就自带了命令行客户端:redis-cli,使用方式如下:
1 | redis-cli [options] [commonds] |
其中常见的options有:
-h 127.0.0.1
:指定要连接的redis节点的IP地址,默认是127.0.0.1-p 6379
:指定要连接的redis节点的端口,默认是6379-a 123321
:指定redis的访问密码
其中的commonds就是Redis的操作命令,例如:
ping
:与redis服务端做心跳测试,服务端正常会返回pong
使用图形化界面
这个仓库可以找到安装包:https://github.com/lework/RedisDesktopManager-Windows/releases
下载后一直next即可
Key键的通用操作
- keys *查看当前库所有key (匹配:keys *1,不建议在生产环境设备上使用)
- exists key判断某个key是否存在
- type key 查看你的key是什么类型
- del key 删除指定的key数据
- unlink key 根据value选择非阻塞删除(仅将keys从keyspace元数据中删除,真正的删除会在后续异步操作。)
- expire key 10 10秒钟:为给定的key设置过期时间
- ttl key 查看还有多少秒过期,-1表示永不过期,-2表示已过期
- select命令切换数据库
- dbsize查看当前数据库的key的数量
- flushdb清空当前库
- flushall通杀全部库
key的结构
很多时候为了避免key重复,可以将key设计成 项目名:业务名:类型:id
的形式
如:
KEY | VALUE |
---|---|
project:user:1 | {“id” :1,”name” : “Jack” ,”age”: 21} |
project:product:1 | { “id” :1,”name” :”小米11”,”price”: 4999} |
Redis数据库键空间
Redis是一个键值对( key-value pair)数据库服务器,服务器中的每个数据库都由一个redis.h/redisDb结构表示,其中,redisDb结构的dict字典保存了数据库中的所有键值对,我们将这个字典称为键空间( key space ) 。
为了实现从键到值的快速访问,Redis 键空间使用了一个哈希表来保存所有键值对(称为全局哈希表):
哈希冲突问题:
当你往哈希表中写入更多数据时,哈希冲突是不可避免的问题。这里的哈希冲突,也就是指,两个 key 的哈希值和哈希桶计算对应关系时,正好落在了同一个哈希桶中。
Redis为了解决hash冲突问题,Redis 会对哈希表做 rehash 操作。rehash 也就是增加现有的哈希桶数量,让逐渐增多的 entry 元素能在更多的桶之间分散保存,减少单个桶中的元素数量,从而减少单个桶中的冲突。
Redis 开始执行 rehash看,Redis 默认使用了两个全局哈希表:哈希表 1 和哈希表 2,这个过程分为三步:
- 给哈希表 2 分配更大的空间,例如是当前哈希表 1 大小的两倍;
- 把哈希表 1 中的数据重新映射并拷贝到哈希表 2 中;
- 释放哈希表 1 的空间。
渐进式 rehash
由于如果在拷贝步骤涉及大量数据,会造成Redis线程阻塞。
简单来说就是在第二步拷贝数据时,Redis 仍然正常处理客户端请求,每处理一个请求时,从哈希表 1 中的第一个索引位置开始,顺带着将这个索引位置上的所有 entries 拷贝到哈希表 2 中;等处理下一个请求时,再顺带拷贝哈希表 1 中的下一个索引位置的 entries。
这样就巧妙地把一次性大量拷贝的开销,分摊到了多次处理请求的过程中,避免了耗时操作,保证了数据的快速访问。
常用数据类型
String字符串
常见命令
set
添加键值对 get
查询对应键值 append
将给定的 追加到原值的末尾 strlen
获得值的长度 setnx
只有在 key 不存在时 设置 key 的值 incr
将 key 中储存的数字值增1只能对数字值操作,如果为空,新增值为1 decr
将 key 中储存的数字值减1只能对数字值操作,如果为空,新增值为-1 incrby / decrby
<步长>:将 key 中储存的数字值增减。自定义步长。 incrbyfloat/decrbyfloat
步长:将 key 中储存的浮点数增减。自定义步长。 mset
….. 同时设置一个或多个 key-value对 mget
…..同时获取一个或多个 value msetnx
…同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在。 getrange
<起始位置><结束位置> 获得值的范围,类似java中的substring,前包,后包
setrange
<起始位置> 用 覆写 所储存的字符串值,从<起始位置>开始(索引从0开始)。 setex
<**过期时间**> 设置键值的同时,设置过期时间,单位秒。 getset
以新换旧,设置了新值同时获得旧值。
底层结构
String的数据结构为**简单动态字符串(Simple Dynamic String,缩写SDS)**。
简单动态字符串( simple dynamic string,SDS):
SDS的定义:
1
2
3
4
5
6
7
8struct sdshdr {
//记录buf数组中已使用字节的数量等于sDs所保存字符串的长度
int len;
//记录buf数组中未使用字节的数量
int free;
//字节数组,用于保存字符串
char buf[];
};特点:
- 常数复杂度获取字符串长度
- 杜绝缓冲区溢出,能够进行动态扩展
- 空间预分配((当字符串长度小于1M时,扩容都是加倍现有的空间,如果超过1M,扩容时一次只会多扩1M的空间。需要注意的是字符串最大长度为512M。)、惰性空间释放
- 二进制安全,因为使用字节数组保存数据
List列表
Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。
常见命令
lpush/rpush
…. 从左边/右边插入一个或多个值。 lpop/rpop
从左边/右边吐出一个值。值在键在,值光键亡。 rpoplpush
从 列表右边吐出一个值,插到 列表左边。 lrange
按照索引下标获得元素(从左到右) lrange mylist 0 -1 0左边第一个,-1右边第一个,(0-1表示获取所有)
lindex
按照索引下标获得元素(从左到右) llen
获得列表长度 linsert
before 在 的后面插入 插入值 lrem
从左边删除n个value(从左到右) lset
将列表key下标为index的值替换成value
底层结构
其底层有ZipList和QuickList两种存储方式。
- 首先在列表元素较少的情况下会使用一块连续的内存存储,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,这个结构是ZipList,也即是压缩列表。
- 当数据量比较多的时候才会改成QuickList双向链表。既满足了快速的插入删除性能,又不会出现太大的空间冗余。
ZipList压缩列表:
压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型( sequential )数据结构。一个压缩列表可以包含任意多个节点( entry ),每个节点可以保存一个字节数组或者一个整数值。
结构:
zlbytes:表示列表总字节数
zltail:列表尾的偏移量
zllen:列表中的 entry 个数
entry:
previous_entry_length:记录了压缩列表中前一个节点的长度
encoding:记录了节点的content属性所保存数据的类型以及长度
content:数据内容
zlend:表示列表结束
特点:
- 要查找定位第一个元素和最后一个元素O(1),查找其他元素O(N)。
- 节约空间。
- 有可能会因为entry内容长度的问题,引发一系列的连锁更新,但很少发生。
QuickList双向链表
QuickList数据结构是Redis在3.2版本引入的一种新得数据结构,他是一个双端链表,链表中的每个节点都是一个 ZipList
结构:
node结构:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15typedf struct quicklistNode{
//前一个节点
quicklistNode* prev;
//后一个节点
quicklistNode* next;
//压缩列表
ziplist* zl;
//ziplist大小
int32 size;
//ziplist 中元素数量
int16 count;
//编码形式 存储 ziplist 还是进行LZF算法压缩储存的zipList
int2 encoding;
...
}quickListNode整个链表:
1
2
3
4
5
6
7
8
9
10
11
12
13typedf struct quicklist{
//指向头结点
quicklistNode* head;
//指向尾节点
quicklistNode* tail;
//元素总数
long count;
//quicklistNode节点的个数
int nodes;
//压缩算法深度
int compressDepth;
...
}quickList
特点:
- 比传统链表更省空间,所以能存储大量数据。
- 还能对数据进行压缩
Set集合
Redis的Set是string类型的无序集合。它底层其实是一个value为null的hash表,所以添加,删除,查找的**复杂度都是O(1)**。并且可以自动的去重。
常见命令
sadd
….. 将一个或多个 member 元素加入到集合 key 中,已经存在的 member 元素将被忽略 smembers
取出该集合的所有值。 sismember
判断集合 是否为含有该 值,有1,没有0 scard
返回该集合的元素个数。 srem
…. 删除集合中的某个元素。 spop
随机从该集合中吐出一个值。 srandmember
随机从该集合中取出n个值。不会从集合中删除 。 smove
sinter
返回两个集合的交集元素。 sunion
返回两个集合的并集元素。 sdiff
返回两个集合的差集元素(key1中的,不包含key2中的)
底层结构
Set数据结构是dict字典(用哈希表实现的)和整数数组;
- 当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis就会使用整数集合作为集合键的底层实现。
- 其余情况就使用字典。
整数数组:
整数集合( intset)是集合键的底层实现之一。
结构:
1
2
3
4
5
6
7
8typedef struct intset {
//编码方式
uint32_t encoding;
//集合包含的元素数量
uint32_t length;
//保存元素的数组,里面按照从小到大的顺序排列,不会有重复项
int8_t contents[];
}intset;特点:
- 只存整数,保证了数据不重复。
- 并且会根据数据类型,进行升级降级。
字典:
字典,又称为符号表( symbol table)、关联数组( associative array)或映射(map),是一种用于保存键值对( key-value pair )的抽象数据结构。
结构:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24// 哈希表
typedef struct dictht {
//哈希表数组
dictEntry**table;
//哈希表大小
unsigned long size;
//哈希表大小掩码,用于计算索引值,总是等于size-1
unsigned long sizemask;
//该哈希表已有节点的数量
unsigned long used;
} dictht;
// 一个键值对Entry
typedef struct dictEntry {
//键
void *key;
//值
union {
void *val;
uint64_tu64;
int64_ts64;
} v;
//指向下个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;特点:
- 快速查找O(1)的复杂度。
- 不支持范围查找,只能全部遍历很慢。
Hash哈希
Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象。类似Java里面的Map<String,Object>
常见命令
- hset
给 集合中的 键赋值 - hget
从 集合 取出 value - hmset
… 批量设置hash的值 - hexists
查看哈希表 key 中,给定域 field 是否存在。 - hgetall
:列出该hash集合的所有field和value - hkeys
列出该hash集合的所有field - hvals
列出该hash集合的所有value - hincrby
为哈希表 key 中的域 field 的值加上增量 1 -1 - hsetnx
将哈希表 key 中的域 field 的值设置为 value ,当且仅当域 field 不存在 .
底层结构
Hash类型对应的数据结构是两种:Ziplist(压缩列表),Hashtable(哈希表)。当field-value长度较短且个数较少时,使用Ziplist,否则使用Hashtable。
Zset集合
Redis有序集合zset与普通集合set非常相似,是一个没有重复元素的字符串集合。
不同之处是有序集合的每个成员都关联了一个评分(score),这个评分(score)被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分可以是重复了 。
常见命令
zadd
…将一个或多个 member 元素及其 score 值加入到有序集 key 当中。 zrange
[WITHSCORES]返回有序集 key 中,下标在 之间的元素带WITHSCORES,可以让分数一起和值返回到结果集。 zrangebyscore key min max [withscores] [limit offset count]返回有序集 key 中,所有 score 值介于 min 和max 之间(包括等于 min 或 max )的成员。有序集成员按 score 值递增(从小到大)次序排列。
zrevrangebyscore key maxmin [withscores] [limit offset count] 同上,改为从大到小排列。
zincrby
为元素的score加上增量 zrem
删除该集合下,指定值的元素 zcount
统计该集合,分数区间内的元素个数 zrank
返回该值在集合中的排名,从0开始。
底层结构
zset底层使用了两个数据结构:
如果一个有序集合包含的元素数量比较多,又或者有序集合中元素的成员( member)是比较长的字符串时,Redis就会使用跳跃表+哈希表来作为有序集合键的底层实现,这样能保证各方面的性能。
1
2
3
4typedef struct zset {
dict *dict; // 字典,保存了从成员到分值的映射关系;
zskiplist *zsl; // 跳跃表,按分值由小到大保存所有集合元素;
} zset;- 跳跃表的目的在于给元素value排序,根据score的范围获取元素列表。
- 哈希表的作用就是关联元素value和权重score,保障元素value的唯一性,可以通过元素value找到相应的score值。
其余使用ziplist压缩列表:每个 value/score 元素紧凑排列存在entry中,节省内存(会根据score进行排序,类似于数组排序:先遍历找位置,再后移节点腾出空间,所以只适用于小规模场景)
跳跃表:
跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。
结构:
- header:指向跳跃表的表头节点。
- tail:指向跳跃表的表尾节点。
- level:记录目前跳跃表内,层数最大的那个节点的层数(表头节点的层数不计算在内)。
- length:记录跳跃表的长度,也即是,跳跃表目前包含节点的数量(表头节点不计算在内)。
zskiplistNode跳跃表的节点:
- 层指针(level):每个层都带有两个属性:前进指针和跨度。
- 前进指针用于访问位于表尾方向的其他节点。
- 跨度则记录了前进指针所指向节点和当前节点的距离。
- 后退指针(backward):节点中用BW字样标记节点的后退指针,它指向位于当前节点的前一个节点。后退指针在程序从表尾向表头遍历时使用。
- 分值(score):在跳跃表中,节点按各自所保存的分值从小到大排列。
- 成员对象(obj):各个节点中的o1、o2和o3是节点所保存的成员对象。
特点:
- 由于维护了多级索引,查找时间平均为O(logN)。
- 可以通过顺序性操作来批量处理节点。
跳跃表的排序是通过多层链表结构和随机跳跃算法来实现的。它不需要像平衡树那样保持完美平衡,因此在插入、删除和查找元素时可以获得更高的性能。(当插入一个新节点时,Redis 会从最高层链表开始,根据一个随机算法,以一定概率向下跳跃到下一层链表继续查找。这个随机算法的概率分布是一个指数分布,即跳跃到下一层链表的概率会随着层数的增加而逐渐减小。这样就可以在保证数据有序的前提下,尽可能地减少查找、插入和删除元素的时间复杂度。)
Redis的发布和订阅
Redis 发布订阅 (pub/sub) 是一种消息通信模式:发送者 (pub) 发送消息,订阅者 (sub) 接收消息。并且可以根据频道来划分(支持任意数量的频道)
实现
客户端订阅频道进行监听
SUBSCRIBE channel1
发布者发布消息到指定频道
publish channel1 hello
客户端自动收到发布的消息
Redis6新数据类型
Bitmaps
(1) Bitmaps本身不是一种数据类型, 实际上它就是字符串(key-value) , 但是它可以对字符串的位进行操作。
(2) Bitmaps单独提供了一套命令, 所以在Redis中使用Bitmaps和使用字符串的方法不太相同。 可以把Bitmaps想象成一个以位为单位的数组, 数组的每个单元只能存储0和1, 数组的下标在Bitmaps中叫做偏移量。
常见命令
setbit
设置Bitmaps中某个偏移量的值(0或1)*offset:偏移量从0开始 getbit
获取Bitmaps中某个偏移量的值 bitcount
[start end] 统计字符串从start字节到end字节比特值为1的数量 bitop and(or/not/xor)
[key…] bitop是一个复合操作, 它可以做多个Bitmaps的and(交集) 、 or(并集) 、 not(非) 、 xor(异或) 操作并将结果保存在destkey中。
HyperLogLog
Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。
在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。
常见命令
pfadd
< element> [element …] 添加指定元素到 HyperLogLog 中 pfcount
[key …] 计算HLL的近似基数,可以计算多个HLL,比如用HLL存储每天的UV,计算一周的UV可以使用7天的UV合并计算即可 pfmerge
[sourcekey …] 将一个或多个HLL合并后的结果存储在另一个HLL中,比如每月活跃用户可以使用每天的活跃用户来合并计算可得
Geospatia
Redis 3.2 中增加了对GEO类型的支持。GEO,Geographic,地理信息的缩写。该类型,就是元素的2维坐标,在地图上就是经纬度。redis基于该类型,提供了经纬度设置,查询,范围查询,距离查询,经纬度Hash等常见操作。
常见命令
- geoadd
< longitude> [longitude latitude member…] 添加地理位置(经度,纬度,名称) - geopos
[member…] 获得指定地区的坐标值 - geodist
[m|km|ft|mi ] 获取两个位置之间的直线距离 - georadius
< longitude> radius m|km|ft|mi 以给定的经纬度为中心,找出某一半径内的元素
更多命令见Redis命令参考
Jedis
连接步骤
引入依赖:
1
2
3
4
5<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.2.0</version>
</dependency>注意需要将redis.conf中的protected-mode配置调成no(设置了密码不用)、以及bind ip的配置给注释掉
并且关闭linux防火墙:systemctl stop firewalld.service
创建jedis客户端:
1
2
3
4
5
6
7
8public static void main(String[] args) {
//建立连接
Jedis jedis = new Jedis("192.168.227.130", 6379);
//设置密码
jedis.auth("123456");
//选择库
jedis.select(0);
}进行操作如:
jedis.set(key,value)
跟redis命令一致
相关基本操作
大多与redis原本操作一致
Key通用的操作
- 获取获取key值:jedis.keys(“*”)
- 判断是否存在该key:jedis.exists(“k1”)
- 判断超时时间:jedis.ttl(“k1”)
- 获取key的值:jedis.get(“k1”)
String类型的操作
- 设置值:jedis.set(“k1”, “v1”)
- 多次设置值:jedis.mset(“str1”,”v1”,”str2”,”v2”,”str3”,”v3”)
- 获取多个值:jedis.mget(“str1”,”str2”,”str3”)
List类型的操作
- 插入jedis.lpush(“k1”,”22”,”333”);
- 获取所有值:jedis.lrange(“mylist”,0,-1);
Set类型的操作
- 插入值:jedis.sadd(“orders”, “order01”);
- 获取值:jedis.smembers(“orders”);
- 删除值:jedis.srem(“orders”, “order02”);
Hash类型的操作
- 插入值:jedis.hset(“hash1”,”userName”,”lisi”);
- 获取值:jedis.hget(“hash1”,”userName”);
- 批量插入:jedis.hmset(“hash2”,map);
- 批量获取:jedis.hmget(“hash2”, “telphone”,”email”);
Zset类型的操作
- 插入值:jedis.zadd(“zset01”, 100d, “z3”);
- 获取值:jedis.zrange(“zset01”, 0, -1);
Jedis连接池
Jedis本身是线程不安全的,并且频繁的创建和销毁连接会有性能损耗,因此使用Jedis连接池代替Jedis的直连方式是比较理想的
1 | package com.wht; |
Springboot集成redis
SpringBoot是使用的SpringDataRedis来整合的redis(SpringData是Spring中数据操作的模块,包含对各种数据库的集成)
- 提供了RedisTemplate工具类,其中封装了各种对Redis的操作
- 提供了对不同Redis客户端的整合(Lettuce和Jedis)
- 提供了RedisTemplate统一API来操作Redis
- 支持Redis的发布订阅模型
- 支持Redis哨兵和Redis集群
- 支持基于Lettuce的响应式编程
- 支持基于JDK.JSON.字符串.Spring对象的数据序列化及反序列化
- 支持基于Redis的JDKCollection实现
基本使用
导入依赖:
1
2
3
4
5
6
7
8
9
10
11
12<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- spring2.X集成redis所需common-pool2-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.6.0</version>
</dependency>在配置文件中配置:
1
2
3
4
5
6
7
8
9
10
11
12
13spring:
redis:
database: 0 #Redis数据库索引(默认为0)
host: 192.168.227.130 #Redis服务器地址
lettuce:
pool:
20 #连接池最大连接数(使用负值表示没有限制) :
5 #连接池中的最大空闲连接 :
-1 #最大阻塞等待时间(负数表示没限制) :
0 #连接池中的最小空闲连接 :
port: 6379 #Redis服务器连接端口
timeout: 1800000 #连接超时时间(毫秒)
password: root123456 #验证密码自定义RedisTemplate(因为RedisTemplate可以接收任意Object作为值写入Redis,但是默认使用的是JDK序列化这样可读性很差)
方式一:(注意一定要导入json依赖或者web的starter)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class RedisConfig {
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory){
// 创建RedisTemplate对象
RedisTemplate<String, Object> template = new RedisTemplate<>();
// 设置连接工厂
template.setConnectionFactory(connectionFactory);
// 创建JSON序列化工具
GenericJackson2JsonRedisSerializer jsonRedisSerializer =
new GenericJackson2JsonRedisSerializer();
// 设置Key的序列化
template.setKeySerializer(RedisSerializer.string());
template.setHashKeySerializer(RedisSerializer.string());
// 设置Value的序列化
template.setValueSerializer(jsonRedisSerializer);
template.setHashValueSerializer(jsonRedisSerializer);
// 返回
return template;
}
/**
* JSON 序列化器
*/
public RedisSerializer<Object> RedisSerializer() {
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
// JavaTimeModule用于处理LocalDate类型
objectMapper.registerModule(new JavaTimeModule());
// ALL表示序列化对象中的所有成员(包括getter、setter、field等);ANY表示序列化所有可见度的成员(包括private)
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// LaissezFaireSubTypeValidator.instance表示:不做验证,允许序列化所有实例
// ObjectMapper.DefaultTyping.NON_FINAL表示:除了final声明的值和基本类型外,都会在序列化时添加类名标识(final不应被序列化)
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
serializer.setObjectMapper(objectMapper);
return serializer;
}
}使用redisTemplate
redisTemplate的操作对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class RedisTestController {
private RedisTemplate redisTemplate;
public String testRedis() {
//设置值到redis
redisTemplate.opsForValue().set("name","lucy");
//从redis获取值
String name = (String)redisTemplate.opsForValue().get("name");
return name;
}
}使用StringRedisTemplate
为了节省内存空间,我们可以不使用JSON序列化器来处理value,而是统一使用String序列化器,要求只能存储String类型的key和value。这样必须由我们自己手动的来序列化、反序列化。
1
2
3
4
5
6
7
8
9
void test03(){
User user = new User(1L, "jack");
//使用stringRedisTemplate存入自己转换的json
stringRedisTemplate.opsForValue().set("user:3",JSON.toJSONString(user));
//转换获取出来的json字符串
User user1 = JSON.parseObject(stringRedisTemplate.opsForValue().get("user:3"),User.class);
System.out.println(user1);
}更多RedisTemplate的方法参考其他博客
Springboot 的缓存抽象
Spring从3.1开始定义了org.springframework.cache.Cache,Spring从3.1开始定义了org.springframework.cache.Cache
和org.springframework.cache.CacheManager接口来统一不同的缓存技术;并支持使用JCache (JSR-107)注解简化我们开发。
每次调用需要缓存功能的方法时,Spring会检查检查指定参数的指定的目标方法是否已经被调用过;如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户。下次调用直接从缓存中获取。
使用Spring缓存抽象时我们需要关注以下两点:
- 确定方法需要被缓存以及他们的缓存策略
- 从缓存中读取之前缓存存储的数据
重要概念
- Cache:缓存接口,定义缓存操作。实现有:RedisCache、EhCacheCache、ConcurrentMapCache等
- CacheManager:缓存管理器,管理各种缓存(Cache)组件
- keyGenerator:缓存数据时key生成策略
- serialize:缓存数据时value序列化策略
常用注解
@EnableCaching:开启基于注解的缓存
@Cacheable:主要针对方法配置,能够根据方法的请求参数对其结果进行缓存
- 属性:
- cacheNames/value:指正缓存组件的名字
- key:缓存数据使用的key,可以用它来指定。默认是使用方法参数的值(编写SpEL; #id;参数id的值#a#p日#root.args[0])
- keyGenerator: key的生成器;可以自己指定key的生成器的组件id(key/keyGenerator:二选一使用)
- cacheManager:指定缓存管理器;或者cacheResolver指定获取解析器
- condition:指定符合条件的情况下才缓存
- unless:否定缓存:当unless指定的条件为true,方法的返回值就不会被缓存
- 可以获取到结果进行判断
unless ="#result == nuLL"
- 可以获取到结果进行判断
- sync:是否使用异步模式
@CachePut:保证方法被调用,又希望结果被缓存。(常用于更新方法)
@CacheEvict:方法被调用后清空缓存。(常用于删除方法)
- 可以配置allEntries = true清空所有缓存
- beforeInvocation = false:缓存的清除是否在方法之前执行
默认代表缓存清除操作是在方法执行之后执行;如果出现异常缓存就不会清除 - beforeInvocation = true:
代表清除缓存操作是在方法运行之前执行,无论方法是否出现异常,缓存都清除
使用Redis当缓存
springboot默认使用ConcurrentMapCacheManager以及ConcurrentMapCache
自定义keyGenerator
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16/**
* 配置默认键生成类
*/
public KeyGenerator keyGenerator() {
return myKeyGenerator();
}
/**
* 自定义键生成类 [类名_方法名_参数]
*/
KeyGenerator myKeyGenerator(){
return (target, method, params) -> target.getClass().getSimpleName() + "_"
+ method.getName() + "_"
+ StringUtils.arrayToDelimitedString(params, "_");
}自定义 CacheManager(由于默认缓存管理器还是用jdk序列化的)
1
2
3
4
5
6
7
8
9
10
11
12
13/**
* 缓存管理器
*/
public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.string())) // 设置键序列化方式
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jsonSerializer)) // 设置值序列化方式
.entryTtl(Duration.ofHours(1)) // 设置缓存有效期
.disableCachingNullValues(); // 不缓存空值
return new RedisCacheManager(redisCacheWriter, redisCacheConfiguration);
}完整配置类
1 | import com.fasterxml.jackson.annotation.JsonAutoDetect; |
封装工具类
1 | package com.wht.utils; |
事务和锁机制
Redis事务特性:
单独的隔离操作 :事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
没有隔离级别的概念 :队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行
不保证原子性 :事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚
事务相关命令
- Multi:从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec
- Exec:Redis会将之前的命令队列中的命令依次执行
- discard:组队的过程中可以通过discard来放弃组队。
事务错误机制
- 组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消。
- 如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。
锁机制
悲观锁
悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
乐观锁
乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。
相关命令
watch key1 [key2]:在执行multi之前执行,可以监视一个(或多个) key ,在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
unwatch :取消 WATCH 命令对所有 key 的监视。如果在执行 WATCH 命令之后,EXEC 命令或DISCARD 命令先被执行了的话,那么就不需要再执行UNWATCH 了。
Redis持久化
因为Redis的数据是放在内存中的,持久化就是把内存的数据写到磁盘中去,防止服务宕机了内存数据丢失。
Redis 提供了2个不同形式的持久化方式:
- RDB(Redis DataBase)
- AOF(Append Of File)
RDB
在指定的时间间隔内将内存中的数据集快照写入磁盘(具体操作:Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到 一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件)
使用
默认情况下,Redis 将数据集的快照保存在磁盘上的一个名为dump.rdb
.
如果数据集中至少有 M 次更改,您可以将 Redis 配置为每 N 秒保存一次数据集,或者您可以手动调用SAVE
orBGSAVE
命令
如:save 60 1000
:如果至少更改了 1000 个键,则此配置将使 Redis 每 60 秒自动将数据集转储到磁盘
如:bgsave
:命令用于在后台异步保存当前数据库的数据到磁盘
使用场景:
- 适合大规模的数据恢复
- 对数据完整性和一致性要求不高更适合使用
- 节省磁盘空间
- 恢复速度快
缺点
- Fork的时候,内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑
- 虽然Redis在fork时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能。
- 在备份周期在一定间隔时间做一次备份,所以如果Redis意外down掉的话,就会丢失最后一次快照后的所有修改。
AOF
以日志的形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来(AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量),redis 重启的时候就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作
使用
因为AOF默认不开启,需要在redis.conf中配置
设置appendonly yes
从现在开始,每次 Redis 接收到更改数据集的命令(例如SET
时,它都会将其附加到 AOF。当您重新启动 Redis 时,它将重新播放 AOF 以重建状态。
AOF和RDB同时开启,系统默认取AOF的数据(数据不会存在丢失),所以建议两个都启用,不建议单独使用AOF
AOF的同步频率
appendfsync always
:始终同步,每次Redis的写入都会立刻记入日志;性能较差但数据完整性比较好appendfsync everysec
:每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失。appendfsync no
:redis不主动进行同步,把同步时机交给操作系统。
缺点:
比起RDB占用更多的磁盘空间。
恢复备份速度要慢。
每次读写都同步的话,有一定的性能压力。
存在个别Bug,造成恢复不能。
IO多线程
Redis6终于支撑多线程了,告别单线程了吗?
IO多线程其实指客户端交互部分的网络IO交互处理模块多线程,而非执行命令多线程。Redis6执行命令依然是单线程。
开启多线程
进入配置文件
修改配置:
1
2
3
4#是否开启多线程
io-threads-do-reads yes
#线程数
io-threads 4