Redis学习笔记
一、什么是Redis
Redis是一个非关系型数据库,它允许使用者以键值对的形式存储数据,每个键中可以存储不同的数据结构,如字符串,数值,哈希,有序数据结构等。
关键词:单线程、基于内存操作、高性能
使用redis-benchmark可以测试redis的性能,比如:
测试当前机器上100并发量下处理10万次请求的性能。
redis-benchmark -h localhost -p 6379 -c 100 -n 100000 -a password
-h:主机IP -p:端口号 -c:连接数 -n:请求数 -a:密码
二、Redis安装
1. Linux下安装
1)官方网站下载压缩包,上传到服务器
2)解压文件
tar -zxvf redis-6.0.6.tar.gz
3)进入redis目录
cd redis-6.0.6
4)编译安装
make
2. Windows下安装
1)Github下载安装包
2)按步骤安装
三、基本指令
1. 启动Redis服务
步骤:1)进入Redis目录下的src文件夹
2)执行命令 ./redis-server
2. 启动Redis客户端
步骤:1)进入Redis目录下的src文件夹
2)执行命令 ./redis-cli
3. 关闭Redis服务
方式一:使用kill命令结束进程
步骤:1)查看进程,命令:ps -ef | grep redis
2)找到进程编号
3)结束进程,命令:kill -9 进程编号
方式二:使用shutdown命令停止服务
步骤:1)启动redis客户端
2)停止服务,命令:shutdown
4. 后台启动Redis服务
步骤:1)进入Redis目录
2)编辑redis.conf文件,命令:vim redis.conf
3)找到"daemonize no"这一行,修改为"daemonize yes",保存退出
4)启动Redis服务,命令:./src/redis-server ./redis.conf
5. 设置Redis连接密码
步骤:1)编辑redis.conf文件
2)找到"requirepass"这一行,修改为"requirepass 123456",保存退出
3)重启Redis服务
测试:1)启动redis-cli
2)输入ping命令,回车,出现"(error) NOAUTH Authentication required."则表示设置成功
3)连接Redis服务器,命令:auth 123456,出现“OK"提示,则表示连接成功
4)输入ping命令,提示"pong",表示连接畅通。
6. 切换数据库
redis默认有16个数据库,索引从0~15,可以使用命令切换
语法格式:
select index
例如:
> select 3
OK
> select 0
OK
7. 查看数据量
查看当前数据库存在的数据总量。
> dbsize
8. 查看当前数据库的key
1)查看所有key
语法格式:
keys *
2)检查key是否存在
语法格式:
EXISTS key
例如:检查name是否存在
> EXISTS name
(integer) 0
9. 移动key
把键值对移动到指定索引的数据库
语法格式:
move key index
例如:把name移动到索引为1的数据库
> move name 1
10. 删除key
从Redis中删除一个键值对,需要指定Key
语法格式:
del key
例如:
> del name
(integer) 1
> get name
(nil)
11. 清空数据
1)清空当前库
> flushdb
2)清空全部数据
> flushall
12. 设置key的时效
指定key在一定时间后失效
语法格式:
expire key seconds
例如:把name设置成10秒后失效
> expire name 10
13. 查看key的时效
语法格式:
ttl key
例如:
> ttl name
(integer) 10
> set age 10
OK
> ttl age
(integer) -1
> ttl sex
(integer) -2
-1表示没有设置时效,-2表示已失效。
14. 查看key的类型
语法格式:
type key
例如:
> type name
string
四、五大数据类型
1. string 字符串
string 是 redis 最基本的类型,你可以理解成与 Memcached 一模一样的类型,一个 key 对应一个 value。
string 类型是二进制安全的。意思是 redis 的 string 可以包含任何数据。比如jpg图片或者序列化的对象。
string 类型是 Redis 最基本的数据类型,string 类型的值最大能存储 512MB
1)SET 新增/修改
向Redis中添加一个键值对,需要指定Key和Value
> SET name "Miguel"
ok
> SET count 10
ok
2)GET 查询
从Redis中读取数据,需要指定Key
> GET name
"Miguel"
3)APPEND 拼接
向指定key的值后拼接字符串,如果指定的key不存在,则相当于set key value
> APPEND name "_Pan"
(integer) 10
4)STRLEN 长度
查看指定key的字符串值的长度
> STRLEN name
(integer) 10
5)INCR 自增
指定key的字符串值加一
> INCR count
(integer) 11
6)INCRBY 加法
指定key的字符串值加上特定数值
> INCRBY count 10
(integer) 21
7)DECR 自减
指定key的字符串值减一
> DECR count
(integer) 20
8)DECRBY 减法
指定key的字符串值减去特定数值
> DECRBY count 10
(integer) 10
9)GETRANGE 截取
返回指定key的指定索引范围的字符串
> GETRANGE name 0 5
"miguel"
10)SETRANGE 范围修改
替换指定key的字符串值的从指定位置开始的字符
> SETRANGE name 7 "winnie"
(integer) 13
> GET name
"miguel_winnie"
2. hash 哈希
Redis hash 是一个键值(key=>value)对集合。
Redis hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象。
1)HSET 新增/修改
设置一个字段值。
语法格式:
HSET key field value
例如:
> HSET runoob name "miguel"
OK
2)HGET 查询
查询一个字段值。
语法格式:
HGET key field
例如:
> HGET runoob name
"miguel"
3)HMSET 新增/修改
设置多个字段值。
语法格式:
HMSET key field value [field value …]
例如:
> HMSET runoob field1 value1 field2 value2
OK
4)HMGET 查询
查询多个字段值。
语法格式:
HGET key field [field …]
例如:
> HGET runoob field1
"value1"
5)HGETALL 查询全部
查询全部字段值。
语法格式:
HGETALL key
例如:
> HGETALL runoob
1) "name"
2) "miguel"
3) "field1"
4) "value1"
5) "field2"
6) "value2"
6)HLEN 统计
查询hash表的字段数量。
语法格式:
HLEN key
例如:
> HLEN runoob
(integer) 3
7)HEXISTS 判断存在
判断指定字段是否存在于指定hash表,存在则返回1,不存在则返回0。语法格式:
HEXISTS key field
例如:
> HEXISTS runoob name
(integer) 1
> HEXISTS runoob age
(integer) 0
8)HKEYS 查询全部字段
查询指定hash表的全部字段。
语法格式:
HKEYS key
例如:
> HKEYS runoob
1) "name"
2) "field1"
3) "field2"
9)HVALS 查询全部值
查询指定hash表的全部值。
语法格式:
HVALS key
例如:
> HVALS runoob
1) "miguel"
2) "value1"
3) "value2"
10)HINCRBY 加法
给指定字段值加上一个数值,成功后返回结果值。
语法格式:
HINCRBY key field num
例如:
> HSET runoob age 10
OK
> HINCRBY runoob age 5
(integer) 15
3. list 列表
Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。
1)lpush 左侧新增
从列表的左边插入元素,成功则返回当前列表的大小,语法格式:
lpush key value
例如:
> DEL runoob
(integer) 1
> lpush runoob redis
(integer) 1
> lpush runoob mongodb
(integer) 2
> lpush runoob mysql
(integer) 3
2)rpush 右侧新增
从列表的右边插入元素,成功则返回当前列表的大小,语法格式:
rpush key value
例如:
> rpush runoob oracle
(integer) 4
3)lpop 左侧移除
移除列表左边第一个元素并返回,语法格式:
lpop key
例如:
> lpop runoob
"mysql"
4)rpop 右侧移除
移除列表右边第一个元素并返回,语法格式:
rpop key
例如:
> rpop runoob
"oracle"
5)lindex 索引查找
通过下标获取列表元素,语法格式:
lindex key index
例如:
> lindex runoob 1
"mongodb"
6)lrange 范围遍历
获取列表指定范围的元素,语法格式:
lrange key start stop
例如:
> lrange runoob 0 2
"redis"
"mongodb"
"mysql"
7)llen 统计
查看列表的大小,语法格式:
llen key
例如:
> llen runoob
(integer) 2
8)lrem 指定移除
移除列表中的指定个数的指定元素,成功则返回删除的元素个数,语法格式:
lrem key count value
例如:
> lrem runoob 1 "mongodb"
(integer) 1
9)ltrim 截取
通过下标截取列表,成功后列表会被改变,语法格式:
ltrim key start stop
例如:
> flushdb
OK
> lpush runoob "redis" "mongodb" "mysql" "oracle"
(integer) 4
> ltrim runoob 0 2
OK
> lrange runoob 0 -1
1) "oracle"
2) "mysql"
3) "mongodb"
10)lset 修改
替换列表指定下标的元素值,前提是指定下标存在元素,语法格式:
lset key index value
例如:
> lset runoob 0 redis
OK
11)linsert 插入
在列表指定元素的前面或后面插入新的元素,成功则返回列表大小,语法格式:
linsert key before|after pivot element
例如:
> linsert runoob after redis oracle
(integer) 4
4. set 集合
Redis 的 Set 是 string 类型的无序集合。
集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。
1)sadd 增加
用于添加一个 string 元素到 key 对应的 set 集合中,成功返回 1,如果元素已经在集合中返回 0,语法格式:
sadd key member
例如:
> DEL runoob
(integer) 1
> sadd runoob redis
(integer) 1
> sadd runoob mongodb
(integer) 1
> sadd runoob rabbitmq
(integer) 1
> sadd runoob rabbitmq
(integer) 0
2)smembers 遍历
遍历set集合中的元素,语法格式:
smembers key
例如:
> smembers runoob
1) "redis"
2) "rabbitmq"
3) "mongodb"
3)sismember 判断存在
判断指定值是否存在于set集合中,存在返回1,否则返回0,语法格式:
sismember key value
例如:
> smembers runoob redis
(integer) 1
4)scard 统计
获取set集合中元素的个数,语法格式:
scard key
例如:
> scard runoob
(integer) 3
5)srem 删除
移除set集合中指定元素,语法格式:
srem key value
例如:
> srem runoob rabbitmq
(integer) 1
6)srandmember 随机返回
随机返回set集合中指定个数的元素,语法格式:
srandmember key count
例如:
> srandmember runoob 1
"redis"
7)spop 随机移除
随机移除元素并返回,语法格式:
spop key
例如:
> spop runoob
"mongodb"
8)smove 移动
移动指定元素到另一个set集合中,成功则返回移动的元素个数,语法格式:
smove source destination member
例如:
> smove runoob myset "redis"
(integer) 1
9)sdiff 差集
返回指定set集合与一个或多个set集合的差集,语法格式:
sdiff key [key …]
例如:
> sadd runoob "redis" "mongodb" "mysql"
(integer) 3
> sadd myset "hello" "world"
(integer) 3
> sdiff runoob myset
1) "mongodb"
2) "mysql"
10)sinter 交集
返回指定set集合与一个或多个set集合的交集,语法格式:
sinter key [key …]
例如:
> sinter runoob myset
1) "redis"
11)sunion 并集
返回指定set集合与一个或多个set集合的并集,语法格式:
sunion key [key …]
例如:
> sunion runoob myset
1) "redis"
2) "mongodb"
3) "mysql"
4) "hello"
5) "world"
5. zset 有序集合
Redis zset 和 set 一样也是string类型元素的集合,且不允许重复的成员。
不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。
zset的成员是唯一的,但分数(score)却可以重复。
1)zadd 新增
添加元素到集合,元素在集合中存在则更新对应score。
语法格式:
zadd key score member
例如:
> zadd myset 100 redis
(integer) 1
> zadd myset 200 mongodb
(integer) 1
> zadd myset 300 rabbitmq
(integer) 1
2)zrange 范围查询
遍历zset集合中在指定索引范围内的元素,分数从低到高排序。
语法格式:
zrange key start stop [withscores]
例如:
> zrange myset 0 -1
1) "redis"
2) "mongodb"
3) "rabbitmq"
> zrange myset 0 -1 withscores
1) "redis"
2) "100"
3) "mongodb"
4) "200"
5) "rabbitmq"
6) "300"
-1表示倒数第一个元素,-2代表倒数第二个元素,withscores表示把分数一起返回。
3)zrevrange 范围查询
遍历zset集合中在指定索引范围内的元素,分数从高到低排序。
语法格式:
zrevrange key start stop [withscores]
例如:
> zrevrange myset 0 -1 withscores
1) "rabbitmq"
2) "300"
3) "mongodb"
4) "200"
5) "redis"
6) "100"
4)zrangebyscore 通过分数范围查询
遍历zset集合中在指定分数区间的元素。
语法格式:
zrangebyscore key min max [withscores] [limit offset count]
例如:
> zrangebyscore myset 0 1000
1) "redis"
2) "mongodb"
3) "rabbitmq"
5)zrem 删除
删除zset集合中的指定元素
语法格式:
zrem key member
例如:
> zrem myset rabbitmq
(integer) 1
6)zcard 统计
查询zset集合中的元素个数。
语法格式:
zcard key
例如:
> zcard myset
(integer) 2
7)zcount 范围统计
查询zset集合中指定分数范围内的元素个数。
语法格式:
zcount key min max
例如:
> zcount myset 0 100
(integer) 1
五、三种特殊数据类型
1. Geospatial 地理空间
1)geoadd 新增
向key中新增一个地理位置。
语法格式:
geoadd key longitude latitude member
例如:
> geoadd china:city 116.23 40.22 beijing
(integer) 1
2)geopos 查询
查询key中的地理位置。
语法格式:
geopos key member [member …]
例如:
> geopos china:city beijing
1) 1) "116.23000055551528931"
2) "40.2200010338739844"
3)geodist 计算距离
计算key中存在的两个地理位置之间的直线距离。
语法格式:
geodist key member1 member2 [m|km|ft|mi]
- m 表示单位为 米
- km 表示单位为 千米
- ft 表示单位为 英里
- mi 表示 单位为 英尺
例如:
> geodist china:city beijing shanghai #默认单位为m
"1088785.4302"
> geodist china:city beijing shanghai km
"1088.7854"
4)georadius | georadiusbymember 范围查询
以给定的经纬度(地理位置)为中心,查询指定半径范围内的key中存在的元素。
语法格式:
georadius key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count]
georadius key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count]
- WITHCOORD 表示携带 经纬度
- WITHDIST 表示携带 距离
- WITHHASH 表示 以 52 位有符号整数的形式, 返回位置元素经过原始 geohash 编码的有序集合分值
- COUNT 表示 限制返回元素的个数
例如:
> georadius china:city 110 30 1000 km
1) "shenzhen"
2) "guangzhou"
> georadiusbymember china:city guangzhou 500 km
1) "shenzhen"
2) "guangzhou"
5)geohash 查询哈希
把指定的地理位置的经纬度转换为一个11位的字符串并返回。
语法格式:
geohash key member [member …]
例如:
> geohash china:city beijing shanghai
1) "wx4sucu47r0"
2) "wtw6sk5n300"
PS:GEO底层的实现原理就是Zset,所以我们可以用Zset命令操作GEO的元素。
2. HyperLogLog 基数统计
HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素
1)pfadd 新增
添加指定元素到 HyperLogLog 中。
语法格式:
pfadd key [member …]
例如:
> pfadd myhlog "redis" "mongodb" "rabbitmq"
(integer) 1
> pfadd myhlog2 "mysql" "oracle" "mongodb"
(integer) 1
2)pfcount 统计
返回给定 HyperLogLog 的基数估算值。
语法格式:
pfcount key
例如:
> pfcount myhlog
(integer) 3
> pfcount myhlog2
(integer) 3
3)pfmerge 整合
将多个 HyperLogLog 合并为一个 HyperLogLog。
语法格式:
pfmerge destkey [sourcekey…]
例如:
> pfmerge myhlog3 myhlog myhlog2
OK
> pfcount myhlog3
(integer) 5
3. Bitmap 位图
Bitmap是用于存储二进制位的数据结构。
1)setbit 新增/修改
给指定索引的位设置值。
语法格式:
setbit key offset value
例如:
> setbit sign 0 1
(integer) 0
> setbit sign 1 0
(integer) 0
> setbit sign 2 1
(integer) 0
> setbit sign 3 1
(integer) 0
2)getbit 查询
查询指定索引的位的值。
语法格式:
getbit key offset
例如:
> getbit sign 1
(integer) 0
> getbit sign 2
(integer) 1
六、事务
redis事务有三大特性:
-
批量操作:开启事务后输入的操作指令被放入队列缓存,发送exec命令前不会执行。
-
原子性:事务中的命令要么全部被执行,要么全部不执行。
-
排他性:其他客户端提交的命令不会插入到事务执行命令序列中。
事务中的命令如果出现编译错误,那么事务中的全部命令都不会执行;如果命令中只存在逻辑异常,全部命令都会执行。
1. 开启事务
开启一个事务,后续输入的命令都会进入队列,但是不会立刻执行。
语法格式:
multi
例如:
> multi
OK
> keys *
QUEUED
> flushdb
QUEUED
2. 执行事务
执行事务中的全部命令。
语法格式:
exec
例如:
> multi
OK
> keys *
QUEUED
> flushdb
QUEUED
> exec
1) 1) "china:city"
2) OK
3. 乐观锁
乐观锁,即不加锁,只在数据更新时进行版本校验。如果版本不对,则更新失败;否则,更新数据。
1)watch 监视(锁定)
监视所有指定的key,在事务中有条件的执行。
语法格式:
watch key [key …]
例如:
> watch money
OK
2)unwatch 解除监视(解锁)
解除监视所有指定的key。
语法格式:
unwatch key [key…]
例如:
> unwatch money
OK
PS:如果事务执行失败,一般应该先解除监视,再重新监视以获取最新的值。
七、Jedis
Jedis是Java连接和操作redis的中间件,可以让Java完成所有Redis的操作。
1. 导入依赖
使用前必须导入Jedis相关依赖,以maven工程为例:
<dependency>
<!--导入jedis的包-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.2.0</version>
</dependency>
</dependency>
2. 建立连接
创建Jedis对象时,传入主机名和端口号,即可建立redis连接:
Jedis jedis = new Jedis("127.0.0.1", 6379);
3. 操作
jedis对redis的所有操作的方法名都与redis命令相同,如:
// 五大数据类型
// set
jedis.set("name", "miguel");
// lpush
jedis.lpush("mylist", "redis", "oracle", "mongodb");
// sadd
jedis.sadd("myset", "java");
// hset
jedis.hset("myhash", "age", 10);
// zadd
jedis.zadd("myzset", 1000, "zhangsan");
// 三种特殊类型
// geoadd
jedis.geoadd("mygeo", 116.23, 40.22, "Beijing");
// pfadd
jedis.pfadd("mylog", "Tencent", "Alibaba", "JingDong");
// setbit
jedis.setbit("mybitmap", 0, 1);
4. 断开连接
为了避免占用线程,每次使用完后尽量主动断开redis连接,如:
jedis.close();
八、Springboot整合
Springboot 2.0以下的版本使用的是Jedis连接redis,但是从Springboot 2.0开始改为使用lectuce连接redis,原因是:
jedis是基于tcp的阻塞式连接方式,在并发量高的前提下,性能低;并且,jedis采用的是socket共享的方式,每个线程操作的是同一个jedis实例,这会导致线程不安全,所以通常会采用连接池管理jedis连接,使每个线程拥有自己的Jedis实例。
lectuce是基于netty的多路复用异步非阻塞的连接方式,连接实例可以在多个线程间并发访问,所以一个实例就可以满足多线程并发的情况,在并发量高的前提下,相比于jedis,性能较高,线程更安全;而且可以根据实际情况增减连接实例的数量。
1. 创建工程
创建一个springboot的maven工程,版本为2.4.2,并勾选相应的依赖:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uXnzBue4-1613040297269)(C:\Users\Miguel\AppData\Roaming\Typora\typora-user-images\image-20210131155606396.png)]
2. 查看源码
创建完毕后可以看到pom.xml文件中已经导入以下依赖:
<dependencies>
<!--用于操作redis的依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--其他依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
深入上面的redis依赖查看,如下图:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.4.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>2.4.3</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>6.0.2.RELEASE</version>
<scope>compile</scope>
</dependency>
</dependencies>
可以看到底层采用的是lettuce连接redis。
3. 基本配置
通常我们都是连接远程服务器上的redis,所以需要在application.properties配置,如下:
## 主机名
spring.redis.host=127.0.0.1
## 端口号
spring.redis.port=6379
4. 测试运行
springboot为redis设计了一个模板对象RedisTemplate,我们可以直接注入使用:
@SpringBootTest
class Redis02SpringbootApplicationTests {
@Autowired
private RedisTemplate redisTemplate;
@Test
void contextLoads() {
// 五大数据类型
redisTemplate.opsForValue().set("name", "miguel");
redisTemplate.opsForHash().put("myhash", "age", 10);
redisTemplate.opsForList().leftPush("mylist", "redis");
redisTemplate.opsForSet().add("myset", "java");
redisTemplate.opsForZSet().add("myzset", "zhangsan", 100);
// 三种特殊类型
redisTemplate.opsForGeo().add("mygeo", new GeoLocation("Beijing", new Point(116.23, 40.22)));
redisTemplate.opsForHyperLogLog().add("mylog", "redis", "mongodb", "mysql");
redisTemplate.opsForValue().setBit("mybitmap", 0, false); //opsForValue包含操作String和Bitmap的方法
// 获取连接对象
RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
}
}
5. 保存对象
企业开发中,Java对象经常是数据的载体,但是对于redis,却不能直接保存对象,如下面代码:
@SpringBootTest
class Redis02SpringbootApplicationTests {
@Autowired
private RedisTemplate redisTemplate;
@Test
void contextLoads() {
// 关于对象的保存
User user = new User("miguel", 10);
redisTemplate.opsForValue().set("user", user);
System.out.println(redisTemplate.opsForValue().get("user"));
}
}
运行后报出以下错误:
org.springframework.data.redis.serializer.SerializationException: Cannot serialize; nested exception is org.springframework.core.serializer.support.SerializationFailedException: Failed to serialize object using DefaultSerializer; nested exception is java.lang.IllegalArgumentException: DefaultSerializer requires a Serializable payload but received an object of type [com.miguel.pojo.User]
从错误信息中可以知道,缺少了序列化操作,这说明我们首先要让对象序列化,然后再进行保存,所以应该改为下面代码:
@SpringBootTest
class Redis02SpringbootApplicationTests {
@Autowired
private RedisTemplate redisTemplate;
@Test
void contextLoads() {
// 关于对象的保存
User user = new User("miguel", 10);
String jsonUser = new ObjectMapper().writeValueAsString(user);
redisTemplate.opsForValue().set("user", jsonUser);
System.out.println(redisTemplate.opsForValue().get("user"));
}
}
或者在实体类中实现Serializable接口:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements Serializable {
private String name;
private Integer age;
}
企业开发中通常采用第二种方式,这样对象就能转化为字符串保存到Redis中了。
6. 自定义配置类
由于springboot默认采用的JDK序列化会导致key存入redis后出现乱码的情况,不能满足我们的需求,所以通常在企业开发中会自己编写配置类,以便自定义序列化器,如下代码:
@Configuration
public class RedisConfig {
// 编写自己的restTemplate
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
// 设置连接工厂
template.setConnectionFactory(redisConnectionFactory);
// 创建自己想要的序列化器
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
// 对象映射
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
// 加载对象映射
jackson2JsonRedisSerializer.setObjectMapper(om);
// String序列化器
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// Key采用String序列化器
template.setKeySerializer(stringRedisSerializer);
// Hash的key采用String序列化器
template.setHashKeySerializer(stringRedisSerializer);
// Value采用jackson序列化器
template.setValueSerializer(jackson2JsonRedisSerializer);
// Hash的value采用jackson序列化器
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
这里使用jackson的序列化器把key转换成json字符串,可以有效的避免乱码问题。
7. 封装工具类
如果在日常开发中使用redisTemplate中现成的方法来操作redis,就无法避免每次都需要编写redisTemplate.opsForXXX(),使得代码非常臃肿,所以在企业开发中我们通常封装自己的工具类,方便使用,还能使代码看起来更加简洁易懂,以本人封装的工具类作为参考:
@Component
public final class RedisUtil {
@Autowired
@Qualifier("redisTemplate")
private RedisTemplate redisTemplate;
public boolean expire(String key, long timeout) {
try {
if (timeout > 0) {
redisTemplate.expire(key, timeout, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
public Long ttl(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
public boolean del(String... keys) {
if (keys.length != 0) {
return redisTemplate.delete(Arrays.asList(keys)) > 0;
}
return false;
}
//---------------String相关操作-----------------
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
public boolean set(String key, Object value, Long seconds) {
try {
redisTemplate.opsForValue().set(key, value, seconds, TimeUnit.SECONDS);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
public Object get(String key) {
return StringUtils.hasLength(key) ? redisTemplate.opsForValue().get(key) : null;
}
public Double incr(String key, double num) throws Exception {
if (num < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, num);
}
public Long decr(String key, long num) {
if (num > 0) {
throw new RuntimeException("递减因子必须小于0");
}
return redisTemplate.opsForValue().decrement(key, num);
}
//---------------Hash相关操作-----------------
public boolean hset(String key, String field, Object value) {
try {
redisTemplate.opsForHash().put(key, field, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
public boolean hset(String key, String field, Object value, long timeout) {
try {
this.hset(key, field, value);
if (timeout > 0) {
this.expire(key, timeout);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
public boolean hmset(String key, Map<String, Object> entries) {
try {
redisTemplate.opsForHash().putAll(key, entries);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
public boolean hmset(String key, Map<String, Object> entries, long timeout) {
try {
this.hmset(key, entries);
if (timeout > 0) {
this.expire(key, timeout);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
public Object hget(String key, String field) {
return StringUtils.hasLength(key) ? redisTemplate.opsForHash().get(key, field) : null;
}
public List hmget(String key, List<String> fields) {
return redisTemplate.opsForHash().multiGet(key, fields);
}
public Map<String, Object> hgetall(String key) {
return redisTemplate.opsForHash().entries(key);
}
public Long hdel(String key, Object... fields) {
return redisTemplate.opsForHash().delete(key, fields);
}
public Boolean hexists(String key, String field) {
return redisTemplate.opsForHash().hasKey(key, field);
}
public double hincr(String key, String field, double incr) {
return redisTemplate.opsForHash().increment(key, field, incr);
}
public double hdecr(String key, String field, double decr) {
return redisTemplate.opsForHash().increment(key, field, -decr);
}
//---------------List相关操作-----------------
public boolean lpush(String key, Object value) {
try {
redisTemplate.opsForList().leftPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
public boolean lpush(String key, List value) {
try {
redisTemplate.opsForList().leftPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
public boolean rpush(String key, Object value) {
try {
redisTemplate.opsForList().leftPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
public boolean rpush(String key, List value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
public Object lpop(String key) {
return StringUtils.hasLength(key) ? redisTemplate.opsForList().leftPop(key) : null;
}
public Object rpop(String key) {
return StringUtils.hasLength(key) ? redisTemplate.opsForList().rightPop(key) : null;
}
public Object lindex(String key, long index) {
return StringUtils.hasLength(key) ? redisTemplate.opsForList().index(key, index) : null;
}
public Long llen(String key) {
return redisTemplate.opsForList().size(key);
}
public boolean lrem(String key, Object value) {
try {
redisTemplate.opsForList().remove(key, 1, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
public boolean lset(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
//---------------Set相关操作-----------------
public boolean sadd(String key, Object... value) {
try {
redisTemplate.opsForSet().add(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
public Set smembers(String key) {
return redisTemplate.opsForSet().members(key);
}
public Long srem(String key, Object... values) {
return redisTemplate.opsForSet().remove(key, values);
}
public boolean sismember(String key, Object value) {
return redisTemplate.opsForSet().isMember(key, value);
}
public Long scard(String key) {
return redisTemplate.opsForSet().size(key);
}
//---------------Zset相关操作-----------------
public boolean zadd(String key, Object value, double score) {
try {
redisTemplate.opsForZSet().add(key, value, score);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
public boolean zrem(String key, Object... values) {
try {
redisTemplate.opsForZSet().remove(key, values);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
public Long zcard(String key) {
return redisTemplate.opsForZSet().size(key);
}
public Long zcount(String key, double min, double max) {
return redisTemplate.opsForZSet().count(key, min, max);
}
}
到这里,Springboot与redis的整合已经基本完成。
九、Redis.conf
Redis.conf 作为Redis的配置文件,我们有必要了解其中的一些重要配置,以便日后的项目搭建。
1. Unit 单位
指定Redis分配的内存大小,默认是不配置的,配置时不区分大小写字母。
不同内存单位对应的字节数
# 1k => 1000 bytes
# 1kb => 1024 bytes
# 1m => 1000000 bytes
# 1mb => 1024*1024 bytes
# 1g => 1000000000 bytes
# 1gb => 1024*1024*1024 bytes
2. Includes 包含
指定其他配置文件的路径,引入它们的配置。
# include /path/to/local.conf
# include /path/to/other.conf
3. Network 网络
指定可访问的IP地址,可设置多个,如下:
bind 127.0.0.1 58.62.32.108
4. Protect Mode 保护模式
保护模式关闭后,外部网络可以直接访问;开启后,只允许绑定的IP地址访问,一般默认是开启的。
protected-mode yes
5. Port 端口号
指定Redis占用的端口号,默认是6379,可根据情况自行修改。
port 6379
6. Daemonize 守护进程
是否把Redis作为守护进程启动。一般都设置为yes,避免断开连接后Redis服务自动关闭。
daemonize yes
7. Pid File pid文件
当指定了端口号,就需要指定pid文件路径。该文件会在redis服务启动时创建,redis服务关闭时删除。
pidfile /var/run/redis_6379.pid #日志将记录在这个文件
8. Log Level 日志级别
设置输出日志的级别,有如下选项:
# 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)
loglevel notice #默认是notice
9. Log File 日志文件
指定日志记录的文件路径,默认是空字符串,会输出到空设备,即无记录:
logfile "/develop/redis-6.0.8/log/redis.log"
10. Databases 数据库
设置初始的数据库数量,默认是16。
databases 16
11. Always show logo 始终展示图标
是否始终展示redis的图标,默认是yes。
always-show-logo yes
十、Redis持久化
Redis是内存数据库,如果不把内存中的数据保存到磁盘中,那么一旦服务器进程退出,redis中的数据也会丢失,所以Redis提供了持久化功能。
1. RDB ( Redis Database )
在指定的时间间隔内将内存中的数据集状态写入磁盘,也就是行话讲的Snapshot快照,redis进行数据恢复时是将快照文件直接读取到内存里。
Redis会单独创建一个子进程(fork)来进行持久化。它会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。
优点:整个过程中,主进程是不进行任何IO操作的,确保了极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感的话,RDB方式要比AOF方式更加的高效。
缺点:RDB的缺点是最后一次持久化以后的数据可能会丢失。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4mOZ7UwG-1613040297271)(C:\Users\Miguel\AppData\Roaming\Typora\typora-user-images\image-20210202210339857.png)]
1) 快照间隔
可以给快照设定条件,达到条件就执行快照。
save 900 1 # 900秒内,有至少1个key被修改,就执行快照
save 300 10 # 300秒内,有至少10个key被修改,就执行快照
save 60 10000 # 60秒内, 有至少10000个key被修改,就执行快照
2) 快照文件
可以指定快照文件的名称,默认是dump.rdb,该文件默认保存在redis根目录。redis启动时会自动检查dump.rdb 并恢复其中的数据。
dbfilename dump.rdb
3)执行快照的条件
以下三种条件会触发快照:
- 满足save配置的规则
- 执行flushdb / flushall 命令后
- 退出redis
2. AOF ( Append Only File)
AOF是以日志的性质把Redis执行过的每个写操作都记录下来,只追加文件而不修改文件,Redis启动时会读取aof文件重新构建数据,换言之,就是把日志记录的操作指令从前到后执行一遍。
1)开启AOF
默认情况下,AOF是关闭的,如果想采用AOF方式持久化,需要自行修改为以下配置:
appendonly yes
2)文件名设置
默认情况下,文件名为appendonly.aof,可自行修改:
appendfilename "appendonly.aof"
3) 频率设置
默认情况是每秒保存一次。
appendfsync everysec
4)文件修复
如果 appendonly.aof 文件被损坏,就会导致Redis服务无法启动,此时我们可以使用redis-check-aof 对其进行修复,命令如下:
> redis-check-aof --fix ../appendonly.aof
十一、发布订阅
1. 订阅频道
语法格式:
SUBSCRIBE [channel…]
例如:
> SUBSCRIBE runoobChat
2. 发布消息
语法格式:
PUBLISH channel message
例如:
> PUBLISH runoobChat "Redis PUBLISH test"
3. 实现原理
redis-server里维护了一个字典,字典的键就是一个个频道,而字典的值则是一个链表,链表中保存了所有订阅这个频道的客户端。当用户通过SUBSCRIBE命令订阅某频道时,就是将客户端添加到指定频道的链表中。
当发布者通过PUBLISH命令发布消息时,redis-server会使用指定的频道作为键去查找链表,最后遍历这个链表,将消息发布给每个订阅者客户端。
十二、Redis主从复制
1. 概念
主从复制,是指将一台Redis服务器的数据复制到其他的Redis服务器。前者称为主节点(Master),后者称为从节点(Slave);数据的复制是单向的,只能由主节点到从节点。
默认情况下,每台Redis服务器都是主节点;且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。
2. 主要作用
主从复制的作用主要包括:
- 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
- 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;这实际上是一种服务冗余。
- 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
- 高可用基石:主从复制是哨兵和集群能够实施的基础,
3. 环境搭建
在同一台主机上搭建一个Redis伪集群。
1)查看配置
首先了解一下Redis主从复制的相关信息:
> info replication # 查看当前库的主从复制信息
# Replication
role:master # 角色 默认是master
connected_slaves:0 # 从机数量
master_replid:80834ceaa532efb740ff48d2aa1ec1d12c8219b5
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
2)修改相关配置
复制多份配置文件,对应多个redis服务
> mkdir mconfig
> mv redis.conf mconfig
> cd mconfig
> cp redis.conf redis79.conf
> cp redis.conf redis80.conf
> cp redis.conf redis81.conf
三个配置文件都要做相应的配置修改,主要有:
- port:端口号,这里分别配成6379、6380、6381
- logfile:日志文件,这里分别设置为6379.log、6380.log和6381.log
- pidfile:端口对应文件,改为对应的端口号,例如:/var/run/redis_6380.pid
- dbfilename:快照文件,加上对应的端口号,例如:dump6381.rdb
3)启动服务
分别启动三个redis服务,语法格式如下:
> redis-server mconfig/redis_63XX.conf
启动完成后,可通过命令查看进程:
> ps -ef | grep redis
root 25942 1 0 15:07 ? 00:00:00 redis-server *:6379
root 25951 1 0 15:08 ? 00:00:00 redis-server *:6380
root 25960 1 0 15:09 ? 00:00:00 redis-server *:6381
root 25969 25880 0 15:09 pts/3 00:00:00 grep --color=auto redis
看到上面三个redis进程,证明过程无误。
4)主从设置
默认情况下,redis服务器都是主机,我们有两种方式可以设置从机:
(1) 手动在redis-cli中操作认主。
我们现在有3台redis,如果让端口号为6379的redis作为主机,则我们只需要在另外两台redis服务器上操作,语法格式:
SLAVEOF host port
例如:
> slaveof 127.0.0.1 6379
OK
(2) 修改Replication配置
在配置文件中,可以配置主机IP、端口号和主机密码。
# replicaof <masterip> <masterport>
replicaof 127.0.0.1 6379
# masterauth <masterpass>
masterauth 123456
修改完成后,重新启动redis服务,这样就建立了一主二从的关系。
再次通过命令查看相关信息,如下:
> info replication
# Replication
role:master
connected_slaves:2
slave0:ip=127.0.0.1,port=6381,state=online,offset=126,lag=0
slave1:ip=127.0.0.1,port=6380,state=online,offset=126,lag=1
master_replid:8b52394f08cdd677a6c6ce533576e21543a4d7fe
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:126
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:126
注意:
-
主从关系建立后,从机就会被禁止写操作。
-
如果是链式的主从关系,主机宕机后可手动通过命令 slaveof no one 设置新的主机。
-
企业开发中,一般使用哨兵模式,而很少通过手动干预设置新主机。
4. 哨兵模式
Redis的哨兵模式是对所有Redis服务进行监控,每个哨兵需要一个独立的进程。
当一个哨兵监控的主机没有响应时,该哨兵就会认为这台主机已下线,称为主观下线。
该哨兵会通知其他哨兵继续请求这台主机,如果也没有响应,这台主机就会被认为客观下线。
一台主机客观下线后,所有哨兵会基于投票算法选出新的主机。
1)配置哨兵
首先需要创建配置文件sentinel.conf,并配置以下内容:
sentinel monitor myredis 127.0.0.1 6379 1
这行配置的格式为:
sentinel monitor 主机名 主机IP 服务端口号 X
最后的数字X表示当有X个哨兵认为主机下线了,这台主机才算真正下线。
2)启动哨兵
redis-sentinel用于启动哨兵进程,命令如下:
> redis-sentinel sentinel.conf
3)测试
停止端口为6379的redis服务,等待一段时间后,哨兵进程将会执行failover操作,重新选出新的主机。
优点:
- 哨兵集群,基于主从复制模式,所有的主从配置优点,它都有。
- 主从可以切换,故障可以转移,系统的可用性更好。
- 哨兵模式就是主从模式的升级,从手动到自动,更加健壮。
缺点:
- Redis不好在线扩容,集群容量一旦达到上限,在线扩容就十分麻烦。
- 实现哨兵模式的配置特别麻烦,有很多选项。
十三、Redis缓存穿透和雪崩
1. Redis缓存穿透
概念:用户发送的读请求一般会先到Redis缓存服务器,如果缓存中没有,即缓存未命中,用户的读请求会直接访问后台的数据库(如:Mysql),这时候就发生了缓存穿透。
解决方法:
1)布隆过滤器:它是一种数据结构,对所有可能查询的参数以hash形式存储,在控制层进行校验,不符合则丢弃,从而避免了对底层存储系统的查询压力。
2)缓存空对象:当缓存未命中后,即使返回的空对象也缓存起来,同时设置一个过期时间,之后再访问这个数据将会从缓存中获取,保存了后端数据源。
2. 缓存击穿
概念:一个Key非常热点,在不断承担着大并发访问,当这个key在失效的瞬间,持续的高并发就穿破缓存,直接请求数据库,这就是缓存击穿。
解决方法:
1)设置热点key永不过期:从缓存层面来看,没有设置过期时间,所以不会出现热点key过期后产生的问题。
2)加互斥锁:使用分布式锁,保证对于每个key同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只能等待。这种方式将高并发的压力转移到了分布式锁,因此对分布式锁的考验很大。
3. 缓存雪崩
概念:缓存雪崩,是指在某一个时间段,缓存集中过期失效,这会导致数据库承受周期性的波峰式压力。
解决方案:
1)redis高可用:增设多台Redis服务器,搭建Redis集群。
2)限流降级:在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。
3)数据预热:在即将发生高并发访问前,手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量合理。