文章目录
Redis
cd /usr/local/bin
#启动redis
redis-server kconfig/redis.conf
#启动客户端
redis-cli
#关闭
shutdown
#退出
exit
#测试启动的线程
ps -ef|grep redis
#目录
cd /usr/local/bin
#性能测试(50个用户,100000个请求)
redis-benchmark -h localhost -p 6379 -c 50 -n 100000
1.redis-benchmark性能测试
写入
2.基础知识
redis默认有16个数据库,默认使用的是第0个数据库
可以使用select进行切换数据库
[root@localhost bin]# redis-cli -p 6379
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> select 1 #切换数据库
OK
127.0.0.1:6379[1]> dbsize # 查看DB大小
(integer) 0
127.0.0.1:6379[1]>
不同数据库可以存储不同的值
127.0.0.1:6379[5]> keys * #查看所有的key
1) "wizard"
127.0.0.1:6379[5]> flushdb #清除当前数据库
127.0.0.1:6379[5]> flushall #清除所有数据库
3.Redis是单线程的
redis是很快的 ,是基于内存操作,CPU不是Redis性能瓶颈,Redis的瓶颈是根据机器的内存和网络带宽
为什么单线程还这么快
Redis是C语言写的,100000+的QPS
-
高性能的服务器不一定是多线程的
-
多线程(CPU上下文会切换)不一定比单线程效率高
CPU>内存>硬盘速度
-
核心:redis是将所有的数据全部放在内存中,所以说使用单线程去操作效率就是高,多线程(CPU上下文会切换:耗时操作),对于内存系统来说,如果没有上下文切换效率就是最高的 ,多次读写都是在一个CPU上的,在内存情况下,这个就是最佳方案
4.五大数据类型
Redis-Key
127.0.0.1:6379> keys * #查看所有的key值
1) "age"
2) "team"
3) "num"
4) "name"
127.0.0.1:6379> get name #获得key的value值
"curry"
127.0.0.1:6379> expire team 20 #设置过期时间
(integer) 1
127.0.0.1:6379> ttl team #查看当前key的剩余时间
(integer) 12
127.0.0.1:6379> ttl team
(integer) 9
127.0.0.1:6379> ttl team
(integer) 7
127.0.0.1:6379> ttl name
(integer) -1
127.0.0.1:6379> ttl team
(integer) -2
127.0.0.1:6379> exists name #判断当前的key是否存在
(integer) 1
127.0.0.1:6379> move name 1 #移除当前的key
(integer) 1
127.0.0.1:6379> type name #查看当前key的类型
(integer) 1
String(字符串)
127.0.0.1:6379> exists name #是否存在key键 (integer) 1 127.0.0.1:6379> append name 30 #追加字符串 (integer) 7 #追加成功返回长度 127.0.0.1:6379> append xingming whz #如果追加的key不存在,就相当于set key (integer) 3 127.0.0.1:6379> get xingming "whz" 127.0.0.1:6379> get name #获取key的value值 "curry30" 127.0.0.1:6379> strlen name #获取key的长度 (integer) 7 127.0.0.1:6379> incr views #自增1,相当于i++ (integer) 1 127.0.0.1:6379> incr views (integer) 2 127.0.0.1:6379> get views "2" 127.0.0.1:6379> decr views #自减1 (integer) 1 127.0.0.1:6379> incrby views 5 #自增特定的长度(5) (integer) 6 127.0.0.1:6379> decrby views 5 #自减特定的长度(5) (integer) 1 ################################################################ #字符串范围 range 127.0.0.1:6379> get key1 "hello,wizard30" 127.0.0.1:6379> getrange key1 0 3 #截取字符串[0,3] "hell" 127.0.0.1:6379> getrange key1 0 -1 #获取全部的字符串 "hello,wizard30" #替换 127.0.0.1:6379> get key2 "abcdefghijk" 127.0.0.1:6379> setrange key2 1 xx #字符串替换,指定位置开始1 (integer) 11 127.0.0.1:6379> get key2 "axxdefghijk" ################################################################ #setex(set with expire) #设置过期时间 #setnx(set if not exist) #不存在设置(在分布式锁中常用) 127.0.0.1:6379> setex key3 60 "hello,java" #设置60秒后过期 OK 127.0.0.1:6379> ttl key3 #查看key剩余时间 (integer) 52 127.0.0.1:6379> get key3 "hello,java" 127.0.0.1:6379> setnx mykey "redis" #如果mykey不存在,创建myke (integer) 1 127.0.0.1:6379> setnx mykey "java" #如果mykey已经存在,创建失败 (integer) 0 127.0.0.1:6379> keys * 1) "key2" 2) "mykey" 3) "key1" ########################################################## #mset #mget 127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3 #同时设置多个值 OK 127.0.0.1:6379> keys * 1) "k3" 2) "k2" 3) "k1" 127.0.0.1:6379> mget k1 k2 #同时取出多个值 1) "v1" 2) "v2" 127.0.0.1:6379> msetnx k1 v1 k4 v4 #是原子性操作,如果不存在就创建,如果存在就报错,k1存在,k4不存在 (integer) 0 127.0.0.1:6379> get k4 (nil) #对象 set user:1 {name:zhangsan,age:4} #设置一个user:1 对象 值为json对象来保存对象 #这里的key是一个巧妙的设计; user:{id}:{filed} 127.0.0.1:6379> mset user:1:name Kobe user:1:age 38 OK 127.0.0.1:6379> mget user:1:name 1) "Kobe" ################################################# #getset 先获取一个值再设置一个值 127.0.0.1:6379> getset db redis #如果不存在值返回null (nil) 127.0.0.1:6379> get db "redis" 127.0.0.1:6379> getset db java #如果存在,返回存在的值并设置新的值 "redis" 127.0.0.1:6379> get db "java"
List
基本数据类型,列表
在redis里面,可以吧list变成,栈,队列,阻塞队列
所有的list命令都是用l开头(redis不区分大小写)
127.0.0.1:6379> lpush list one #将一个值或者多个值 插入到列表的头部(左) (integer) 1 127.0.0.1:6379> lpush list two (integer) 2 127.0.0.1:6379> lpush list three (integer) 3 127.0.0.1:6379> lrange list 0 -1 #获取list中的所有值 1) "three" 2) "two" 3) "one" 127.0.0.1:6379> lrange list 0 1 1) "three" 2) "two" 127.0.0.1:6379> Rpush list 555 #将一个值或者多个值 插入到列表的尾部(右) (integer) 4 127.0.0.1:6379> lrange list 0 -1 1) "three" 2) "two" 3) "one" 4) "555" ################################################# #移除 #lpop #rpop 127.0.0.1:6379> lrange list 0 -1 1) "three" 2) "two" 3) "one" 4) "555" 127.0.0.1:6379> lpop list #左边移除 "three" 127.0.0.1:6379> rpop list #右边移除 "555" 127.0.0.1:6379> lrange list 0 -1 1) "two" 2) "one" ################################################# lindex 127.0.0.1:6379> lrange list 0 -1 1) "two" 2) "one" 127.0.0.1:6379> lindex list 1 #通过下标获取list 中的 某一个值 "one" ################################################# llen 127.0.0.1:6379> lpush list one (integer) 1 127.0.0.1:6379> lpush list two (integer) 2 127.0.0.1:6379> lpush list three (integer) 3 127.0.0.1:6379> llen list #获取当前list下的值 (integer) 3 ################################################# lrem #移除指定的值 127.0.0.1:6379> lrange list 0 -1 1) "three" 2) "three" 3) "two" 4) "one" 127.0.0.1:6379> lrem list 1 one #移除一个one (integer) 1 127.0.0.1:6379> lrange list 0 -1 1) "three" 2) "three" 3) "two" 127.0.0.1:6379> lrem list 2 three #移除2个three (integer) 2 127.0.0.1:6379> lrange list 0 -1 1) "two" ################################################# trim 修剪 list 截断 127.0.0.1:6379> lrange mylist 0 -1 1) "hello1" 2) "hello2" 3) "hello3" 4) "hello4" 127.0.0.1:6379> ltrim mylist 1 2 #通过下标截取指定的长度 OK 127.0.0.1:6379> lrange mylist 0 -1 1) "hello2" 2) "hello3" ################################################# rpoplpush #移除列表中的最后一个元素,并移动到新的列表中 127.0.0.1:6379> rpoplpush mylist myotherlist "hello3" 127.0.0.1:6379> lrange mylist 0 -1 1) "hello2" ################################################# lset #将列表中指定下标的值替换为另一个值 127.0.0.1:6379> exists list #是否存在某个list 0:不存在 (integer) 0 127.0.0.1:6379> lset list 0 item #如果不存在就去更新就会报错 (error) ERR no such key 127.0.0.1:6379> lpush list value #往列表中添加某个值 (integer) 1 127.0.0.1:6379> lrange list 0 0 #查看列表第0 个元素的值 1) "value" 127.0.0.1:6379> lset list 0 item #更新当前下标的值 OK 127.0.0.1:6379> lrange list 0 0 1) "item" ############################################################# linsert #将某个具体的value插入到列表中某个元素的前面或者后面 127.0.0.1:6379> rpush mylist hello (integer) 1 127.0.0.1:6379> rpush mylist world (integer) 2 127.0.0.1:6379> linsert mylist before world value #往world元素前面插入value (integer) 3 127.0.0.1:6379> lrange mylist 0 -1 1) "hello" 2) "value" 3) "world" 127.0.0.1:6379> linsert mylist after world curry #往world元素后面插入curry (integer) 4 127.0.0.1:6379> lrange mylist 0 -1 1) "hello" 2) "value" 3) "world" 4) "curry"
Set(集合)
############################################################# 127.0.0.1:6379> sadd myset hello #set集合中添加元素 (integer) 1 127.0.0.1:6379> sadd myset wizard (integer) 1 127.0.0.1:6379> sadd myset wizard30 (integer) 1 127.0.0.1:6379> smembers myset #查看指定set中所有值 1) "wizard30" 2) "wizard" 3) "hello" 127.0.0.1:6379> sismember myset hello #判断某个值是不是在set集合中 (integer) 1 127.0.0.1:6379> sismember myset word (integer) 0 ############################################################# scard 127.0.0.1:6379> scard myset #获取set集合中的内容元素个数 (integer) 3 ############################################################# rem 127.0.0.1:6379> srem myset hello #移除指定元素 (integer) 1 127.0.0.1:6379> smembers myset #查看所有元素 1) "wizard30" 2) "wizard" ############################################################# set 无序不重复集合 抽随机 srandmember 127.0.0.1:6379> smembers myset 1) "wizard30" 2) "wizard" 3) "whz" 4) "hello" 127.0.0.1:6379> srandmember myset #随机抽取出myset中的一个元素 "hello" 127.0.0.1:6379> srandmember myset "wizard" 127.0.0.1:6379> srandmember myset 2 #随机抽取指定个数的元素 1) "wizard30" 2) "whz" ############################################################# 删除指定的key 随机删除key 127.0.0.1:6379> smembers myset 1) "wizard30" 2) "wizard" 3) "whz" 4) "hello" 127.0.0.1:6379> spop myset #随机删除一些set元素 "hello" ############################################################# 将一个指定的值,移动到另外一个set集合 127.0.0.1:6379> smove myset myset2 whz #移动myset中的whz到myset2中 (integer) 1 127.0.0.1:6379> smembers myset 1) "wizard30" 2) "wizard" 127.0.0.1:6379> smembers myset2 1) "whz" 2) "set2" ############################################################# 数字集合类: -差集 sdiff -交集 sinter -并集 sunion 127.0.0.1:6379> smembers key1 1) "d" 2) "b" 3) "c" 4) "a" 127.0.0.1:6379> smembers key2 1) "f" 2) "e" 3) "d" 4) "c" 5) "g" 127.0.0.1:6379> sdiff key1 key2 #key1和key2的差集 1) "b" 2) "a" 127.0.0.1:6379> sinter key1 key2 #key1和key2的交集 1) "d" 2) "c" 127.0.0.1:6379> sunion key1 key2 #key1和key2的并集 1) "g" 2) "e" 3) "c" 4) "f" 5) "d" 6) "b" 7) "a" #############################################################
Hash(哈希)
Map集合,key-map, 值是map集合
127.0.0.1:6379> hset myhash field1 curry #set一个具体的key-value (integer) 0 127.0.0.1:6379> hget myhash field1 #获取一个字段值 "curry" 127.0.0.1:6379> hmset myhash field1 wizard field2 wizard30 #set多个key-value OK 127.0.0.1:6379> hmget myhash field1 field2 #获取多个字段值 1) "wizard" 2) "wizard30" 127.0.0.1:6379> hgetall myhash #获取全部的数据 1) "field1" 2) "wizard" 3) "wizard30" 4) "curry" 5) "field2" 6) "wizard30" 127.0.0.1:6379> hdel myhash field1 wizard30 #删除指定的key字段,对应value的值也就消失了 (integer) 2 127.0.0.1:6379> hgetall myhash 1) "field2" 2) "wizard30" ############################################################# hlen #获取hash的内容长度 127.0.0.1:6379> hgetall myhash 1) "field1" 2) "wizard" 3) "field2" 4) "wizard30" 5) "field3" 6) "curry" 7) "field4" 8) "ys" 127.0.0.1:6379> hlen myhash #获取myhash的内容长度 (integer) 4 ###################################################### 127.0.0.1:6379> hexists myhash field1 #判断哈市中指定字段是否存在 (integer) 1 127.0.0.1:6379> hexists myhash field5 (integer) 0 ###################################################### #只获得所有的field #只获得所有的value 127.0.0.1:6379> hkeys myhash #只获得所有的field 1) "field1" 2) "field2" 3) "field3" 4) "field4" 127.0.0.1:6379> hvals myhash #只获得所有的value 1) "wizard" 2) "wizard30" 3) "curry" 4) "ys" ###################################################### #自增 incr 自减 decr 127.0.0.1:6379> hset myhash field5 30 (integer) 1 127.0.0.1:6379> hincrby myhash field5 5 #自增步长为5 (integer) 35 127.0.0.1:6379> hsetnx myhash field5 hello #如果存在则不能设置 (integer) 0 127.0.0.1:6379> hsetnx myhash field10 999 #如果不存在则可以设置 (integer) 1
hash变更的数据user name age ,用户信息保存,经常变动的信息(参考String)
127.0.0.1:6379> hset user:1 name wizard age 30 (integer) 2 127.0.0.1:6379> hmget user:1 name age 1) "wizard" 2) "30"
Zset(有序集合)
在set的基础上增加了一个值
127.0.0.1:6379> zadd myset 1 one #添加一个值 (integer) 1 127.0.0.1:6379> zadd myset 2 two 3 three #添加多个值 (integer) 2 127.0.0.1:6379> zrange myset 0 -1 1) "one" 2) "two" 3) "three" ##################################################################### #排序如何实现 127.0.0.1:6379> zadd salary 2500 xiaoming #添加三个用户 (integer) 1 127.0.0.1:6379> zadd salary 5000 zhangsan (integer) 1 127.0.0.1:6379> zadd salary 100 lp (integer) 1 127.0.0.1:6379> zrangebyscore salary -inf +inf #显示全部用户 从小到大排序 1) "lp" 2) "xiaoming" 3) "zhangsan" 127.0.0.1:6379> zrevrange salary 0 -1 #显示所有用户 从大到小 1) "zhangsan" 2) "xiaoming" 127.0.0.1:6379> zrevrange salary 0 -1 withscores 1) "zhangsan" 2) "5000" 3) "xiaoming" 4) "2500" 127.0.0.1:6379> zrangebyscore salary -inf +inf withscores #显示所有用户,并附带成绩 1) "lp" 2) "100" 3) "xiaoming" 4) "2500" 5) "zhangsan" 6) "5000" 127.0.0.1:6379> zrangebyscore salary -inf 2500 withscores #显示工资小于2500的员工升序排列 1) "lp" 2) "100" 3) "xiaoming" 4) "2500" ##################################################################### 移除rem中的元素 127.0.0.1:6379> zrange salary 0 -1 1) "lp" 2) "xiaoming" 3) "zhangsan" 127.0.0.1:6379> zrem salary lp #移除一个元素 (integer) 1 127.0.0.1:6379> zrange salary 0 -1 1) "xiaoming" 2) "zhangsan" 127.0.0.1:6379> zcard salary #获取集合中的个数 (integer) 2 ##################################################################### 127.0.0.1:6379> zadd myset 1 hello 2 world 3 wizard (integer) 3 127.0.0.1:6379> zcount myset 1 3 #获取指定区间的成员数量 (integer) 3 127.0.0.1:6379> zcount myset 2 3 (integer) 2 #####################################################################
5.三种特殊数据类型
geospatial 地理位置
朋友的定位,附近的人
getadd
#规则:两级无法直接添加
#参数 key值(纬度、经度、名称) 127.0.0.1:6379> geoadd chian: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 114.05 22.52 shenzheng (integer) 2
getpos
127.0.0.1:6379> geopos china:city beijing #获取指定城市经度纬度 1) 1) "116.39999896287918091" 2) "39.90000009167092543" 127.0.0.1:6379> geopos china:city shanghai 1) 1) "121.47000163793563843" 2) "31.22999903975783553"
geodist
两地之间的距离
127.0.0.1:6379> geodist china:city beijing shanghai #查看北京到上海的直线距离 "1067378.7564" 127.0.0.1:6379> geodist china:city beijing chongqing km #查看北京到重庆的距离 "1464.0708"
georadius
附近的人(获得所有附近的地址,定位)通过半径来查询
获得指定数量 2
127.0.0.1:6379> georadius china:city 110 30 1000 km #以100 30 这个经纬度为中心,寻找方圆1000km内的城市 1) "chongqing" 2) "shenzheng" 3) "shanghai" 4) "beijing" 127.0.0.1:6379> georadius china:city 110 30 1000 km withdist #显示到中心距离的位置 1) 1) "chongqing" 2) "341.9374" 2) 1) "shenzheng" 2) "924.6408" 127.0.0.1:6379> georadius china:city 110 30 1000 km withcoord #显示他人的定位信息,即经度纬度 1) 1) "chongqing" 2) 1) "106.49999767541885376" 2) "29.52999957900659211" 2) 1) "shenzheng" 2) 1) "114.04999762773513794" 2) "22.5200000879503861" 127.0.0.1:6379> georadius china:city 110 30 1000 km withdist withcoord count 2 #筛选指定数量的用户 1) 1) "chongqing" 2) "341.9374" 3) 1) "106.49999767541885376" 2) "29.52999957900659211" 2) 1) "shenzheng" 2) "924.6408" 3) 1) "114.04999762773513794" 2) "22.5200000879503861"
georadiusbymember
127.0.0.1:6379> georadiusbymember china:city beijing 1000 km #找出位于指定元素周围的其他元素 1) "beijing" 127.0.0.1:6379> georadiusbymember china:city beijing 10000 km 1) "chongqing" 2) "shenzheng" 3) "shanghai" 4) "beijing"
GEO底层实现原理其实就是Zset,可以使用Zset命令来操作GEO
127.0.0.1:6379> zrange china:city 0 -1 1) "chongqing" 2) "shenzheng" 3) "shanghai" 4) "beijing" 127.0.0.1:6379> zrem china:city shenzheng (integer) 1 127.0.0.1:6379> zrange china:city 0 -1 1) "chongqing" 2) "shanghai" 3) "beijing"
Hyperloglog(基数计数)
什么是基数
A{1,3,5,7,8,7}
B{1,3,5,7,8}
基数(不重复的元素)=5,可以接受误差
Hyperloglog基数统计的算法:
优点:占用的内存是固定的2^64不同元素的基数,只要12KB内存
网页UV(一个人访问一个网站多次,但是只算一次浏览)
传统的方式,set保存用户的id,然后就可以统计set中的元素数量作为标准判断
127.0.0.1:6379> PFadd mykey a b c d e f g h i j k l #创建第一组元素mykey
(integer) 1
127.0.0.1:6379> pfcount mykey #统计mykey基数数量
(integer) 12
127.0.0.1:6379> pfadd mykey2 i j k z x c v b n #创建第二组元素mykey2
(integer) 1
127.0.0.1:6379> pfcount mykey2 #统计mykey2基数数量
(integer) 9
127.0.0.1:6379> pfmerge mykey3 mykey mykey2 #合并两组,并集
OK
127.0.0.1:6379> pfcount mykey3 #查看并集数量
(integer) 16
如果允许容错,使用Hyperloglog,不允许容错,使用set或自己的数据类型
Bitmaps(位图)
位存储
统计用户信息,活跃,不活跃;登录,未登录;打卡(是否打卡),两个状态使用Bitmaps
Bitmaps位图,数据结构,操作二进制位来进行记录,只有0和1两个状态
使用Bitmaps来记录周一到周日的打卡情况,打卡:1 迟到:0
127.0.0.1:6379> setbit sign 0 1 (integer) 0 127.0.0.1:6379> setbit sign 1 0 (integer) 0 127.0.0.1:6379> setbit sign 2 0 (integer) 0 127.0.0.1:6379> setbit sign 3 1 (integer) 0 127.0.0.1:6379> setbit sign 4 1 (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 3 #周四打卡 (integer) 1 127.0.0.1:6379> getbit sign 6 #周日打卡 (integer) 1
统计操作,统计打卡天数
127.0.0.1:6379> bitcount sign #统计打卡记录 (integer) 4
6.事务
Redis事务本质:一组命令的集合;一个事务中的所有命令都会被序列化,在事务执行过程中,会按照顺序执行
一次性、顺序性、排他性
Redis事务没有隔离级别的概念
所有的命令在事务中,并没有直接被执行,只有发起执行命令时才会去执行
Redis单条命令保证原子性,但是事务不保证原子性
Redis的事务:
- 开启事务(multi)
- 命令入队()
- 执行事务(exec)
正常执行事务
127.0.0.1:6379> multi #开启事务 OK #命令入队 127.0.0.1:6379> set k1 v1 QUEUED 127.0.0.1:6379> get 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> get k3 QUEUED 127.0.0.1:6379> exec #执行事务 1) OK 2) "v1" 3) OK 4) OK 5) "v3"
放弃事务
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 k4 v4 QUEUED 127.0.0.1:6379> discard #放弃事务 OK 127.0.0.1:6379> get k1 #事务队列中的命令都不会被执行 (nil) 127.0.0.1:6379> get k4 (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> get k2 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 k1 #所有命令都不会执行 (nil)
运行时异常(1/0),如果事务队列中存在语法性,那么执行命令的时候,其他命令是可以正常执行的,错误命令会抛异常
127.0.0.1:6379> set k1 v1 OK 127.0.0.1:6379> multi OK 127.0.0.1:6379> incr k1 QUEUED 127.0.0.1:6379> set k2 v2 QUEUED 127.0.0.1:6379> set k4 v4 QUEUED 127.0.0.1:6379> get k1 QUEUED 127.0.0.1:6379> get k4 QUEUED 127.0.0.1:6379> exec 1) (error) ERR value is not an integer or out of range #虽然第一条命令报错了,但是依然可以执行成功 2) OK 3) OK 4) "v1" 5) "v4"
监控
悲观锁:
- 很悲观,什么时候都会出问题,无论做什么都会加锁
乐观锁:
- 很乐观,认为什么时候都不会出问题,所以不会上锁,更新数据的时候去判断一下在此期间是否有人修改过数据,version
- 获取version
- 更新的时候比较version
Redis的监视测试
正常执行成功
127.0.0.1:6379> set money 100 OK 127.0.0.1:6379> set out 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的乐观锁操作
127.0.0.1:6379> watch money #监视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 #执行之前,另外一个线程修改了值,就会导致事务执行失败 (nil)
127.0.0.1:6379> get money #另外一个线程修改了 "80" 127.0.0.1:6379> set money 1000 OK
解决办法
#如果事务执行失败,先解锁 127.0.0.1:6379> unwatch OK 127.0.0.1:6379> watch money #再重新加监视(加锁) OK #再执行事务 exec #比对监视的值是否发生变化,如果没变 ,可以执行成果,如果变化了就重复此步骤
7.Jedis
使用java 操作redis
-
导入对应依赖
<!--导入Jedis包--> <dependencies> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>3.2.0</version> </dependency> <!--fastjson--> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.62</version> </dependency> </dependencies>
-
编码测试
- 连接数据库
- 操作命令
- 断开连接
import redis.clients.jedis.Jedis; public class TestPing { public static void main(String[] args) { //1. new Jedis 对象 Jedis jedis = new Jedis("127.0.0.1",6379); //所有指令就是方法 System.out.println(jedis.ping());//输出PONG } }
Test.Key
import redis.clients.jedis.Jedis; import java.util.Set; public class TestKey { public static void main(String[] args) { //1. new Jedis 对象 Jedis jedis = new Jedis("127.0.0.1",6379); //所有指令就是方法 System.out.println(jedis.ping()); System.out.println("清空教据:"+jedis.flushDB()); System.out .println("判断某个键是否存在: "+jedis.exists( "username" )); System.out.println("新增< 'username' , ' wizard'>的键值对:"+jedis.set("username","wizard")); System.out.println("新增< 'password' , ' password'>的键值对:"+jedis.set("password","password")); System.out.print("系统中所有的键如下: "); Set<String> keys = jedis.keys( "*"); System.out.println(keys); System.out.println("删除键password: "+jedis.del( "password")); System.out.println("判断健password是否存在:"+jedis.exists( "password")); System.out.println("查看键username所存储的值的类型:"+jedis.type( "username")); System.out.println("随机返回key空间的一个:"+jedis.randomKey()); System.out.println("重命名key: "+jedis.rename( "username" , "name")); System.out.println("取出改后的name: "+jedis.get("name") ); System.out.println("切换数据库:"+jedis.select(0)); System.out.println("删除当前选择数据库中的所有key: "+jedis.flushDB()); System.out.println("返回当前数据库中key的数日: "+jedis.dbSize()); System.out.println("删除所有数据库中的所有key: "+jedis.flushAll()); } }
TestString
import redis.clients.jedis.Jedis; import java.util.concurrent.TimeUnit; public class TestString { public static void main(String[] args) { Jedis jedis = new Jedis("127.0.0.1",6379); jedis.flushDB( ); System.out.println( "----------增加数据----------"); System.out.println(jedis.set( "key1" , "value1")); System.out.println(jedis.set( "key2" , "value2")); System.out.println(jedis.set( "key3" , "value3")); System.out.println("删除健key2 : "+jedis.del( "key2")); System.out.println("获取键key2 : "+jedis.get( "key2") ); System.out.println("修改key1:"+jedis.set( "key1" ,"value1Changed")); System.out.println("获取key1的值:"+jedis.get("key1")); System.out.println("在:key3后面加入值:"+jedis.append("key3","End")); System.out.println("key3的值:"+jedis.get("key3")); System.out.println("增加多个键值对:"+jedis.mset ("key01","value01","key02","value02","key03","value03")) ; System.out.println("获取多个键值对:"+jedis.mget("key01","key02","key03")); System.out.println("获取多个键值对: "+jedis.mget("key01","key02","key03","key04")); System.out.println("剧除多个诎值对:"+jedis.del("key01","key02")); System.out.println("获取多个键值对: "+jedis.mget("key01","key02","key03")); jedis.flushDB(); System.out.println( "===========新增键值对防止覆盖原先值==============="); System.out.println(jedis.setnx ( "key1", "value1")); System.out.println(jedis.setnx( "key2", "value2" )); System.out.println(jedis.setnx ( "key2", "value2-new" ));//如果键不存在就创建,如果存在就创建失败 System.out.println(jedis.get( "key1")); System.out.println(jedis.get("key2")); System.out.println( "==============新增键值对并设置有效时间================"); System.out.println(jedis.setex( "key3" , 2, "value3")); System.out.println(jedis.get("key3")); try { TimeUnit.SECONDS.sleep(3) ; }catch ( InterruptedException e) { e.printStackTrace( ); } System.out.println(jedis.get( "key3")); System.out.println( "===========获取原值,更新为新值=========="); System.out.println(jedis.getSet( "key2", "key2GetSet") ); System.out.println(jedis.get( "key2" ) ); System.out.println("截取key2的值的字串:"+jedis.getrange( "key2",2,4)); } }
TestShiWu
import com.alibaba.fastjson.JSONObject; import redis.clients.jedis.Jedis; import redis.clients.jedis.Transaction; public class TestTX { public static void main(String[] args) { Jedis jedis = new Jedis("127.0.0.1", 6379); jedis.flushDB(); JSONObject jsonObject = new JSONObject(); jsonObject.put("hello","world"); jsonObject.put("name","wizard"); //开启事务 Transaction multi = jedis.multi(); String string = jsonObject.toJSONString(); //jedis.watch(string) try { multi.set("user1",string); multi.set("user2",string); multi.exec();//执行事务 } catch (Exception e) { multi.discard();//如果失败就放弃事务 e.printStackTrace(); } finally { System.out.println(jedis.get("user1")); System.out.println(jedis.get("user2")); jedis.close();//关闭连接 } } }
8.SpringBoot整合
springboot操作数据:springdata
springdata也是和springboot齐名的项目
在springBoot2.x之后,原来使用的jedis被替换为了lettuce
- jedis:采用的是直连,多个线程操作,不安全,想要避免不安全,使用jedis pool连接池,更像BIO模式
- lettuce:采用netty,实例可以在多个线程中进行共享,不存在线程不安全的情况,可以减少线程数据,更像NIO模式
-
导入依赖
<!--操作redis--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
-
配置properties
#SpringBoot所有的配置类,都有一个自动配置类 RedisAutoConfiguration #自动配置类,都会绑定一个properties配置文件 RedisProperties #配置redis spring.redis.host=127.0.0.1 spring.redis.port=6379
-
测试
@SpringBootTest class Redis02SpringbootApplicationTests { @Autowired private RedisTemplate redisTemplate; @Test void contextLoads() { //redisTemplate.opsForValue(); 操作字符串,类似String //redisTemplate.opsForList(); 操作List 类似List //获取Redis的连接对象 //RedisConnection connection = redisTemplate.getConnectionFactory().getConnection(); //connection.flushAll(); //connection.flushDb(); redisTemplate.opsForValue().set("mykey","wizard30"); System.out.println(redisTemplate.opsForValue().get("mykey")); }
@SpringBootTest class Redis02SpringbootApplicationTests { @Autowired private RedisTemplate redisTemplate; @Test public void test(){ //真实的开发一般都用json来传递对象 User user = new User("农夫山泉", 3); // String jsonUser = new ObjectMapper().writeValueAsString(user); redisTemplate.opsForValue().set("user",user); System.out.println(redisTemplate.opsForValue().get("user")); }
User对象需要序列化,否则会报错,为什么需要序列化?
- 序列化是一种用来处理对象流的机制
- 所谓对象流:就是将对象的内容进行流化。可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间
- 为了解决在对对象流进行读写操作时所引发的问题
- 为了数据传输,在你的代码的里是对象格式,而在传输的时候不可能还保持这对象的样子
- redis的持久化需要写入硬盘
自定义redisTemplate
@Configuration
public class RedisConfig {
//编写自己的配置
@Bean
@SuppressWarnings("all")//用于抑制编译器产生警告信息。
public RedisTemplate<String,Object> myredisTemplate(RedisConnectionFactory redisConnectionFactory) {
//为了开发方便,一般直接使用<String,Object>
RedisTemplate<String,Object> template=new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
//Json序列化配置
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;
}
}
测试
@Test
public void test() throws JsonProcessingException {
//真实的开发一般都用json来传递对象
User user = new User("农夫山泉", 3);
// String jsonUser = new ObjectMapper().writeValueAsString(user);
redisTemplate.opsForValue().set("user",user);
System.out.println(redisTemplate.opsForValue().get("user"));
}
自定义工具类RedisUtil(RedisUtil.java)
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@Component
public final class RedisUtil {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// =============================common============================
/**
* 指定缓存失效时间
* @param key 键
* @param time 时间(秒)
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete((Collection<String>) CollectionUtils.arrayToList(key));
}
}
}
// ============================String=============================
/**
* 普通缓存获取
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
* @param key 键
* @param delta 要增加几(大于0)
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
* @param key 键
* @param delta 要减少几(小于0)
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
// ================================Map=================================
/**
* HashGet
* @param key 键 不能为null
* @param item 项 不能为null
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
* @param key 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
* @param key 键
* @param map 对应多个键值
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashSet 并设置时间
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除hash表中的值
*
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
*
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
*
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
// ============================set=============================
/**
* 根据key获取Set中的所有值
* @param key 键
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据value从一个set中查询,是否存在
*
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将数据放入set缓存
*
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 将set数据放入缓存
*
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key, long time, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0)
expire(key, time);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取set缓存的长度
*
* @param key 键
*/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 移除值为value的
*
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object... values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
// ===============================list=================================
/**
* 获取list缓存的内容
*
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取list缓存的长度
*
* @param key 键
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 通过索引 获取list中的值
*
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @param time 时间(秒)
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0)
expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0)
expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据索引修改list中的某条数据
*
* @param key 键
* @param index 索引
* @param value 值
* @return
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 移除N个值为value
*
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
try {
Long remove = redisTemplate.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
}
测试
@Test
public void test1(){
redisUtil.set("name","wizard50");
System.out.println(redisUtil.get("name"));
}
9.Redis.config详解
启动通过配置文件来启动
-
配置文件大小写不敏感
-
网络
bind 127.0.0.1 #绑定的ip protected-mode yes #保护模式 port 6379 #端口设置
-
通用GENERAL
daemonize yes #以守护线程的方式运行,默认是no,需要手动修改为yes 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 #数据库的数量 默认是16个 always-show-logo yes #是否总是显示log
-
快照
持久化,在规定时间内,执行了多少次操作,则会持久化到文件.rdb .rof
redis是内存数据库,如果没有持久化,那么数据断电即失
save 900 1 #如果900s内,如果至少有1个key进行了修改,我们就进行持久化操作 save 300 10 #如果300s内,如果至少有10个key进行了修改,我们就进行持久化操作 save 60 10000 #如果60s内,如果至少有10000个key进行了修改,我们就进行持久化操作 #实际操作会自定义 stop-writes-on-bgsave-error yes #持久化出错是否继续工作 rdbcompression yes #是否压缩rdb文件,需要消耗一些CPU资源 rdbchecksum yes #保存rdb文件的时候,进行错误的检查效验 dir ./ #rdb文件保存的目录
-
REPLICATION复制
-
SECURITY安全
可以设置redis密码,默认是无密码
127.0.0.1:6379> ping PONG 127.0.0.1:6379> config get requirepass #获取密码 1) "requirepass" 2) "" 127.0.0.1:6379> config set requirepass 123456 #设置密码 OK 127.0.0.1:6379> config get requirepass #无权限 (error) NOAUTH Authentication required. 127.0.0.1:6379> auth 123456 #登录 OK 127.0.0.1:6379> ping PONG 127.0.0.1:6379>
-
限制CLIENTS
maxclients 10000 #设置能连接redis的最大客户端数量 maxmemory <bytes> #redis 配置最大的内存容量 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模式 aof配置
appendonly no #默认不开启aof模式,默认使用rdb方式持久化 appendfilename "appendonly.aof" #持久化的文件的名字 # appendfsync always #每次修改都会同步Sync appendfsync everysec #每秒执行一次Sync,可能会丢失1s的数据 # appendfsync no #不执行Sync,这个时候操作系统自己同步数据,速度最快
10.Redis持久化
RDB(Redis DataBase)
rdb保存的文件是dump.rdb
触发机制:
- save的规则满足的情况下,会自动触发
- 执行了flushall命令,也会触发rdb规则
- 退出redis,也会产生rdb文件
备份就会自动生成一个dump.rdb文件
如何恢复rdb文件
只需要将rdb文件放入redis启动目录就可以,redis启动的时候会自动检查dump.rdb恢复其中的数据
查看需要存在的位置
127.0.0.1:6379> config get dir 1) "dir" 2) "F:\\software\\Redis\\Redis-x64-3.0.504" #如果在这个目录下存在dump.rdb文件,启动就会恢复其中的数据
优点:
- 适合大规模的数据恢复 dump.rdb
- 对数据的完整性要求不高
缺点:
- 需要一定的时间间隔进行操作,如果redis意外宕机,最后一次修改数据就没了
- fork进程的时候,会占用一定的内存空间
AOF(Append Only File)
将所有命令都记录下来,恢复的时候,就把这个文件全部执行一遍
AOF 保存的是appendonly.aof文件
append
默认是不开启的,需要手动进行配置,将appendonly改为yes就行了
重启,redis就可以生效
如果aof文件有错误,redis不能启动,我们需要修复配置文件
redis提供了一个工具redis-check-aof --fix
优点:
- 每次修改都同步,文件的完整性更加好
- 每秒同步一次,可能会丢失1秒的数据
- 从不同步,效率最高
缺点
- 相对于数据文件来说,aof远远大于rdb ,修复速度也比rdb慢
- AOF运行效率也要比rdb慢,所以redis默认的配置是rdb持久化
11.Redis发布订阅
是一种消息通信模式:发送者发送消息,订阅者接收消息。
redis客户端可以订阅任意数量的频道
测试
订阅端
127.0.0.1:6379> subscribe NBA #订阅一个频道
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "NBA"
3) (integer) 1
#等待读取推送的信息
1) "message" #消息
2) "NBA" #哪个频道的消息
3) "curry" #消息的具体内容
1) "message"
2) "NBA"
3) "kobe"
发送端
127.0.0.1:6379> publish NBA curry #发布者发布消息到频道
(integer) 1
127.0.0.1:6379> publish NBA kobe
(integer) 1
12.Redis主从复制
数据的复制是单向的,只能从主节点到从节点。Maser以写为主,Slave以读为主
主从复制的作用:
- 数据冗余
- 故障恢复
- 负载均衡
- 高可用(集群)基石
主从复制,读写分离,80%情况都在进行读操作,减缓服务器压力,架构中常使用,一主二从
环境配置
只配置从库 不配置主库
127.0.0.1:6379> info replication #查看当前库的信息
# Replication
role:master #角色 master
connected_slaves:0 #没有从机
master_replid:7a2fa8c4a8c9f6452a507709a2ea65aa1747b9ec
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
复制三个配置文件,然后修改对应的信息
- 端口号
- pid名字
- log文件名字
- dump.rdb的文件名
修改完毕之后,启动三个redis服务,可以通过进程信息查看
ps -ef|grep redis
一主二从
默认情况下,每台Redis服务器都是主节点,一般情况下配置从机就好了
1.一主(79)二从(80、81)
127.0.0.1:6380> slaveof 127.0.0.1 6379 #在从机中进行配置 slaveof host 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:8
master_sync_in_progress:0
slave_repl_offset:14
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:a0bfad947edb79d69aa62d651f5a97f37114a7c2
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=350,lag=0 #从机的信息
slave1:ip=127.0.0.1,port=6381,state=online,offset=350,lag=0 #从机的信息
master_replid:a0bfad947edb79d69aa62d651f5a97f37114a7c2
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:350
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:350
真实的主从配置在配置文件中配置,这样的话是永久的,我们使用的命令,暂时
细节
主机可以写,从机不能写,只能读;主机中的所有信息和数据,都会被从机保存
主机断开连接,从机依旧连接到主机的,但是没有写操作了;如果主机回来了,从机依旧可以直接进获取到主机写的信息
如果是使用命令行,来配置的主从,如果重启了,就会变回主机;只要变为从机,数据立马就会写过来
Slave启动成功连接到 master后会发送一个sync同步命令
Master 接到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后,master将传送整个数据文件到slave,并完成一次完全同步。
- 全量复制:slave服务在接收到数据库文件数据后,将其存盘并加载到内存中
- 增量复制:Master继续将新的所有收集到的修改命令依次传给slave,完成同步
但是只要是重新连接master,一次完全同步(全量复制)将被自动执行
2. 层层链路:上一个M连接下一个S
如果主机断开连接,从机使用slave no one
使自己变成主机,其他节点手动连接到这个最新的主节点
13.哨兵模式
能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库
哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。
测试
一主二从:
-
配置哨兵配置文件sentinel.conf
sentinel monitor myredis 127.0.0.1 6379 1
后面的这个数字1,代表主机宕机了,slave投票看让谁接替成为主机,票数最多的,就会成为主机
-
启动哨兵
[root@localhost bin]# redis-sentinel kconfig/sentinel.conf 6171:X 16 Nov 2020 03:50:21.710 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo 6171:X 16 Nov 2020 03:50:21.710 # Redis version=6.0.9, bits=64, commit=00000000, modified=0, pid=6171, just started 6171:X 16 Nov 2020 03:50:21.710 # Configuration loaded 6171:X 16 Nov 2020 03:50:21.766 * Increased maximum number of open files to 10032 (it was originally set to 1024). _._ _.-``__ ''-._ _.-`` `. `_. ''-._ Redis 6.0.9 (00000000/0) 64 bit .-`` .-```. ```\/ _.,_ ''-._ ( ' , .-` | `, ) Running in sentinel mode |`-._`-...-` __...-.``-._|'` _.-'| Port: 26379 | `-._ `._ / _.-' | PID: 6171 `-._ `-._ `-./ _.-' _.-' |`-._`-._ `-.__.-' _.-'_.-'| | `-._`-._ _.-'_.-' | http://redis.io `-._ `-._`-.__.-'_.-' _.-' |`-._`-._ `-.__.-' _.-'_.-'| | `-._`-._ _.-'_.-' | `-._ `-._`-.__.-'_.-' _.-' `-._ `-.__.-' _.-' `-._ _.-' `-.__.-' 6171:X 16 Nov 2020 03:50:21.772 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128. 6171:X 16 Nov 2020 03:50:21.800 # Sentinel ID is b3cbc184fd9bd294e378bf9093212a99adfebed9 6171:X 16 Nov 2020 03:50:21.800 # +monitor master myredis 127.0.0.1 6379 quorum 1 6171:X 16 Nov 2020 03:50:21.804 * +slave slave 127.0.0.1:6380 127.0.0.1 6380 @ myredis 127.0.0.1 6379 6171:X 16 Nov 2020 03:50:21.809 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ myredis 127.0.0.1 6379
如果主机节点断开,这个时候会从从机中随机选择一个服务器:哨兵日志
如果主机再次连接,只能归并到新的主机下,当做从机,这就是哨兵模式的规则
优点:
- 哨兵集群,基于主从复制模式,所有的主从配置优点,它全有
- 主从可以切换,故障可以转移,系统的可用性会更好
- 哨兵模式就是主从模式的升级,手动到自动,更加健壮
缺点
- Redis不好在线扩容的,集群容量一旦到达上限,在线扩容就十分麻烦
- 实现哨兵模式的配置是很麻烦的,里面有很多选择
14.Redis缓存穿透和雪崩
缓存穿透(查不到)
缓存穿透的概念很简单,用户想要查询一个数据,发现redis内存数据库没有,也就是缓存没有命中,于是向持久层数据库查询。发现也没有,于是本次查询失败。当用户很多的时候,缓存都没有命中(秒杀!),于是都去请求了持久层数据库。这会给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透。
解决方案:
-
布隆过滤器
布隆过滤器是一种数据结构,对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃,从而避免了对底层存储系统的查询压力;
-
缓存空对象
当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了后端数据源;
存在问题
- 如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键;
- 即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。
缓存击穿(量太大,缓存过期)
这里需要注意和缓存击穿的区别,缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。
当某个key在过期的瞬间,有大量的请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访问数据库来查询最新数据,并且回写缓存,会导使数据库瞬间压力过大。
解决方案:
设置热点数据永不过期
从缓存层面来看,没有设置过期时间,所以不会出现热点key过期后产生的问题。
加互斥锁
分布式锁∶使用分布式锁,保证对于每个key同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只需要等待即可。这种方式将高并发的压力转移到了分布式锁,因此对分布式锁的考验很大。
雪崩
缓存雪崩,是指在某一个时间段,缓存集中过期失效。Redis 宕机!
产生雪崩的原因之一,比如在写本文的时候,马上就要到双十二零点,很快就会迎来一波抢购,这波商品时间比较集中的放入了缓存,假设缓存一个小时。那么到了凌晨一点钟的时候,这批商品的缓存就都过期了。而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。
解决方案
redis高可用:
这个思想的含义是,既然redis有可能挂掉,那我多增设几台redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。
限流降级
这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
数据预热
数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。