Redis使用
2021/4/10 java学习日记
一、Redis是什么?
Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。
二、使用步骤
1.启动Rdis服务,指定配置文件启动:
redis-server
2.查看Redis进程是否开启
ps -ef|grep redis
3.关闭Redis
shutdown
三、Redis的常用命令
set get命令,存取值
切换数据库,redis默认有16个数据库,默认使用0号数据库,可以通过
select
切换
flushdb
清空当前数据库flushall
清空所有数据库
keys*
查询所有key
dbsize
查询数据库的大小
exists keyName
查看key是否存在,存在返回1,否则返回0
move keyName 1
移除指定Key
expire keyName timeout
为key设置过期时间
ttl keyName
查看key的剩余存活时间
type keyName
查看key的类型
四、五大数据类型
1.String操作
strlen
:求value的长度,append
:追加
incr
incrby
decr
decrby
自增,自减操作
getrange
,setrange
字符串的范围操作
setex
(如果存在Key则覆盖,不存在则创建)setnx
(如果不存在就设置,不能覆盖) 设置过期时间
mset
,mget
,msetnx
(具有为原子性,要么都成功,要么都失败)批量设置和获取值
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3
OK
127.0.0.1:6379> mget k1 k2 k3
1) "v1"
2) "v2"
3) "v3"
127.0.0.1:6379> mget k1 k2 k4
1) "v1"
2) "v2"
3) (nil)
127.0.0.1:6379> MSETNX k1 v1
(integer) 0
127.0.0.1:6379> MSETNX k1 v1 k4 v4
(integer) 0
127.0.0.1:6379> MSETNX k4 v4 k5 v5
(integer) 1
127.0.0.1:6379> mget k1 k2 k3 k4 k5
1) "v1"
2) "v2"
3) "v3"
4) "v4"
5) "v5"
127.0.0.1:6379>
用
mset
,mget
存取对象
127.0.0.1:6379> mset user:u:name baxxi user:u:age 20
OK
127.0.0.1:6379> mget user:u:name user:u:age
1) "baxxi"
2) "20"
getset
(先获取再设置,如果不存在就返回nil)
127.0.0.1:6379> getset k1 v1
(nil)
127.0.0.1:6379> getset k1 v2
"v1"
127.0.0.1:6379> get k1
"v2"
2.list操作
lpush
在左侧插入lpop
从左侧删除
127.0.0.1:6379> LPUSH list v1 v2 v3
(integer) 3
127.0.0.1:6379> LRANGE list 0 -1
1) "v3"
2) "v2"
3) "v1"
127.0.0.1:6379> LPOP list
"v3"
127.0.0.1:6379> LRANGE list 0 -1
1) "v2"
2) "v1"
rpush
在右侧插入rpop
从右侧删除
127.0.0.1:6379> RPUSH list v1 v2 v3
(integer) 3
127.0.0.1:6379> LRANGE list 0 -1
1) "v1"
2) "v2"
3) "v3"
127.0.0.1:6379> RPOP list
"v3"
127.0.0.1:6379> LRANGE list 0 -1
1) "v1"
2) "v2"
lindex
通过下标获取元素
127.0.0.1:6379> LRANGE list 0 -1
1) "v1"
2) "v2"
127.0.0.1:6379> LINDEX list 1
"v2"
127.0.0.1:6379> LINDEX list 0
"v1"
127.0.0.1:6379>
llen
获取列表长度
127.0.0.1:6379> RPUSH list v1 v2 v3
(integer) 3
127.0.0.1:6379> LLEN list
(integer) 3
lrem
移除具体的值所在的键值对
127.0.0.1:6379> RPUSH list v1 v2 v3 v3 v4
(integer) 5
127.0.0.1:6379> LRANGE list 0 -1
1) "v1"
2) "v2"
3) "v3"
4) "v3"
5) "v4"
127.0.0.1:6379> LREM list 2 v3
(integer) 2
127.0.0.1:6379> LRANGE list 0 -1
1) "v1"
2) "v2"
3) "v4"
ltrim
截取队列 截取的开始下标 截取的结束下标
127.0.0.1:6379> RPUSH list v1 v2 v3 v3 v4 v5
(integer) 6
127.0.0.1:6379> LRANGE list 0 -1
1) "v1"
2) "v2"
3) "v3"
4) "v3"
5) "v4"
6) "v5"
127.0.0.1:6379> LTRIM list 2 4
OK
127.0.0.1:6379> LRANGE list 0 -1
1) "v3"
2) "v3"
3) "v4"
rpoplpush
移除列表中的最后一个元素并将这个元素放到一个新的列表中
127.0.0.1:6379> RPUSH list v1 v2 v3 v4
(integer) 4
127.0.0.1:6379> RPUSH list1 v5 v6
(integer) 2
127.0.0.1:6379> LRANGE list 0 -1
1) "v1"
2) "v2"
3) "v3"
4) "v4"
127.0.0.1:6379> LRANGE list1 0 -1
1) "v5"
2) "v6"
127.0.0.1:6379> RPOPLPUSH list list1
"v4"
127.0.0.1:6379> LRANGE list1 0 -1
1) "v4"
2) "v5"
3) "v6"
127.0.0.1:6379> LRANGE list 0 -1
1) "v1"
2) "v2"
3) "v3"
EXISTS list
判断列表是否存在
127.0.0.1:6379> EXISTS list
(integer) 1
lset
更新列表中指定下标的元素的value,前提是该下标必须有值,否则报错
127.0.0.1:6379> LRANGE list 0 -1
1) "v1"
2) "v2"
3) "v3"
127.0.0.1:6379> LSET list 1 123
OK
127.0.0.1:6379> LRANGE list 0 -1
1) "v1"
2) "123"
3) "v3"
127.0.0.1:6379> LSET list 3 123
(error) ERR index out of range
linsert
在列表中插入值:前插和后插
127.0.0.1:6379> RPUSH list v1 v2 v3 v4
(integer) 4
127.0.0.1:6379> LRANGE list 0 -1
1) "v1"
2) "v2"
3) "v3"
4) "v4"
127.0.0.1:6379> LINSERT list before v1 hello
(integer) 5
127.0.0.1:6379> LRANGE list 0 -1
1) "hello"
2) "v1"
3) "v2"
4) "v3"
5) "v4"
127.0.0.1:6379> LINSERT list after v2 baxxi
(integer) 6
127.0.0.1:6379> LRANGE list 0 -1
1) "hello"
2) "v1"
3) "v2"
4) "baxxi"
5) "v3"
6) "v4"
3.set操作
set:无序不重复的集合
sadd
添加
127.0.0.1:6379> sadd set v1 v2 v3 v4
(integer) 4
smembers
查看所有
127.0.0.1:6379> SMEMBERS set
1) "v3"
2) "v1"
3) "v4"
4) "v2"
scard
查询set的元素个数
127.0.0.1:6379> SCARD set
(integer) 4
sismember
判断set中是否存在某元素
127.0.0.1:6379> SISMEMBER set v3
(integer) 1
127.0.0.1:6379> SISMEMBER set v5
(integer) 0
srem
移除某个元素
127.0.0.1:6379> SREM set v3 v2
(integer) 2
127.0.0.1:6379> SMEMBERS set
1) "v1"
2) "v4"
SRANDMEMBER
随机抽取一个元素
127.0.0.1:6379> clear
127.0.0.1:6379> SADD set v1 v2 v3 v4 v5
(integer) 5
127.0.0.1:6379> SRANDMEMBER set
"v2"
127.0.0.1:6379> SRANDMEMBER set
"v3"
127.0.0.1:6379> SRANDMEMBER set
"v3"
127.0.0.1:6379> SRANDMEMBER set 2
1) "v5"
2) "v1"
127.0.0.1:6379> SRANDMEMBER set 2
1) "v2"
2) "v3"
spop
随机移除一个元素
127.0.0.1:6379> SMEMBERS set
1) "v2"
2) "v1"
3) "v3"
4) "v5"
5) "v4"
127.0.0.1:6379> SPOP set
"v1"
127.0.0.1:6379> SMEMBERS set
1) "v2"
2) "v3"
3) "v5"
4) "v4"
127.0.0.1:6379> SPOP set 2
1) "v2"
2) "v4"
127.0.0.1:6379> SMEMBERS set
1) "v3"
2) "v5"
smove
将一个集合中的元素移动到另一个集合中
127.0.0.1:6379> SMEMBERS set1
1) "v3"
2) "v1"
3) "v2"
127.0.0.1:6379> SMEMBERS set2
1) "v5"
2) "v6"
3) "v4"
127.0.0.1:6379> SMOVE set1 set2 v1
(integer) 1
127.0.0.1:6379> SMOVE set2 set1 v6
(integer) 1
127.0.0.1:6379> SMEMBERS set1
1) "v3"
2) "v6"
3) "v2"
127.0.0.1:6379> SMEMBERS set2
1) "v1"
2) "v5"
3) "v4"
sinter
补集
127.0.0.1:6379> SMEMBERS set1
1) "v3"
2) "v1"
3) "v2"
127.0.0.1:6379> SMEMBERS set2
1) "v3"
2) "v4"
3) "v2"
127.0.0.1:6379> SDIFF set1 set2
1) "v1"
127.0.0.1:6379> SDIFF set2 set1
1) "v4"
sunion
并集
127.0.0.1:6379> SUNION set1 set2
1) "v1"
2) "v3"
3) "v4"
4) "v2"
sinter
交集
127.0.0.1:6379> SINTER set1 set2
1) "v3"
2) "v2"
4.hash操作
hash的形式:key filed vlaue,相当于java中的map集合
hset
向hash中添加值
127.0.0.1:6379> hset user name baxxi age 20 addr guangzhou
(integer) 3
hgetall
得到hash中的指定filed的值
127.0.0.1:6379> hget user name
"baxxi"
127.0.0.1:6379> hget user age
"20"
127.0.0.1:6379> hget user addr
"guangzhou"
hgetall
得到hash中的所有值
127.0.0.1:6379> HGETALL user
1) "name"
2) "baxxi"
3) "age"
4) "20"
5) "addr"
6) "guangzhou"
hmset
hmget
批量添加和获取
127.0.0.1:6379> HMSET user name baxxi age 20
OK
127.0.0.1:6379> HMGET user name age
1) "baxxi"
2) "20"
hdel
删除指定的字段
127.0.0.1:6379> HDEL user age
(integer) 1
127.0.0.1:6379> HMGET user name age
1) "baxxi"
2) (nil)
127.0.0.1:6379> HDEL user name
(integer) 1
127.0.0.1:6379> HMGET user name age
1) (nil)
2) (nil)
hlen
获取hash的字段长度
127.0.0.1:6379> HSET user name baxxi age 20 addr guangzhou
(integer) 3
127.0.0.1:6379> HLEN user
(integer) 3
hexist
判断hash中的字段是否存在
127.0.0.1:6379> HEXISTS user name
(integer) 1
127.0.0.1:6379> HEXISTS user aaa
(integer) 0
hkeys
获取所有的key(字段),hvals
,获取所有的value
127.0.0.1:6379> HKEYS user
1) "name"
2) "age"
3) "addr"
127.0.0.1:6379> HVALS user
1) "baxxi"
2) "20"
3) "guangzhou"
hincrby
指定增量
127.0.0.1:6379> HINCRBY user age 1
(integer) 21
127.0.0.1:6379> HINCRBY user age 1
(integer) 22
127.0.0.1:6379> HINCRBY user age 2
(integer) 24
5.zset操作
zset是有序不可重复的集合
zadd
添加数据
127.0.0.1:6379> zadd num 20 a
(integer) 1
127.0.0.1:6379> zadd num 30 b
(integer) 1
127.0.0.1:6379> zadd num 90 c
(integer) 1
zrange
获取所有值,按照索引获取
127.0.0.1:6379> zrange num 0 -1 withscores
1) "a"
2) "20"
3) "b"
4) "30"
5) "c"
6) "90"
127.0.0.1:6379> zrange num 0 -1
1) "a"
2) "b"
3) "c"
zrangebyscore
获取所有值,按照score获取
127.0.0.1:6379> ZRANGEBYSCORE num -inf +inf withscores
1) "a"
2) "20"
3) "b"
4) "30"
5) "c"
6) "90"
ZREVRANGE
倒序获取
127.0.0.1:6379> ZREVRANGE num 0 -1
1) "c"
2) "b"
3) "a"
127.0.0.1:6379> ZREVRANGE num 0 -1 withscores
1) "c"
2) "90"
3) "b"
4) "30"
5) "a"
6) "20"
zrem
移除指定元素
127.0.0.1:6379> ZREM num a
(integer) 1
127.0.0.1:6379> ZREVRANGE num 0 -1 withscores
1) "c"
2) "90"
3) "b"
4) "30"
zcard
得到元素的个数
zcount
获取指定区间的成员数量
127.0.0.1:6379> ZCARD num
(integer) 2
127.0.0.1:6379> ZCOUNT num 20 40
(integer) 1
五、三种特殊数据类型
Geospatial (地理空间)
应用场景:
- 朋友的定位、附近的人、打车的距离计算.
- Redis的Geospatial在Redis3.2版本就推出了!这个功能可以推算地理位置的信息,两地之间的距离.
GEOADD
将指定的地理空间位置(纬度、经度、名称)添加到指定的key
中。
# getadd key 经度 纬度 名称
# 规则:两极无法添加,有效的经度从-180度到180度。 有效的纬度从-85.05112878度到85.05112878度。
# 一般会下载城市数据,直接通过Java程序一次性导入!
> geoadd china:city 116.23128 40.22077 beijing #导入城市数据
1
> geoadd china:city 121.48941 31.40527 shanghai
1
> geoadd china:city 106.54041 29.40268 chongqing
1
> geoadd china:city 113.88308 22.55329 shenzheng
1
> geoadd china:city 120.21201 30.2084 hangzhou
1
> geoadd china:city 108.93425 34.23053 xian
1
GEOPOS
获取指定地址的位置,一定是一个坐标值
> geopos china:city beijing #获取指定地址的经度和纬度
116.23128265142441
40.220769054385265
> geopos china:city beijing chongqing #一次性获取多个城市的经纬度
116.23128265142441
40.220769054385265
106.54040783643723
29.402680535172998
GEODIST
返回两个给定位置之间的距离
# 如果用户没有显式地指定单位参数, 那么 GEODIST 默认使用米作为单位。
> geodist china:city beijin shanghai #北京和上海之间的直线距离(默认单位为米)
1088644.3544
> geodist china:city beijin shanghai km #指定单位为km
1088.6444
GEORADIUS
以给定的经纬度为中心, 找出某一半径内的元素
> georadius china:city 110 30 1000 km #获取经度110 纬度30 方圆半径100km的所有城市(人)
chongqing
xian
shenzheng
hangzhou
> georadius china:city 110 30 500 km #获取经度110 纬度30 方圆半径500km的所有城市(人)
chongqing
xian
> georadius china:city 110 30 500 km withdist #指定坐标方圆半径500km的所有城市(人) 并带上距离
chongqing
340.7667
enshi
57.7962
xian
481.1278
> georadius china:city 110 30 500 km withdist count 1 #指定坐标半径500km内所有城市(人)限制返回个数
enshi
57.7962
> georadius china:city 110 30 500 km withdist count 2 #限制2个
enshi
57.7962
chongqing
340.7667
GEORADIUSBYMEMBER
找出位于指定范围内的元素,中心点是由给定的位置元素决定
> GEORADIUSBYMEMBER china:city beijin 1000 km #找出北京为中心点半径1000km之内的城市
beijin
xian
> GEORADIUSBYMEMBER china:city beijin 1000 km withdist #找出北京为中心点半径1000km之内的城市显示距离
beijin
0.0000
xian
927.5371
GEOHASH
返回一个或多个位置元素的 Geohash 表示
Hyperloglog (基数统计)
基数:不重复的元素!可以接受误差
- 优点:占用的内存是固定的,2^64不同的元素的基数,只需要12kb内存!如果要从内存角度来比较的话那么Hyperloglog首选!
- 缺点:有0.81%的错误率,一般可以忽略不记
业务场景
- 网站的UV(一个人访问一个网站多次,但是还是算作一个人!)
- 传统方式:set保存用户的id,然后就可以统计set中的元素数量作为标准判断!
- 这个方式如果保存大量的用户id,就会比较麻烦!我们的目的是为了计数,而不是保存用户id
pfadd
添加数据
> pfadd key1 a b c a d e f #添加7个带有重复值的数据
1
pfcount
输出不重复的元素个数
> pfcount key1 #输出不重复的元素个数,上面数据a重复所以结果为6
6
pfmerge
将key1
和key2
的内容合并到key3
> pfadd key2 s k w a r l e #添加第2个数据
1
> pfcount key2
7
> pfmerge key3 key1 key2 #将mykey1 和 mykey2 的内容合并到mykey3
OK
> pfcount key3 # a b c d e f k l r s w
11
Bitmap (位图场景详解)
使用场景
- 统计疫情感染人数:先全部14亿人全部置为0(内存小),如果不幸感染就变成1
- 活跃用户标记
- 登录和未登录
- 用户365天打卡
- 两个状态的都可以使用Bitmaps,都是操作二进制位来进行记录,只有0和1两个状态
setbit
getbit
bitcount
> setbit sign 0 1 # 周一 打卡了
0
> setbit sign 1 0 # 周二 没打卡
0
> setbit sign 2 0 # 周三 没打卡
0
> setbit sign 3 0 # 周四 没打卡
0
> setbit sign 4 0 # 周五 没打卡
0
> setbit sign 5 0 # 周六 没打卡
0
> setbit sign 6 0 # 周日 没打卡
0
############# 任务:查看某天是否打卡 #############
> getbit sign 3 # 周四 没打卡
0
> getbit sign 0 # 周一 打卡了
1
############# 任务:查看打卡的天数 #############
> bitcount sign # 打卡了一天,查看数字就可以知道是否全勤
1
六、事务
MySQL:ACID原则
ACID是关系型数据库系统采纳的原则,其代表的含义分别是:
- 原子性(Atomicity):是指一个事务要么全部执行,要么完全不执行。
- 一致性(Consistency): 事务在开始和结束时,应该始终满足一致性约束。比如系统要求A+B=100,那么事务如果改变了A的数值,则B的数值也要相应修改来满足这样一致性要求;与CAP中的C代表的含义是不同的。
- 事务独立(Isolation):如果有多个事务同时执行,彼此之间不需要知晓对方的存在,而且执行时互不影响,事务之间需要序列化执行,有时间顺序。
- 持久性(Durability):事务的持久性是指事务运行成功以后,对系统状态的更新是永久的,不会无缘无故回滚撤销。
- Redis 单条命令是保证原子性的 但是Redis事务不保证原子性
- Redis 事务没有隔离级别的概念
- 所有的命令在事务中,并没有直接被执行!只有发起执行命令的时候才会执行
- redis的事务:
- 开启事务(
multi
) - 命令入队( )
- 执行事务(
exec
)
- 开启事务(
正常执行事务
> multi # 开启事务
OK
###### 命令入队 #####
> set k1 v2
QUEUED
> set k2 v2
QUEUED
> get k2
QUEUED
####### 执行事务 #######
> exec # 执行事务,按照入队顺序一步一步执行,执行一次之后事务就结束了,如果需要重新执行事务需要重新开启,命令入队操作
OK
OK
v2
取消事务
127.0.0.1:6379> multi #开启事务
OK
###### 命令入队 #####
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
####### 取消事务 #######
127.0.0.1:6379> discard #取消事务后之前队内所有的命令都不会被执行了
OK
127.0.0.1:6379> get k2
(nil)
编译型异常(命令有错!),事务中所有的命令都不会被执行!
127.0.0.1:6379> multi #开启事务
OK
###### 命令入队 #####
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> getset k3
(error) ERR wrong number of arguments for 'getset' command #这里因为命令有错,但是不会中断事务,所以下面可以继续写
127.0.0.1:6379> set k4 v4
QUEUED
127.0.0.1:6379> set k5 v5
QUEUED
127.0.0.1:6379> exec #执行事务
(error) EXECABORT Transaction discarded because of previous errors. #执行事务报错,所有的命令都不会被执行
127.0.0.1:6379> get k5 #获取值为nil
(nil)
运行时异常,如果事务队列中存在语法性错误,命令执行的时候,其他命令是可以正常执行的,错误命令抛出异常
127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379> set k1 "v1" #设置字符串值
QUEUED
127.0.0.1:6379> incr k1 #字符串值自增,会报错
QUEUED
127.0.0.1:6379> set k2 v2 #接着执行
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> exec
1) OK
2) (error) ERR value is not an integer or out of range #字符串不是整形不能自增,有异常!但是不影响其他事务!
3) OK
4) OK
127.0.0.1:6379> get k2 #依旧可以获取到值
"v2"
监视 Watch (相当于乐观锁)
乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,只在更新的时候会判断一下在此期间别人有没有去更新这个数据。
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞,直到它拿到锁。
乐观锁测试
127.0.0.1:6379> set money 100 #设置零花钱为100
OK
127.0.0.1:6379> set out 0 #设置已经花出去的钱为0
OK
127.0.0.1:6379> watch money #监视money,事务执行成功之后,监控会自动取消
OK
127.0.0.1:6379> multi #开启事务,事务正常结束,数据期间没有发生变动,这个时候就正常执行成功
OK
##### 命令入队 #####
127.0.0.1:6379> decrby money 20
QUEUED
127.0.0.1:6379> incrby out 20
QUEUED
127.0.0.1:6379> exec #执行事务
1) (integer) 80
2) (integer) 20
测试多线程修改值,使用watch可以当作redis的乐观锁操作:
线程1去执行事务,在执行命令之前线程2修改了原来的money,导致线程1事务执行失败
#线程1
127.0.0.1:6379> watch money #监控money
OK
127.0.0.1:6379> multi #开启事务
OK
127.0.0.1:6379> decrby money 10 #money减10
QUEUED
127.0.0.1:6379> incrby out 10 #out加10
QUEUED
127.0.0.1:6379> exec #执行事务失败(在此步之前有线程2修改了money的值)
(nil)
#线程2
127.0.0.1:6379> get money #线程1事务执行之前,获取money的值为80
"80"
127.0.0.1:6379> set money 1000 #线程1事务执行之前,修改money的值为1000
OK
线程1拿到的money是80,执行事务操作之后得到的结果应该是70,但是与线程2修改的1000比较,两个值不一致,导致事务执行失败
如果事务执行失败则重新监视获取最新值即可,unwatch先解锁,然后再次监视,再去创建、执行事务
127.0.0.1:6379> unwatch #解锁
127.0.0.1:6379> watch money #再次开启监视
OK
127.0.0.1:6379> multi #再次开启事务
OK
127.0.0.1:6379> decrby money 10
QUEUED
127.0.0.1:6379> incrby out 10
QUEUED
127.0.0.1:6379> exec
1) (integer) 990
2) (integer) 30
七、Jedis使用步骤
Jedis 是 Redis 官方首选的 Java 客户端开发包
1、idea创建maven项目
2、导入jedis、fastjson依赖包
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.71</version>
</dependency>
3、启动Redis服务
4、测试连接
public class TestPing {
public static void main(String[] args) {
//1 new Jedis 对象
Jedis jedis=new Jedis("127.0.0.1",6379);
//jedis所有的命令就是之前学习的所有指令
System.out.println(jedis.ping());
}
}
2021/4/12 java学习日记
一、SpringBoot集成Redis
1、创建SpringBoot项目
2、选好相关依赖
3、编写配置文件application.properties
# 应用名称
spring.application.name=redis-02-springboot411
# 应用服务 WEB 访问端口
server.port=8080
# SpringBoot 所有配置类 都是一个自动配置类 RedisAutoConfiguration
# 自动配置类都会绑定一个properties配置文件 RedisProperties
spring.redis.host=127.0.0.1
spring.redis.port=6379
jedis:采用的直连,多个线程操作的话,是不安全的,如果要避免不安全的,使用jedis pool连接池,更像BIO模式
lettuce:采用netty,实例可以在多个线程中进行共享,不存在线程不安全的情况!可以减少线程数据了,更像NIO模式
4、观察源码
在导入的依赖中的spring.factories文件可以看到有三个Redis的配置类
其中RedisAutoConfiguration中有
@Bean
@ConditionalOnMissingBean(
name = {"redisTemplate"}
)//我们可以自己定义一个redistemplate来替换这个默认的!
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
//默认的redistemplate没有过多的设置,redis对象都需要序列化
//两个泛型都是object,object的类型,我们使用之后需要强制转换<string,object>
RedisTemplate<Object, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean//由于string是redis中最常用的类型,所以就单独提出来一个bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
我们也可以在自己的config文件夹中创建一个RedisConfig类来自定义一个RedisTemplate:
@Configuration
public class RedisConfig {
@Bean(name = "redisTemplate")
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
//为了方便 直接使用 string object
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
//使用json序列化数据
Jackson2JsonRedisSerializer<Object> json = new Jackson2JsonRedisSerializer<Object>(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
json.setObjectMapper(mapper);
//String的序列化
StringRedisSerializer str = new StringRedisSerializer();
//key采用string的序列化形式
template.setKeySerializer(str);
//hash的key采用string的序列化形式
template.setHashKeySerializer(str);
//value采用json的序列化形式
template.setValueSerializer(json);
//hash的value采用json的序列化形式
template.setHashValueSerializer(json);
template.afterPropertiesSet();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
5、进行测试
首先创建一个pojo:
@Component
@Data
@NoArgsConstructor
@AllArgsConstructor
//在企业中 所有的pojo都要序列化 implements Serializable
public class User implements Serializable {
private String name;
private Integer age;
}
测试一下
@Autowired
@Qualifier("redisTemplate")
private RedisTemplate redisTemplate;
@Test
void test() throws JsonProcessingException {
//redisTemplate 操作不同的数据类型 api和指令一样
//opsForValue 操作字符串 类似String
//opsForList 操作list 类似list
//...
//获取redis的连接对象
// RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
// connection.flushDb();
// connection.flushAll();
//真实开发一般都使用json来传递对象
User user = new User("baxxi", 20);
// String jsonUser = new ObjectMapper().writeValueAsString(user);
redisTemplate.opsForValue().set("user",user);
System.out.println(redisTemplate.opsForValue().get("user"));
}
输出结果:
6、代码优化
由于RedisTemplate类中的方法比较繁琐,为了便利,对RedisTemplate进行了封装,为Redis创建一个工具类RedisUtil。
因为来代码比较长,所以这里就给到链接 RedisUtils工具类
再来测试一下:
@Autowired
private RedisUtil redisUtil;
@Test
void test1() throws JsonProcessingException {
redisUtil.set("name","baxxi");
System.out.println(redisUtil.get("name"));
}
输出结果:
二、Redis.conf详解
网络
bind 172.0.0.1 #绑定的ip
protected-mode yes #保护模式一般是开启的
port 6379 #redis的默认端口
快照
持久化,在规定的时间内,执行了多少次,则持久化到文件.rdp .aof
持久化规则
#如果在900s内,如果至少1个key进行了数据修改,进行数据持久化操作
save 900 1
#如果在300s内,如果至少10个key进行了数据修改,进行数据持久化操作
save 300 10
#如果在60s内,如果至少10000个key进行了数据修改,进行数据持久化操作
save 60 10000
stop-writes-on-bgsave-error yes #持久化如果出错,是否还继续工作
rdbcompression yes #是否压缩rdb文件,需要消耗一点cpu资源
rdbchecksum yes #保存rdb文件的时候,进行错误检查校验!
dir ./ #rdb文件保存的目录!
SECURITY 安全
requirepass #设置redis认证auth
#也可以通过命令去设置密码
config set requirepass #设置密码
config get requirepass #获得密码
CLIENTS 客户端限制
maxclients 10000#最大客户端连接数
maxmemory <bytes> #设置redis配置内存的最大容量
maxmemory-policy noeviction #内存到达上限之后的处理策略
maxmemory-policy 六种方式
1、volatile-lru:只对设置了过期时间的key进行LRU(默认值)
2、allkeys-lru : 删除lru算法的key
3、volatile-random:随机删除即将过期key
4、allkeys-random:随机删除
5、volatile-ttl : 删除即将过期的
6、noeviction : 永不过期,返回错误
APPEND ONLY MODE AOF配置
appendonly no #默认是不开启aof的,默认是使用rdb方式持久化的。
appendfilename "appendonly .aof" #持久化文件的名字
#appendfsync always #每次修改都会sync.消耗慢
appendfsync everysec #每一秒执行一次sync,可能会丢失这1秒的数据
#appendfsync no #不执行sync,这个时候操作系统自己同步数据
三、Redis持久化
RDB(Redis DataBase)
Redis是内存数据库,如果不将内存中的数据库状态保存到磁盘,那么一旦服务进程退出,服务器中的数据库状态也会消失。所以Redis提供了持久化功能!
RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘。也是默认的持久化方式,这种方式是就是将内存中数据以快照的方式写入到二进制文件中,默认的文件名为dump.rdb
RDB触发规则:
- save规则满足的情况下,会自动触发rdb规则
- 使用flushall命令也会触发rdb规则
- 退出redis,也会产生rdb文件
备份自动生成一个dump.rdb文件
优点:
- 适合大规模的数据恢复
- 对数据完整性要求不高
缺点:
- 需要一定的时间间隔进程操作!如果redis意外宕机了,这个最后一次修改的数据就没有了
- fork进程的时候,会占用一定的内存空间
AOF(Append Only File)
全量备份总是耗时的,有时候我们提供一种更加高效的方式AOF,工作机制很简单,redis会将每一个收到的写命令都通过write函数追加到文件中。通俗的理解就是日志记录。
AOF保存的是appendonly.aof文件,每当有一个写命令过来时,就直接保存在我们的AOF文件中。
Redis.conf文件中
appendonly no #默认是不开启aof的,默认是使用rdb方式持久化的。改为yes开启aof持久化
修改配置文件后需要重启服务!
如果aof文件有错误,那么Redis是启动不起来的,我们需要修复这个文件,redis给我们提供了一个工具redis-check-aof --fixe appendonly.aof
来修复aof文件,,aof默认就是文件无线追加,文件会越来越大
优点:
- 每次修改都会同步,文件的完整性会更好。
- 每秒同步一次,可能会丢失一秒的数据。
- 可以不同步,效率最高
缺点:
- 相对于数据文件来说,aof文件远大于rdb文件,修复的速度会比rdb文件慢
- aof运行效率也要比rdb慢,所以Redis默认的配置就是rdb持久化。
四、Redis发布订阅
Redis 发布订阅 (pub/sub) 是一种消息通信模式:发送者 (pub) 发送消息,订阅者 (sub) 接收消息。
Redis 客户端可以订阅任意数量的频道。
命令及描述:
- PSUBSCRIBE pattern [pattern …] 订阅一个或多个符合给定模式的频道。
- PUBSUB subcommand [argument [argument …]] 查看订阅与发布系统状态。
- PUBLISH channel message 将信息发送到指定的频道。
- PUNSUBSCRIBE [pattern [pattern …]] 退订所有给定模式的频道。
- SUBSCRIBE channel [channel …] 订阅给定的一个或多个频道的信息。
- UNSUBSCRIBE [channel [channel …]] 指退订给定的频道。
订阅者:
127.0.0.1:6379> SUBSCRIBE baxxi
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "baxxi"
3) (integer) 1
1) "message"
2) "baxxi"
3) "hello"
1) "message"
2) "baxxi"
3) "hello,redis"
发生者:
127.0.0.1:6379> PUBLISH baxxi hello
(integer) 1
127.0.0.1:6379> PUBLISH baxxi hello,redis
(integer) 1
五、Redis主从复制
从主复制,是指从一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master/leader),后者称为从节点(slave/follower);数据的复制是单向的,只能有主节点到从节点。Master以写为主,Slave以读为主。
默认情况下,每台Redis服务器都是主节点;且一个主节点可以有多个从节点或(没有从节点),但是从节点只能有一个主节点。
主从复制的作用主要包括:
- 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
- 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。
- 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读- Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
- 高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。
info replication
:查看复制信息
127.0.0.1:6379> INFO replication
# Replication
role:master //角色是主机
connected_slaves:0 //从机数量为0
master_failover_state:no-failover
master_replid:8953f6c3aac3b0269abf23e7aa2248a7bbc70f12
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
单机集群环境搭建
复制多个配置文件,修改对应的端口号然后启动服务。
redis.conf修改点:
- port
- pid进程号
- log名字
- dump.rdb
分别通过配置文件,启动对应的服务
默认情况下每一台主机都是主节点,只需要在从机上配置,认老大
从机1:
127.0.0.1:6380> SLAVEOF 127.0.0.1 6379
OK
127.0.0.1:6380> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6379
master_link_status:up
master_last_io_seconds_ago:4
master_sync_in_progress:0
slave_repl_offset:0
slave_priority:100
slave_read_only:1
connected_slaves:0
master_failover_state:no-failover
master_replid:2dd348bda51387cc1555a1baca82a5731694f3da
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:0
从机2:
127.0.0.1:6381> SLAVEOF host 6379
OK
127.0.0.1:6381> info replication
# Replication
role:slave
master_host:host
master_port:6379
master_link_status:down
master_last_io_seconds_ago:-1
master_sync_in_progress:0
slave_repl_offset:0
master_link_down_since_seconds:1618192855
slave_priority:100
slave_read_only:1
connected_slaves:0
master_failover_state:no-failover
master_replid:ad3d8aa6966a4b6205b868eca475027b18de8622
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
主机master:
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:2
slave0:ip=127.0.0.1,port=6380,state=online,offset=196,lag=0
slave1:ip=127.0.0.1,port=6381,state=online,offset=196,lag=0
master_failover_state:no-failover
master_replid:2dd348bda51387cc1555a1baca82a5731694f3da
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:196
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:196
复制原理:
Slave启动成功连接到master后会发送一个sync命令
Master接到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕后,master将传送整个文件到slave,并完成一次同步。
- 全量复制:而slave服务在接受到数据库文件数据后,将其存盘并加载到内存中。
- 增量复制:Master继续将新的所有收集到的修改命令依次传给slave,完成同步,但是只要重新连接master,一次完全同步(全量复制)将被自动执行
六、哨兵模式(自动选取主节点)
vim sentinel.conf
编写配置文件
sentinel monitor [监视名称] [host] [port] 1
#后面这个1代表投票选举主机
redis-sentinel redis-config/sentinel.conf
启动哨兵模式
哨兵模式的主要配置:
# Example sentinel.conf
# 哨兵sentinel实例运行的端口 默认26379
port 26379
# 哨兵sentinel的工作目录
dir /tmp
# 哨兵sentinel监控的redis主节点的ip port
# master-name 可以自己命名的主节点 只能由字母A-z,数字0-9,这三个字符".-_"组成。
# quorum 配置多少个sentinel哨兵统一认为master节点失联 那么这是客观上认为主节点失联了
# sentinel monitor <master-name> <ip> <redis-port> <quorum>
sentinel monitor mymaster 127.0.0.1 6379 2
#当在redis实例中开启了reuirepass foobared 授权密码 这样所有连接redis实例的客户端都要提供密码
#设置哨兵sentinel连接主从的密码 注意必须为主从设置一样的验证密码
# sentinel auth-pass <master-name> <password>
sentinel auth-pass mymaster 123456
#指定多少毫秒之后 主节点没有答应哨兵sentinel此时 哨兵主观上认为主节点下线 默认30秒
# sentinel down-after-milliseconds <master-name> <milliseconds>
sentinel down-after-milliseconds mymaster 30000
#这个配置指定了在发生failover主备切换时最多有多少个slave同时对新的master进行 同步。
这个数字越小,完成failover所需要的时间越长
但是如果这个数字越大,就意味越多的slave因为replication而不可用
可以通过这个值设为1 来保证每次只有一个slave处于不能处理命令请求的状态
#sentinel parallel-syncs <master-name> <numslave>
sentinel parallel-syncs mymaster 1
#故障转移的超时时间failover-timeout 可以用在以下这些方面:
#1.同一个sentinel对同一个master两次failover之间的间隔时间。
#2.当一个slave从一个错误master那里同步开始时间,直到slave被纠正为向正确的master那里同步数据时。
#3.当想要取消一个正在进行failover所需要的时间
#4.当进行failover时,配置所有slave指向新的master所需的最大时间,不过,即使过了这个超时,slaves依然会被正确配置为指向master,但是就不按parallel-syncs所配置的规则来了
# 默认三分钟
# sentinel failover-timeout <master-name> <milliseconds>
setinel failover-timeout mymaster 180000
#scripts execution
#配置当某一事件发生时所需要执行的脚本,可以通过脚本来通知管理员,例如当系统运行不正常时发邮件通知相关人员
#对于脚本的运行结果有以下规则:
#若脚本执行后返回1,那么该脚本稍后将会被再次执行,重复次数目前默认10
#若脚本执行后返回2,或者比2更高的一个返回值,脚本将不会重复执行
#若脚本执行过程中由于受到系统中断信号被终止了,则同返回值为1时的行为相同
#一个脚本的最大执行时间为60s,如果超过这个时间,脚本将会被sigkill信号终止,之后重新执行。
#通知型脚本:当sentinel有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等),将会上调用这个脚本,这时这个脚本应该通过邮件,sms等方式去通知系统管理员关于系统不正常运行的信息,调用该脚本时,将传给脚本两个参数,一个是事件的类型,一件是事件的描述,如果sentinel.conf配置文件中配置了这个脚本路径,那么必须保证这个脚本存在于这个路径,并且是可执行的,否则sentinel无法正常启动成功。
#通知脚本
#sentinel notification-script <master-name> <script-path>
sentinel notification-script mymaster /var/redis/notify.sh
#客户端重新配置主节点参数脚本
#当一个master由于failover而发生改变是,这个脚本将会被调用,通知相关客户端关于master地址已经发生改变的信息。
#以下参数将会在调用脚本时传给脚本:
#<master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port>
#目前<state>总是"failover",
#<role>是"leader"或者"observer"中的其中一个。
#参数 from-ip,from-port,to-ip,to-port是用来和旧的master和新的master(即旧的slave)通信的
#这个脚本应该是通用的,能多次调用,不是针对性的。
#sentinel client-reconfig-script <master-name> <script-path>
sentinel client-reconfig-script mymaster /var/redis/reconfig.sh
七、Redis缓存穿透和雪崩
1.缓存穿透:key对应的数据在数据源并不存在,每次针对此key的请求从缓存获取不到,请求都会到数据源,从而可能压垮数据源。比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。
解决方法
- 设置布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从避免了对底层存储系统的查询压力。
- 如果一个查询返回的数据为空,不管是数据不存在还是系统故障,我们仍然把这个结果进行缓存,但是它的过期时间会很短最长不超过5分钟。
2.缓存击穿:key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
3.缓存雪崩:当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如DB)带来很大压力。
解决方法
- 设置redis集群和DB集群的高可用,如果redis出现宕机情况,可以立即由别的机器顶替上来。这样可以防止一部分的风险。
- 使用互斥锁:在缓存失效后,通过加锁或者队列来控制读和写数据库的线程数量。比如:对某个key只允许一个线程查询数据和写缓存,其他线程等待。单机的话,可以使用synchronized或者lock来解决,如果是分布式环境,可以是用redis的setnx命令来解决。
- 不同的key,可以设置不同的过期时间,让缓存失效的时间点不一致,尽量达到平均分布。
- 永远不过期
redis中设置永久不过期,这样就保证了,不会出现热点问题,也就是物理上不过期。 - 资源保护:使用netflix的hystrix,可以做各种资源的线程池隔离,从而保护主线程池。
总结
经过这几天对Redis的学习,对Redis有了一个新的了解,现在还处于入门阶段,对于Redis的底层还是需要多多去探究。
学习路径:狂神Redis