Redis是一个开源(BSD许可),内存存储的数据结构服务器,可用作数据库,高速缓存和消息队列代理。它支持字符串、哈希表、列表、集合、有序集合,位图,hyperloglogs等数据类型。内置复制、Lua脚本、LRU收回、事务以及不同级别磁盘持久化功能,同时通过Redis Sentinel提供高可用,通过Redis Cluster提供自动分区。
基础命令
redis-cli -h localhost -p 6379 # 客户端连接服务端
localhost:6379> keys * # 查看当前数据库所有key
1) "mykey"
2) "18:45"
localhost:6379> select 0 # 选择数据库index,默认config中redis一旦启动有16个库,不选择则自动选择0号库
OK
localhost:6379> DBSIZE # 查看当前数据库中有多少key
(integer) 2
localhost:6379> exists mykey # 查看当前数据库中是否存在该key
(integer) 1
localhost:6379> ttl mykey # 查看key的倒计时,-1代表永久
(integer) -1
localhost:6379> expire mykey 5 # 给key设置过期时间
(integer) 1
localhost:6379> ttl mykey # -2代表已过期
(integer) -2
localhost:6379> type mykey # 查看key的数据类型
none
localhost:6379> type 18:45 # 查看key的数据类型
string
localhost:6379> move 18:45 1 # 移动key去对应index的库
Redis基本数据结构
五大基本数据结构 | 三大特殊类型 |
---|---|
String(字符串) | Geospatial(地理位置) |
List(列表) | Hyperloglog(基数统计) |
Set(无序集合) | Bitmap(位图) |
ZSet(有序集合) | |
Hash(哈希) |
String(字符串)
- String可以存储字符串和数字!
- 计数器。例如:浏览量
- 统计多单位数量。例如:user的多个维度属性
############################################################################################
localhost:6379> set key1 va # 设置值
OK
localhost:6379> get key1 # 获取值
"va"
localhost:6379> append key1 "hello" # 字符串追加,如果不存在则相当于set,返回追加后的字符串长度
(integer) 7
localhost:6379> get key1
"vahello"
localhost:6379> strlen key1 # 获取key的长度
(integer) 7
############################################################################################
localhost:6379> set views 0
OK
localhost:6379> get views
"0"
localhost:6379> incr views # 自增+1
(integer) 1
localhost:6379> incr view # 自增一个不存在的key,默认从0变成1
(integer) 1
localhost:6379> get view
"1"
localhost:6379> INCRBY views 100 # 设置增加步长为100
(integer) 101
localhost:6379> DECRBY view 200 # 设置减少步长为100
(integer) -198
localhost:6379> DECRBY views 200
(integer) -99
############################################################################################
【截取字符串】
localhost:6379> set key1 wangzichen
OK
localhost:6379> get key1
"wangzichen"
localhost:6379> GETRANGE key1 0 -1
"wangzichen"
localhost:6379> GETRANGE key1 0 3
"wang"
localhost:6379> SETRANGE key1 3 shuaige # 从第N个索引位置改变字符串
(integer) 10
localhost:6379> get key1
"wanshuaige"
############################################################################################
【设置过期时间】
# 为指定的 key 设置值及其过期时间。如果 key 已经存在, SETEX 命令将会替换旧的值。
localhost:6379> setex key2 30 wangzichen
OK
localhost:6379> ttl key2
(integer) 28
# 如果不存在则设置成功,可以用作分布式锁
localhost:6379> setnx key3 wangzichen
(integer) 1
localhost:6379> setnx key3 wangzichen
(integer) 0
############################################################################################
【批量设置】
localhost:6379> mset k1 v1 k2 v2 k3 v3
OK
localhost:6379> keys *
1) "k1"
2) "k3"
3) "k2"
localhost:6379> get k3
"v3"
localhost:6379> mget k1 k2 k3
1) "v1"
2) "v2"
3) "v3"
# 此处设置不成功,虽然k6不存在,但因为k1已经存在,导致k6也没有设置成功,msetnx是原子操作
localhost:6379> msetnx k1 v1 k6 v6
(integer) 0
localhost:6379> get k6
(nil)
############################################################################################
【保存对象】
localhost:6379> set user:1 {name:wangzichen,age:24}
OK
localhost:6379> get user:1
"{name:wangzichen,age:24}"
# user:{id}:{field}
# 这种方法虽然写法复杂了,但是获取值的时候不需要格式化了,视情况选择
localhost:6379> mset user:2:name wangzichen user:2:age 24
OK
localhost:6379> get user:2
(nil)
localhost:6379> mget user:2:name user:2:age
1) "wangzichen"
2) "24"
############################################################################################
List(列表)
- 具有方向性,可以用作队列、栈、阻塞队列
# LPUSH代表是对List的push操作,默认从左向右插入list,Rpush代表从右向左插入
localhost:6379> lpush list1 zhangsan
(integer) 1
localhost:6379> lpush list1 wangwu
(integer) 2
localhost:6379> lpush list1 lisi
(integer) 3
localhost:6379> lrange list1 0 -1 # lrange key 0 -1 查看list中所有元素
1) "lisi"
2) "wangwu"
3) "zhangsan"
localhost:6379> rpush list1 liuneng
(integer) 4
localhost:6379> lrange list1 0 -1
1) "lisi"
2) "wangwu"
3) "zhangsan"
4) "liuneng"
############################################################################################
localhost:6379> lpop list1 # 移除使用pop,方向同理
"lisi"
localhost:6379> lrange list1 0 4
1) "wangwu"
2) "zhangsan"
3) "liuneng"
############################################################################################
localhost:6379> lindex list1 1 # 获取list1中下标为1的元素值
"zhangsan"
localhost:6379> llen list1 # 获取list1的长度
(integer) 3
############################################################################################
localhost:6379> LRANGE list1 0 -1
1) "wangwu"
2) "zhangsan"
3) "liuneng"
localhost:6379> lpush list1 temp
(integer) 4
localhost:6379> lpush list1 wangwu
(integer) 5
# count>0从左向右移除,count<0从右向左移除,count=0移除全部
localhost:6379> lrem list1 -1 wangwu # lrem用于移除指定元素,使用count可以移除多个
(integer) 1
localhost:6379> LRANGE list1 0 -1
1) "wangwu"
2) "temp"
3) "zhangsan"
4) "liuneng"
############################################################################################
# Ltrim 对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。下标 0 表示列表的第一个元素,以 1 表示列表的第二个元素,以此类推。 你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。
localhost:6379> ltrim list1 1 2
OK
localhost:6379> lrange list1 0 -1
1) "temp"
2) "zhangsan"
############################################################################################
localhost:6379> lpush mylist hello1
(integer) 1
localhost:6379> lpush mylist hello2
(integer) 2
localhost:6379> lpush mylist hello3
(integer) 3
localhost:6379> lpush mylist hello4
(integer) 4
localhost:6379> lpush mylist hello5
(integer) 5
localhost:6379> lset mylist 2 hello_mid # 更新某个位置上的值
OK
localhost:6379> lrange mylist 0 -1
1) "hello5"
2) "hello4"
3) "hello_mid"
4) "hello2"
5) "hello1"
localhost:6379> lset mylist 7 hello_mid # lset 如果list和index都不存在会报错
(error) ERR index out of range
localhost:6379> LINSERT mylist before 2 hello_last
(integer) -1
# 如果有多个相同的hello_mid,会插在从左向右找到的第一个值的前/后
localhost:6379> LINSERT mylist after hello_mid hello_after # 在list的某个元素前/后插入新元素
(integer) 6
localhost:6379> lrange mylist 0 -1
1) "hello5"
2) "hello4"
3) "hello_mid"
4) "hello_after"
5) "hello2"
6) "hello1"
小结:实际上内部结构是一个链表,before-Node-after,左右都可以插入值
- 如果key不存在,插入则创建新的链表
- 如果key存在,则新增内容
- 如果移除了所有的值,变成了空链表,也代表不存在
- 在两边插入或者改动值,效率最高;中间元素相对来说效率会低一点
Set(集合)
set中的值是不能重复的。
localhost:6379> sadd myset A # 添加元素,如果不存在则创建,添加成功返回1 ,添加失败返回0
(integer) 1
localhost:6379> sadd myset B
(integer) 1
localhost:6379> sadd myset C
(integer) 1
localhost:6379> sadd myset D
(integer) 1
localhost:6379> sadd myset E
(integer) 1
localhost:6379> smembers myset # 查看集合中的元素,集合是无序的
1) "B"
2) "A"
3) "D"
4) "C"
5) "E"
localhost:6379> sismember myset F # 判断F元素是否在集合中
(integer) 0
localhost:6379> sismember myset A
(integer) 1
############################################################################################
localhost:6379> SCARD myset # 查看集合中元素的数量
(integer) 5
localhost:6379> SREM myset A # 移除指定元素
(integer) 1
localhost:6379> SCARD myset
(integer) 4
localhost:6379> SRANDMEMBER myset # 随机返回集合中一个元素
"C"
localhost:6379> SRANDMEMBER myset
"E"
localhost:6379> SRANDMEMBER myset 2 # 随机返回集合中指定个数的元素
1) "B"
2) "C"
localhost:6379> SRANDMEMBER myset 2
1) "B"
2) "D"
############################################################################################
127.0.0.1:6379> spop myset # 随机移除一个元素
"D"
127.0.0.1:6379> smembers myset
1) "B"
2) "C"
3) "E"
127.0.0.1:6379> spop myset
"E"
127.0.0.1:6379> SREM myset B # 移除集合中指定元素
(integer) 1
127.0.0.1:6379> SREM myset E
(integer) 0
127.0.0.1:6379> smembers myset
1) "C"
############################################################################################
127.0.0.1:6379> smove myset myset2 C # 将指定元素从一个集合移动到另一个集合
(integer) 1
127.0.0.1:6379> smembers myset
(empty array)
127.0.0.1:6379> smembers myset2
1) "C"
############################################################################################
【集合操作:交集、并集、差集】
127.0.0.1:6379> sadd myset a
(integer) 1
127.0.0.1:6379> sadd myset b
(integer) 1
127.0.0.1:6379> sadd myset c
(integer) 1
127.0.0.1:6379> sadd myset2 c
(integer) 1
127.0.0.1:6379> sadd myset2 d
(integer) 1
127.0.0.1:6379> sadd myset2 e
(integer) 1
# sdiff myset元素减去myset2元素
127.0.0.1:6379> sdiff myset myset2
1) "a"
2) "b"
# sinter 两个元素的交集
127.0.0.1:6379> SINTER myset myset2
1) "c"
# sunion 两个元素的并集
127.0.0.1:6379> sunion myset myset2
1) "b"
2) "c"
3) "e"
4) "a"
5) "d"
微博、微信的共同关注、共同好友功能基于集合可以实现
Hash(哈希)
- key-map结构,value的值是一个map集合。
127.0.0.1:6379> hset myhash name wangzichen # 设置key-(key-value)的hash数据
(integer) 1
# 设置key-多个(key-value)的hash数据
127.0.0.1:6379> hmset myhash age 24 tall 180cm hobby basketball
OK
127.0.0.1:6379> hget myhash tall # 获取hash中key对应的value
"180cm"
127.0.0.1:6379> hmget myhash tall age hobby # 获取hash中多个key对应的value的数据
1) "180cm"
2) "24"
3) "basketball"
127.0.0.1:6379> hdel myhash name # 删除myhash中某一个key,key对应的value也随之消失
(integer) 1
127.0.0.1:6379> hgetall myhash # 获取myhash中所有数据
1) "name"
2) "wangzichen"
3) "age"
4) "24"
5) "tall"
6) "180cm"
7) "hobby"
8) "basketball"
127.0.0.1:6379> hlen myhash # 获取myhash的key数量
(integer) 2
############################################################################################
# 判断hash中key是否存在
127.0.0.1:6379> HEXISTS myhash hobby
(integer) 1
127.0.0.1:6379> HEXISTS myhash name
(integer) 0
127.0.0.1:6379> hkeys myhash # 获取hash中所有的key
1) "tall"
2) "hobby"
127.0.0.1:6379> HVALS myhash # 获取hash中所有的value
1) "180cm"
2) "basketball"
############################################################################################
【hash中只有HINCRBY命令】
127.0.0.1:6379> HINCRBY myhash tall 1
(error) ERR hash value is not an integer
127.0.0.1:6379> HINCR myhash tall 1
(error) ERR unknown command `HINCR`, with args beginning with: `myhash`, `tall`, `1`,
127.0.0.1:6379> hsetnx myhash name wangzichen # 和字符串 setnx 作用相同
(integer) 1
127.0.0.1:6379> hsetnx myhash hobby basketball
(integer) 0
Hash更适合对象存储,方便变更,例如用户信息。
Zset(有序集合)
- 给set中的key添加一个权值,方便对key排序
- zrange参数代表排序的index,从start到stop;而zrevrange参数代表排序的index,从start到stop
- zrangeByscore参数代表排序的score,从min到max;而zrevrangeByscore参数代表排序的score,从max到min。
# 1、2、3代表后面key的权值
127.0.0.1:6379> zadd myzset 1 one
(integer) 1
127.0.0.1:6379> zadd myzset 2 two
(integer) 1
127.0.0.1:6379> zadd myzset 3 three
(integer) 1
127.0.0.1:6379> ZRANGE myzset 0 -1
1) "one"
2) "two"
3) "three"
############################################################################################
127.0.0.1:6379> ZRANGEBYSCORE myzset 2 4 # 通过权值排序,范围是【2,4】
1) "two"
2) "three"
3) "four"
127.0.0.1:6379> ZRANGEBYSCORE myzset -inf +inf # 通过权值排序,范围是(负无穷,正无穷)
1) "one"
2) "two"
3) "three"
4) "four"
5) "five"
127.0.0.1:6379> ZRANGEBYSCORE myzset -1 7
1) "one"
2) "two"
3) "three"
4) "four"
5) "five"
127.0.0.1:6379> ZRANGEBYSCORE myzset 0 -1
(empty array)
127.0.0.1:6379> ZRANGEBYSCORE myzset -inf +inf withscores # 返回带有权值
1) "one"
2) "1"
3) "two"
4) "2"
5) "three"
6) "3"
7) "four"
8) "4"
9) "five"
10) "5"
127.0.0.1:6379> zrevrangebyscore myzset 5 0 withscores
1) "four"
2) "5"
3) "five1"
4) "5"
5) "five"
6) "5"
7) "three"
8) "3"
9) "two"
10) "2"
11) "one"
12) "1"
############################################################################################
127.0.0.1:6379> zrem myzset five # 移除有序集合的某个元素
(integer) 1
127.0.0.1:6379> ZCARD myzset # 返回有序集合中的元素数量
(integer) 4
127.0.0.1:6379> zcount myzset 1 3 # 获取指定区间的元素数量
(integer) 3
可以存储班级成绩表、工资表排序、消息权重存储、排行榜
Geospatial(地理位置)
- 导入经纬度,计算距离,地理位置信息
- 应用:附近的人,打车距离
相关命令 |
---|
GEOADD |
GEODIST |
GEOHASH |
GEOPOS |
GEORADIUS |
GEORADIUSBYMEMBER |
# 添加城市经纬度数据 geoadd key 经度 纬度 名称
127.0.0.1:6379> GEOADD china:city 116.40 39.90 beijing
(integer) 1
127.0.0.1:6379> GEOADD china:city 121.47 31.23 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 114.05 22.52 shenzhen
(integer) 1
127.0.0.1:6379> GEOADD china:city 120.16 30.24 hangzhou
(integer) 1
127.0.0.1:6379> geoadd china:city 108.96 34.26 xian
(integer) 1
# 获取城市的地理信息
127.0.0.1:6379> GEOPOS china:city xian
1) 1) "108.96000176668167114"
2) "34.25999964418929977"
# 获取两个城市之间的距离
127.0.0.1:6379> GEODIST china:city xian chongqing
"575046.9885"
127.0.0.1:6379> GEODIST china:city xian shanghai
"1216930.7473"
# 以100,30为经纬度的点,半径1000km之内的城市
127.0.0.1:6379> GEORADIUS china:city 100.00 30.00 1000 km
1) "chongqing"
2) "xian"
# 带有参数的返回
127.0.0.1:6379> GEORADIUSBYMEMBER china:city xian 1000 km
1) "xian"
2) "chongqing"
3) "beijing"
127.0.0.1:6379> GEORADIUSBYMEMBER china:city xian 1000 km withcoord
1) 1) "xian"
2) 1) "108.96000176668167114"
2) "34.25999964418929977"
2) 1) "chongqing"
2) 1) "106.49999767541885376"
2) "29.52999957900659211"
3) 1) "beijing"
2) 1) "116.39999896287918091"
2) "39.90000009167092543"
127.0.0.1:6379> GEORADIUSBYMEMBER china:city xian 1000 km withdist
1) 1) "xian"
2) "0.0000"
2) 1) "chongqing"
2) "575.0470"
3) 1) "beijing"
2) "910.0565"
127.0.0.1:6379> GEORADIUSBYMEMBER china:city xian 1000 km withdist count 2
1) 1) "xian"
2) "0.0000"
2) 1) "chongqing"
2) "575.0470"
############################################################################################
# geospatial 底层使用的是Zset,所以Zset的命令适用于geospatial,并且geo没有提供的remove操作,Zset可以
127.0.0.1:6379> zrange china:city 0 -1
1) "chongqing"
2) "xian"
3) "shenzhen"
4) "hangzhou"
5) "shanghai"
6) "beijing"
127.0.0.1:6379> zrem china:city chongiqng
(integer) 0
127.0.0.1:6379> zrem china:city chongqing
(integer) 1
127.0.0.1:6379> zrange china:city 0 -1
1) "xian"
2) "shenzhen"
3) "hangzhou"
4) "shanghai"
5) "beijing"
Hyperloglog(基数)
什么是基数
A{1,3,5,7,9,7},B{1,3,5,7,8}
两个集合去重后,基数都是5
- 一般用作UV的统计,只需要去重后的数量,不需要记录uid或pin信息
- 如果使用set会造成内存浪费
- Hyperloglog使用特定的算法和数据结构,2^64个元素只需要12KB的内存
# 合并之后只有{a,b,c,d,e,f}
127.0.0.1:6379> pfadd hyperkey a b c d e
(integer) 1
127.0.0.1:6379> pfcount hyperkey
(integer) 5
127.0.0.1:6379> pfadd hyperkey2 b c d e f
(integer) 1
127.0.0.1:6379> PFMERGE hyperkey3 hyperkey hyperkey2
OK
127.0.0.1:6379> pfcount hyperkey3
(integer) 6
Bitmap(位图)
使用0和1来表示事件是否发生。每一位只有两种状态
例如:用户打卡信息,7天每天打卡就用“1111111”表示,有几个1就代表打卡了几天。
这样一千万个用户365天的打卡信息就是10000000*365bit/8/1024/1024=435MB内存,占用内存非常小。
# setbit key offset value
# 设置每一天的打卡状态,第0、1、2、5天打卡了
# offset有最大限制,0~2^32之间
127.0.0.1:6379> setbit sign 0 1
(integer) 0
127.0.0.1:6379> setbit sign 1 1
(integer) 0
127.0.0.1:6379> setbit sign 2 1
(integer) 0
127.0.0.1:6379> setbit sign 3 0
(integer) 0
127.0.0.1:6379> setbit sign 4 0
(integer) 0
127.0.0.1:6379> setbit sign 5 1
(integer) 0
127.0.0.1:6379> setbit sign 6 0
(integer) 0
# 获取某一天的打卡情况
127.0.0.1:6379> getbit sign 0
(integer) 1
# bitmap也是使用string存储
127.0.0.1:6379> type sign
string
# 获取bitmap中为1的数量
127.0.0.1:6379> bitcount sign
(integer) 4
Redis事务
拓展:Mysql中,ACID代表 原子性、一致性、隔离性、持久性
原子性:事务一旦触发,就必须完成整个流程,如果中间一环出现问题,会回滚到最初的状态。比如银行转账,事务为【A转出,B转入】,但由于故障B没有转入,所以最后会让A的转出回滚,保证金额不变。
一致性:在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设约束、触发器、级联回滚等。
隔离性:数据库拥有,代表不同事务之间相互干扰的程度。隔离级别有【未提交读、提交读、可重复读、串行化】
持久性:事务对数据处理之后,对于数据的改变就是永久性的,即使系统故障也不会丢失。
- **但是! Redis的事务不保证原子性,单条命令是原子性的 **
- Redis事务中的命令会按照顺序执行
开启事务(multi)
命令入队(get/set)
提交(exec)
# 事务被成功执行
redis 127.0.0.1:6379> MULTI
OK
redis 127.0.0.1:6379> INCR user_id
QUEUED
redis 127.0.0.1:6379> INCR user_id
QUEUED
redis 127.0.0.1:6379> INCR user_id
QUEUED
redis 127.0.0.1:6379> PING
QUEUED
redis 127.0.0.1:6379> EXEC
1) (integer) 1
2) (integer) 2
3) (integer) 3
4) PONG
############################################################################################
# 如果出现问题可以取消事务
redis 127.0.0.1:6379> MULTI
OK
redis 127.0.0.1:6379> PING
QUEUED
redis 127.0.0.1:6379> SET greeting "hello"
QUEUED
redis 127.0.0.1:6379> DISCARD
OK
编译性异常(代码有问题,命令有错),事务中所有命令都不会被执行。
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> setget k3
(error) ERR unknown command `setget`, with args beginning with: `k3`,
127.0.0.1:6379> EXEC
(error) EXECABORT Transaction discarded because of previous errors.
运行时异常(类似于1除以0),如果事务队列中有命令在执行时抛出异常,那么其他命令可以正常执行。
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> incr k1 # 由于incr只能针对integer类型,所以运行的时候抛出了异常
QUEUED
127.0.0.1:6379> get k1
QUEUED
127.0.0.1:6379> exec
1) OK
2) (error) ERR value is not an integer or out of range
3) "v1"
监控(watch)
悲观锁:在任何时候都认为会有冲突,所以在做操作前就给方法或者对象上锁,比较耗费性能,但是在冲突较多的情况下应该使用,比如synchronize。
乐观锁:认为大概率不会冲突,只要在更新数据的时候,判断在此期间是否有人修改过这个数据,如果不符合预期就等待,自旋。CAS就是乐观锁的一种实现方式。
- 获取version
- 更新的时候比较version
127.0.0.1:6379> watch k1 # 对 k1 添加监控
OK
127.0.0.1:6379> set k1 v1 #对 k1 进行修改
OK
127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379> set k1 v2
QUEUED
127.0.0.1:6379> get k1
QUEUED
127.0.0.1:6379> exec # 此处执行发现 watch 的 k1 值已经变化,所以事务不会执行(所有语句都不会执行)
(nil)
# 执行一次exec之后,watch就失效了
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v2
QUEUED
127.0.0.1:6379> get k1
QUEUED
127.0.0.1:6379> exec
1) OK
2) "v2"
使用Java代码实现redis的乐观锁,效果如下:
主线程不断循环获得watch,然后休眠4秒(给其他线程更改数据的时间),之后开启事务对key进行操作,检测到key对应的value被更改,继续循环获得watch。
副线程每隔3秒对key的值进行nextInt操作,随机赋值。
public static void main(String[] args) {
Jedis jedis = new Jedis("127.0.0.1",6379);
//另开一个A线程随机更改key的值
new Thread(()->{
Jedis jedis2 = new Jedis("127.0.0.1",6379);
while (true){
jedis2.set("18:45",Integer.toString(new Random().nextInt(500)));
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"---> 设置了value");
}
},"A线程").start();
//由于try-catch,初始化需要提到外面来
Transaction multi = null;
List<Object> exec = null;
//循环次数
int loopCount = 0;
try {
do {
System.out.println("此时key的值是: " + jedis.get("18:45"));
//添加监控
jedis.watch("18:45");
Thread.sleep(4000);
System.out.println("自旋次数" + loopCount++);
//开启事务
multi = jedis.multi();
multi.set("18:45", String.valueOf(3));
//如果事务执行失败,exec会返回null,否则是"[OK]"
exec = multi.exec();
} while (exec == null);
} catch (Exception e) {
if (multi != null){
multi.discard();
}
e.printStackTrace();
} finally {
jedis.close();
}
}
-
事务执行的结果是一个List,如果成功会返回[OK],如果有部分语句执行异常会返回[OK,异常信息]
-
multi.exec后,需要重新添加事务,需要重新对key做watch操作
-
***redis事务的实现原理:***待补充
SpringBoot整合Redis
等我后面把spring看完再说。。。
Redis.conf配置详解
Units 单位
可以对存储单位进行配置
# Note on units: when memory size is needed, it is possible to specify
# it in the usual form of 1k 5GB 4M and so forth:
#
# 1k => 1000 bytes
# 1kb => 1024 bytes
# 1m => 1000000 bytes
# 1mb => 1024*1024 bytes
# 1g => 1000000000 bytes
# 1gb => 1024*1024*1024 bytes
#
# units are case insensitive so 1GB 1Gb 1gB are all the same.
Include 包含
可以引入其他config文件来做配置,如果对每个服务器有公用配置模版,又有单个差异化配置,可以使用这个。
类似import
# Include one or more other config files here. This is useful if you
# have a standard template that goes to all Redis servers but also need
# to customize a few per-server settings. Include files can include
# other files, so use this wisely.
#
# Notice option "include" won't be rewritten by command "CONFIG REWRITE"
# from admin or Redis Sentinel. Since Redis always uses the last processed
# line as value of a configuration directive, you'd better put includes
# at the beginning of this file to avoid overwriting config change at runtime.
#
# If instead you are interested in using includes to override configuration
# options, it is better to use include as the last line.
#
# include /path/to/local.conf
# include /path/to/other.conf
Modules 模块
加载so文件
Network 网络
bind 127.0.0.1 # 绑定的ip
port 6379 # 对外开放的端口
protected-mode yes # 保护模式 是都需要密码认证等
tcp等选项
GENERAL 通用
daemonize no # 以守护进程的方式运行,默认是不开启。开启后可以后台运行服务
pidfile /var/run/redis_6379.pid # 如果以后台方式运行,需要指定一个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)
loglevel notice
logfile "" #日志的文件位置名
databases 16 # 数据库数量
REPLICATION 主从复制
replicaof <masterip> <masterport> # 如果配置了主机地址则会变成该主机的从机,搭建集群
masterauth <master-password> # 不论主机从机,都应该配置这个选项,如果集群中出现重新选举,改变主机时,从机连接主机会用到密码
SNAPSHOTTING 快照(RDB)
################################ SNAPSHOTTING ################################
#
# Save the DB on disk:
#
# save <seconds> <changes>
#
# Will save the DB if both the given number of seconds and the given
# number of write operations against the DB occurred.
#
# In the example below the behaviour will be to save:
# after 900 sec (15 min) if at least 1 key changed
# after 300 sec (5 min) if at least 10 keys changed
# after 60 sec if at least 10000 keys changed
#
# Note: you can disable saving completely by commenting out all "save" lines.
#
# It is also possible to remove all the previously configured save
# points by adding a save directive with a single empty string argument
# like in the following example:
#
# save ""
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes # 持久化如果出错后是否继续工作
rdbcompression yes # 是否压缩RDB文件,压缩会消耗CPU资源
rdbchecksum yes # 保存RDB文件时进行完整性检查
dbfilename dump.rdb # 持久化的文件名称
dir /usr/local/var/db/redis/ # 持久化的文件路径
SECURITY 安全
可以在这里设置连接密码
CLIENTS 客户端限制
maxclients 10000 # 最多连接10000个客户端
MEMORY MANAGEMENT 内存管理
maxmemory <bytes> # 最大内存容量
maxmemory-policy noeviction # 如果内存达到上限之后的处理策略
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
appendfsync everysec # 每秒执行一次同步,可能会丢失一秒的数据
# appendfsync no
1、appendfsync no
当设置appendfsync为no的时候,Redis不会主动调用fsync去将AOF日志内容同步到磁盘,所以这一切就完全依赖于操作系统的调试了。对大多数Linux操作系统,是每30秒进行一次fsync,将缓冲区中的数据写到磁盘上。
2、appendfsync everysec
当设置appendfsync为everysec的时候,Redis会默认每隔一秒进行一次fsync调用,将缓冲区中的数据写到磁盘。但是当这一 次的fsync调用时长超过1秒时。Redis会采取延迟fsync的策略,再等一秒钟。也就是在两秒后再进行fsync,这一次的fsync就不管会执行多长时间都会进行。这时候由于在fsync时文件描述符会被阻塞,所以当前的写操作就会阻塞。
所以,结论就是:在绝大多数情况下,Redis会每隔一秒进行一次fsync。在最坏的情况下,两秒钟会进行一次fsync操作。这一操作在大多数数据库系统中被称为group commit,就是组合多次写操作的数据,一次性将日志写到磁盘。
3、appednfsync always
当设置appendfsync为always时,每一次写操作都会调用一次fsync,这时数据是最安全的,当然,由于每次都会执行fsync,所以其性能也会受到影响
Redis持久化
Redis的数据存储在在内存中,是断电即失的,所以要做持久化。数据从内存备份到磁盘中要有下面五个过程:
(1)客户端向服务端发送写操作(数据在客户端的内存中)。
(2)数据库服务端接收到写请求的数据(数据在服务端的内存中)。
(3)服务端调用write这个系统调用,将数据往磁盘上写(数据在系统内存的缓冲区中)。
(4)操作系统将缓冲区中的数据转移到磁盘控制器上(数据在磁盘缓存中)。
(5)磁盘控制器将数据写到磁盘的物理介质中(数据真正落到磁盘上)。
这5个过程是在理想条件下一个正常的保存流程,但是在大多数情况下,我们的机器等等都会有各种各样的故障,这里划分了两种情况:
(1)Redis数据库发生故障,提供两种持久化方式(RDB和AOF),只要在上面的第三步执行完毕,那么就可以持久化保存,剩下的两步由操作系统替我们完成。
(2)操作系统发生故障,必须上面5步都完成才可以。
RDB(RedisDatabase)
-
Redis会单独fork(完全复制父进程的资源,拥有父进程的内存数据)一个子进程,进行bgsave(BackgroundSave),不会阻塞主线程响应客户端请求。
# 客户端 127.0.0.1:6379> bgsave Background saving started # 服务端 18240应该就是子进程号了 11193:M 15 Mar 2021 19:16:10.859 * Background saving started by pid 18240 18240:C 15 Mar 2021 19:16:10.861 * DB saved on disk 11193:M 15 Mar 2021 19:16:10.913 * Background saving terminated with success
-
父进程修改内存子进程不会反映出来,所以在快照持久化期间修改的数据不会被保存,可能丢失数据。
-
类似于Copy-On-Write,缓冲区数据写入完成后才替换原来的dump.rdb文件。
-
保存的是内存中二进制序列化数据,将redis中所有的数据备份到磁盘中。
AOF(Append-Only-File)
- AOF保存的是每一条写数据的命令,相当于命令日志,不断追加在aof文件后,根据日志可以恢复数据。
- 当AOF存储过大时,会进行重写(rewriteaof),根据redis中的数据,重新写一份类似set命令的日志文件。
- 重写也是用的是fork子进程的方式,与RDB的bgsave相似。
- Redis默认不开启AOF,可以使用bgrewriteaof命令主动触发。
# 客户端
127.0.0.1:6379> BGREWRITEAOF
Background append only file rewriting started
# 服务端
11193:M 16 Mar 2021 10:57:52.755 * Background append only file rewriting started by pid 22807
11193:M 16 Mar 2021 10:57:52.781 * AOF rewrite child asks to stop sending diffs.
22807:C 16 Mar 2021 10:57:52.781 * Parent agreed to stop sending diffs. Finalizing AOF...
22807:C 16 Mar 2021 10:57:52.782 * Concatenating 0.00 MB of AOF diff received from parent.
22807:C 16 Mar 2021 10:57:52.782 * SYNC append only file rewrite performed
11193:M 16 Mar 2021 10:57:52.801 * Background AOF rewrite terminated with success
11193:M 16 Mar 2021 10:57:52.801 * Residual parent diff successfully flushed to the rewritten AOF (0.00 MB)
11193:M 16 Mar 2021 10:57:52.802 * Background AOF rewrite finished successfully
机制 | RDB | AOF |
---|---|---|
文件体积 | 小 | 大 |
恢复速度 | 快 | 慢 |
数据丢失 | 子进程在生成快照的时候,如果数据更改并且宕机会丢失 | 如果选择everysec,可能会丢失1秒之内的数据;如果手动rewriteaof,写入间隔修改数据并且宕机也会丢失 |
触发方式 | save [second] [changes] bgsave flushall命令 退出redis时 | appendfsync [no/everysec/always] bgrewriteaof |
同步性能 | RDB文件紧凑,全量备份,适合用于进行备份和灾难恢复。 | 如果是everysec方式,每秒会占用磁盘I/O 如果是always方式,性能较差数据完整性好 |
Redis订阅与发布
Redis发布订阅(pub/sub)是一种消费通信模式:发送者发送消息,订阅者接收消息。微信、微博的关注系统。
Redis客户端可以订阅任意数量的频道。
127.0.0.1:6379> SUBSCRIBE china blue # 订阅“china”和“blue”这;两个频道
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "china"
3) (integer) 1
1) "subscribe"
2) "blue"
3) (integer) 2
# 另一个客户端给两个频道分别发送消息
127.0.0.1:6379> PUBLISH china "hello"
(integer) 1
127.0.0.1:6379> PUBLISH blue "hello"
# 订阅者打印接收到的消息
1) "message"
2) "china"
3) "hello"
1) "message"
2) "blue"
3) "hello"
实现原理
- Redis的发布订阅模式维护了一个字典,所有chanel就是字典的key,字典的值是一个链表,每一个客户端就是链表的节点。
- 当有客户端订阅了某个channel,就把自己添加到channel对应的字典key中。
- 当有消息发布的时候,redis-server就会遍历该channel对应的所有链表节点。
与MQ消息的区别在于:如果客户端离线了,或者没有客户端订阅,那么在这个channel中发布的消息不会被记录,也不会被后来在关注订阅的客户端所消费
使用场景:
1.实时消息系统
2.实时聊天(频道当作聊天室,将信息显示给所有人)
3.订阅,关注系统
Redis集群环境搭建
主从复制,读写分离!
- 将一台服务器(master)的数据复制到其他服务器(slave),前者称为主节点(master/leader),后者称为从节点(slave/follower)。
- master以写操作为主,slave以读为主。
- 数据备份:主机的数据在几个从机上复制了一份。
- 负载均衡:减轻主节点的压力,提高redis服务器的并发处理能力
- 故障恢复:主节点出问题后可以由其他节点继续提供服务(涉及到选举)
- 高可用基石:是哨兵模式和集群能够实施的基础。
环境配置
- 只配置从机,不配置主机,因为服务器默认自己是主机
127.0.0.1:6379> info replication # 查看本机主从复制的信息
# Replication
role:master # 角色:主机
connected_slaves:0 # 无从机连接
master_replid:8b32f06a2ba02640823ad037de51c157af399438
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
# 拷贝三分配置文件,端口名作为后缀
dzsb-001996@DZ0400478 etc % cp redis.conf ./wconfig/redis79.config
dzsb-001996@DZ0400478 etc % cp redis.conf ./wconfig/redis80.config
dzsb-001996@DZ0400478 etc % cp redis.conf ./wconfig/redis81.config
dzsb-001996@DZ0400478 etc % cd wconfig
dzsb-001996@DZ0400478 wconfig % ls
redis79.config redis80.config redis81.config
-
分别配置每一个配置项,改变端口号、改变pidfile的端口号、守护模式开启、改变日志文件的名称、改变dump文件名称
-
使用命令:
redis-server …/etc/wconfig/redis79.config
redis-server …/etc/wconfig/redis80.config
redis-server …/etc/wconfig/redis81.config
启动三个服务:
dzsb-001996@DZ0400478 bin % redis-server ../etc/wconfig/redis79.config
dzsb-001996@DZ0400478 bin % redis-server ../etc/wconfig/redis80.config
dzsb-001996@DZ0400478 bin % redis-server ../etc/wconfig/redis81.config
启动后使用命令:ps -ef|grep redis 查看进程信息,确认有三个端口开放
dzsb-001996@DZ0400478 ~ % ps -ef|grep redis
502 11193 1 0 三02下午 ?? 12:16.76 redis-server *:6379
502 75212 1 0 3:45下午 ?? 0:00.52 redis-server 127.0.0.1:6379
502 76176 1 0 3:46下午 ?? 0:00.27 redis-server 127.0.0.1:6380
502 76178 1 0 3:46下午 ?? 0:00.25 redis-server 127.0.0.1:6381
502 76824 62231 0 3:47下午 ttys004 0:00.00 grep redis
主从配置
# 在80端口的客户端中,使用“slaveof [ip] [port]”命令配置从机
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:6
master_sync_in_progress:0
slave_repl_offset:14
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:39779cefcf4f0ecf529aecf6b4bbc97972d92a3d
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:14
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:14
# 主机查看信息
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:2
slave0:ip=127.0.0.1,port=6380,state=online,offset=252,lag=0
slave1:ip=127.0.0.1,port=6381,state=online,offset=252,lag=0
master_replid:39779cefcf4f0ecf529aecf6b4bbc97972d92a3d
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:252
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:252
复制原理
- 全量复制:当slave连接到master时,自动触发sync同步命令,master会启动后台的存盘进程,同时收集所有接收到的用于修改数据集的命令,在后台进程执行完毕之后,传送整个数据文件到slave。
- 增量复制:当slave连接到master后,master收集到的修改命令依次传给slave,完成同步。
- 重新连接master,全量复制将被自动执行。
手动变更主机
- 在没有哨兵模式的情况下,如果出现主机宕机的情况,可以使用“slaveof no one”命令把自己变成主机。
哨兵模式
- 哨兵起到监督的作用,向每个服务器发送命令,如果收不到回复,则认为服务器已经宕机。例如哨兵2检测到master离线,此时称为主观离线。当其他哨兵也检测到master下线,并且数量达到一定值的时候,哨兵集群会进行一次投票(投票算法),选举出新的主机,此时故障的主机被称为客观下线。
配置哨兵
1.配置哨兵文件 sentinel.conf
# myredis是被监控的名称,数字1代表如果主机挂了,投票给谁成为主机
sentinel monitor myredis 127.0.0.1 6379 1
# 启动哨兵
redis-sentinel ./etc/wconfig/sentinel.conf
37274:X 16 Mar 2021 16:55:23.228 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
37274:X 16 Mar 2021 16:55:23.229 # Redis version=6.0.9, bits=64, commit=00000000, modified=0, pid=37274, just started
37274:X 16 Mar 2021 16:55:23.229 # Configuration loaded
37274:X 16 Mar 2021 16:55:23.230 * Increased maximum number of open files to 10032 (it was originally set to 2560).
_._
_.-``__ ''-._
_.-`` `. `_. ''-._ Redis 6.0.9 (00000000/0) 64 bit
.-`` .-```. ```\/ _.,_ ''-._
( ' , .-` | `, ) Running in sentinel mode
|`-._`-...-` __...-.``-._|'` _.-'| Port: 26379
| `-._ `._ / _.-' | PID: 37274
`-._ `-._ `-./ _.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' | http://redis.io
`-._ `-._`-.__.-'_.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' |
`-._ `-._`-.__.-'_.-' _.-'
`-._ `-.__.-' _.-'
`-._ _.-'
`-.__.-'
37274:X 16 Mar 2021 16:55:23.233 # Sentinel ID is e5c6f2eaf1bc20373b862b0bd5894fb645320acb
37274:X 16 Mar 2021 16:55:23.233 # +monitor master myredis 127.0.0.1 6379 quorum 1
37274:X 16 Mar 2021 16:55:23.234 * +slave slave 127.0.0.1:6380 127.0.0.1 6380 @ myredis 127.0.0.1 6379
37274:X 16 Mar 2021 16:55:23.236 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ myredis 127.0.0.1 6379
2.将主机shutdown,哨兵自动选举6380为主机
3.此时6379的机器重新连接后,会变成slave,从属于6380机器
总结
优点:
1.哨兵集群,基于主从复制的模式,所有的主从配置的优点都具备。
2.主从可以快速自动切换,故障可以转移,实现系统高可用。
3.哨兵模式是主从模式的升级,手动到自动,更加健壮。
缺点:
1.Redis不好实现在线扩容,集群容量一旦达到上限,在线扩容十分麻烦。
2.哨兵模式的配置项实际有很多项。
# Example sentinel.conf # 哨兵sentinel实例运行的端口 默认26379 port 26379 # 哨兵sentinel的工作目录 dir /tmp # 哨兵sentinel监控的redis主节点的 ip port # master-name 可以自己命名的主节点名字 只能由字母A-z、数字0-9 、这三个字符".-_"组成。 # quorum 当这些quorum个数sentinel哨兵认为master主节点失联 那么这时 客观上认为主节点失联了 # sentinel monitor <master-name> <ip> <redis-port> <quorum> sentinel monitor mymaster 127.0.0.1 6379 2 # 当在Redis实例中开启了requirepass foobared 授权密码 这样所有连接Redis实例的客户端都要提供密码 # 设置哨兵sentinel 连接主从的密码 注意必须为主从设置一样的验证密码 # sentinel auth-pass <master-name> <password> sentinel auth-pass mymaster MySUPER--secret-0123passw0rd # 指定多少毫秒之后 主节点没有应答哨兵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> <numslaves> sentinel parallel-syncs mymaster 1 # 故障转移的超时时间 failover-timeout 可以用在以下这些方面: #1. 同一个sentinel对同一个master两次failover之间的间隔时间。 #2. 当一个slave从一个错误的master那里同步数据开始计算时间。直到slave被纠正为向正确的master那里同步数据时。 #3.当想要取消一个正在进行的failover所需要的时间。 #4.当进行failover时,配置所有slaves指向新的master所需的最大时间。不过,即使过了这个超时,slaves依然会被正确配置为指向master,但是就不按parallel-syncs所配置的规则来了 # 默认三分钟 # sentinel failover-timeout <master-name> <milliseconds> sentinel 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
缓存穿透、击穿、雪崩
缓存和数据库处理流程
缓存穿透
- 缓存和数据库中都没有的数据,被恶意或者大量请求,会导致绕过缓存给数据库造成很大压力
- 解决方法:布隆过滤器,设置key-null(缓存过期时间需要设置的稍微短一些)
缓存击穿
-
缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力
-
解决方法:
1.设置热点数据永不过期
2.如果缓存中没有数据,设置独占锁,暂时阻塞其他线程,由一个线程去数据库中查询,并将数据更新到缓存中。
缓存雪崩
-
在某一个时间段,缓存集中过期失效或者Redis宕机!(比如6.18、双十一抢购,在12点准时上线一波抢购商品,设置过期时间一小时,等到1点整,仍在访问抢购商品的流量会瞬间访问数据库,出现缓存雪崩的现象)
-
解决方法:
1.设置key随机过期时间
2.redis高可用:异地多活
3.限流降级:停掉部分服务,限制一定流量
lua脚本
进阶还没看。。。
布隆过滤器
进阶还没看。。。