Redis学习笔记
学习内容:数据库发展史(单机MySql的演进),NoSql,Redis的概念 数据库发展史,关系数据库和非关系数据库的关系和差别,Redis基本命令,基本数据类型,事务锁,集成Java(Jedis,SpringBoot自定义模板,封装util工具类等),持久化方式,发布订阅,主从复制,哨兵模式,基本配置,场景情况,缓存异常基本处理等
数据库发展史
前言:现在是大数据时代,互联网需求比90年更大(90年网站的访问量一般不会太大,单个数据库足够),普通的mysql满足不了,数据库单表数据量超过三百万所以分离读写操作(数据库主要就是进行读写操作),缓存可以有效避免用户对数据库的直接访问减少压力(mysql单表数据300万以上就一定要建立索引!).缓存也从传统的memcached交换成新的缓存技术redis
关于数据库的写的发展:
早些年Myisam:表锁(每次查询只锁这一张表),十分影响效率!高并发的时候会出现严重的锁问题
后来转战Innodb:行锁(每次查询只锁这一行)
慢慢的就开始使用分库分表来解决写的压力,Mysql在那个年代推出了表分区!但是并没有多少公司使用
Mysql的集群,很好的满足了那个年代的所有需求
如今的年代(大数据年代)
Mysql等关系型数据库就不够用了,数据量很大,变化也很快(微博热搜,排行榜等)
比如一篇文章,它的正在浏览量10w+,实现机制就是先把它存在redis中,过一段时间进行持久化
Mysq有的人会用它存储一些大的文件(数据),博客,图片,这时候效率就低了,如果有一种数据库专门用来存储这些东西,那么会极力减少Mysql的压力,那我们就研究如何处理这些问题,这时候关系数据库就无法实现了,大数据IO压力大,表几乎无法更改的(比如我有一亿条数据,这时候动态加一个列,无法实现),
关系型数据库什么意思?
关系型数据库:就好比我们的表格,是由行和列来记录的
关系型数据库,是指采用了关系模型来组织数据的数据库,其以行和列的形式存储数据,以便于用户理解,关系型数据库这一系列的行和列被称为表,一组表组成了数据库。”
1.Redis
Redis(Remote Dictionary Server ),即远程字典服务,是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。从2010年3月15日起,Redis的开发工作由VMware主持。从2013年5月开始,Redis的开发由Pivotal赞助。
1 . Redis关键字
开源的,C语言编写的,支持多种语言(Java,Go,C,Python等等),基于内存可持久化的日志型键值对应(key-value)数据库,主要做缓存处理
2. 缓存以及缓存流程
缓存可以减少直接对数据库的访问量压力,流程为:用户发送请求访问数据库,接收到请求后首先会判断一下缓存中有没有该数据,如果没有的话就访问数据库并且把得到的数据相应返回给用户,同时把数据存到缓存中下次请求相同的数据就直接从缓存中那,如果有的话,那就直接取缓存中的数据不用再访问数据库
3. Redis端口号
6379
4. Windows安装使用(已经停更很长时间,推荐使用linux使用redis!!!)
去github下载压缩包,解压即可,启动的时候首先启动redis.server服务,然后启动redis.cli客户端
5. 常用命令以及Redis-key的一些操作
- ping(有没有连接到.返回pong表示连接成功)
- set get(set name wyh get name)
- 清除全部的数据库内容 flushall
- 清除当前数据库flushdb
- keys(*)查看全部的键
- exists(key) 查看键是否存在,存在的话返回1,不存在返回0
- select (数据库num)进行切换数据库
- clear清屏
- move key numb (键,移动到那个数据库) 移动属性到指定的数据库
- expire (name 10 )设置某个键的过期时间(单位是秒s)
- ttl (key) 查看当前某个键的剩余时间,过期的话返回负数
- type 查看数据类型 type key
- shutdown 关闭服务连接 也就是我们的server
- save 保存文件
- exit退出我们的客户端
127.0.0.1:6379[5]> ping #测试连接
PONG #连接成功
127.0.0.1:6379[1]> select 0 #切换数据库0
OK
127.0.0.1:6379> select 1 #切换数据库0
OK
127.0.0.1:6379[1]> set name wyh #设置key键
OK
127.0.0.1:6379[1]> set age 22 #设置key键
OK
127.0.0.1:6379[1]> keys * #查看全部的键
1) "name"
2) "age"
127.0.0.1:6379[1]> get name
"wyh"
127.0.0.1:6379[1]> exists age #判断是否存在
(integer) 1
127.0.0.1:6379[1]> flushdb # 刷新当期数据库
OK
127.0.0.1:6379[1]> flushall #刷新全部数据库
OK
127.0.0.1:6379[1]> clear #清空
127.0.0.1:6379[2]> set animal dog #设置属性
OK
127.0.0.1:6379[2]> move animal 5 #把animal键移动到数据库5
(integer) 1
127.0.0.1:6379[2]> keys * #查看全部的键
1) "age"
127.0.0.1:6379[2]> select 5 #切换到数据库5
OK
127.0.0.1:6379[5]> keys * #查看全部的键
1) "animal"
127.0.0.1:6379[2]> expire name 10 #设置过期时间10秒
(integer) 1 #成功的话返回1 不成功返回0
127.0.0.1:6379[2]> ttl name #查看剩余过期时间
(integer) 6
127.0.0.1:6379[2]> ttl name #查看当前key的剩余过期时间
(integer) 1
127.0.0.1:6379[2]> ttl name #查看当前key的剩余过期时间
(integer) -2 #已经过期2秒
127.0.0.1:6379[2]> get name #查看name,已经过期
(nil) #没有了,说明已经过期
127.0.0.1:6379[2]> type age #查看key的类型
string
127.0.0.1:6379[2]> type name #查看key的类型
string
127.0.0.1:6379[2]> save #保存配置
ok
127.0.0.1:6379[2]> shutdown #关闭服务连接 也就是我们的server
6. redis-benchmark官方自带的压力测试工具
以上是它在windows里面的位置,因为我目前没配置linux
redis-benchmark -h 127.0.0.1 -p 6379 -c 50 -n 10000 ##压测命令
100000 requests completed in 0.72 seconds #对我们的10万个请求进行写入测试在0.72秒完成
50 parallel clients # 50个并发客户端
3 bytes payload #每次写入三个字节
keep alive: 1 #只有一台服务器来处理这些请求,单机性能
99.95% <= 13 milliseconds
99.96% <= 14 milliseconds
99.97% <= 15 milliseconds
99.98% <= 16 milliseconds
99.99% <= 17 milliseconds
99.99% <= 18 milliseconds
100.00% <= 19 milliseconds
100.00% <= 19 milliseconds #所以请求在19秒内完成
142247.52 requests per second #一秒处理142247次请求
7. Redis的5种基本数据类型
1 String(字符串)
String类型使用场景
value除了是我们的字符串还可以是我们的数字,redis会自动帮我们辨别转化
- 计数器
- 统计多单位的数量
- 粉丝数,关注数,浏览量
- 对象缓存存储
String类型常用方法
1.append append (key 给哪个键 string 追加什么内容)追加字符串,追加成功后返回当期这个key的长度, 如果当前追加的这个key不存在就新创建一个key
127.0.0.1:6379[2]> get name #查看wyh
"wyh"
127.0.0.1:6379[2]> append name yyt #给wyh这个键拼接字符串
(integer) 6 #返回当前这个key追加内容后的长度
127.0.0.1:6379[2]> append name1 zhangsan #如果当前追加的这个key不存在就新创建一个key
(integer) 8 #返回当前这个key追加内容后的长度
2 strlen strlen (key) 查看某个key的长度
127.0.0.1:6379[2]> strlen name #查看某个key的长度
(integer) 6 #返回这个key的长度
127.0.0.1:6379[2]> strlen name2 #查看某个不存在key的长度
(integer) 0 #不存在的话返回0
127.0.0.1:6379[2]> set views 0
OK
3 incr incr(key)自增 默认自增1
场景:浏览量,当前观看人数,都是它实现的,相当于java的i++
127.0.0.1:6379[2]> decr views #让views这个key自减1
(integer) 3
127.0.0.1:6379[2]> decr views #让views这个key自减1
(integer) 2
127.0.0.1:6379[2]> decr views #让views这个key自减1
(integer) 1
127.0.0.1:6379[2]> decr views #让views这个key自减1
(integer) 0
127.0.0.1:6379[2]> decr views #让views这个key自减1
(integer) -1 #可以为负数
127.0.0.1:6379[2]> get views #查看views这个Key
"-1"
4 decr decr(key)自减,默认每次减少1,可以为负数
127.0.0.1:6379[2]> decr views #让views这个key自减1
(integer) 3
127.0.0.1:6379[2]> decr views #让views这个key自减1
(integer) 2
127.0.0.1:6379[2]> decr views #让views这个key自减1
(integer) 1
127.0.0.1:6379[2]> decr views #让views这个key自减1
(integer) 0
127.0.0.1:6379[2]> decr views #让views这个key自减1
(integer) -1 #可以为负数
127.0.0.1:6379[2]> get views #查看views这个Key
"-1"
5 incrby incrby (key) (numb)让某个键一次性步长几个长度
127.0.0.1:6379[2]> incrby views 10 #一次性步长10
(integer) 9
6 decrby decrby (key) (numb)让某个键一次性步减几个长度
127.0.0.1:6379[2]> decrby views 10 #一次性步减10
(integer) -1
7 range range (key) (index从哪里开始,长度也是从0开始的) (截取几位)查看区间指定的值,类似substring截取,如果截取的这个参数为-1显示全部的字符串,其实和get key是一样的作业
127.0.0.1:6379[2]> set key1 hello,weiyihe #创建一个key
OK
127.0.0.1:6379[2]> getrange key1 0 3 #查看区间指定的值(从0开始,数3位,也就是前四位)
"hell"
127.0.0.1:6379[2]> getrange key1 0 -1 ##如果截取的这个参数为-1显示全部的字符串 和get key是一样的
"hello,weiyihe"
8 setrange setrange(key)(index)(value)把key按照指定的下表替换成指定的值
其实就是java中的replace替换
127.0.0.1:6379[2]> set key3 abc #设置一个新的key
OK
127.0.0.1:6379[2]> setrange key3 0 x #把key3按照指定的下表和值进行替换(把第0位换成x)
(integer) 3 #替换之后得到最终结果的长度
127.0.0.1:6379[2]> get key3 #
"xbc" # 查看key3
127.0.0.1:6379[2]> setrange key3 0 xxx # #把key3按照指定的下表和值进行替换(把第0位换成xxx)
(integer) 3 # 替换之后得到最终结果的长度
127.0.0.1:6379[2]> get key3 #查看key3
"xxx"
9 setex settex(set with expire) 设置过期时间 setex(key) (time,存活时间,一般都是秒s)(key的value)
127.0.0.1:6379[2]> setex key3 30 "hello" #设置一个值为hello的键Key3,有效期30秒
OK
127.0.0.1:6379[2]> get key3 #查看key3
"hello"
127.0.0.1:6379[2]> ttl key3 #查看当期key3的有效时间
(integer) 19 #19s(秒)
127.0.0.1:6379[2]> ttl key3 #查看当期key3的有效时间
(integer) 18 #18s(秒)
127.0.0.1:6379[2]> get key3 #查看key3
(nil) #没了(过期了)
127.0.0.1:6379[2]> ttl key3 #查看key3
(integer) -2 #没了(过期了)
11 setnx settex(set if not exist) 不存在设置 (在分布式锁中经常使用) setnx (key) (key 的value)成功的话返回1(一般成功就创建一个新的key,或者把key的value改变),不成功返回0,不成功因为设置那个key已经存在了
127.0.0.1:6379[2]> setnx mykey redis #设置一个叫mykey的键
(integer) 1 #成功返回1
127.0.0.1:6379[2]> get key3
(nil)
127.0.0.1:6379[2]> setnx mykey mysql #重新设置mykey的键,不成功,因为已经有了一个叫mykey的key
(integer) 0 #不成功返回0
127.0.0.1:6379[2]> get mykey #查看key mykey
"redis"
127.0.0.1:6379[2]> setnx mykey1 mysql #设置一个叫mykey1的键
(integer) 1 #成功,因为没有叫mykey1的key
127.0.0.1:6379[2]> get mykey1 #查看key mykey1
"mysql"
12 mset mset (k1 v1)( k2 v2) (k3 v3)一次性设置多个键值对
127.0.0.1:6379[2]> mset k1 v1 k2 v2 k3 v3 #一次性设置三个键值对(k1 v1)( k2 v2) (k3 v3)
OK
127.0.0.1:6379[2]> keys * #查看全部的值
1) "k3"
2) "k2"
3) "k1"
13 mget mget (k1 k2 k3)一次性获取多个key的值
127.0.0.1:6379[2]> mget k1 k2 k3 #一次性获取多个key的值
1) "v1"
2) "v2"
3) "v3"
14 msetnx msetnx(key value)msetnx是一个原子性操作(要么全部成功,要么全部失败)
127.0.0.1:6379[2]> mset k1 v1 k2 v2 k3 v3 #一次性获取多个key的值
OK
127.0.0.1:6379[2]> keys #查看全部的key
1) "k3"
2) "k2"
3) "k1"
127.0.0.1:6379[2]> mget k1 k2 k3 #一次查看三个key
1) "v1"
2) "v2"
3) "v3"
127.0.0.1:6379[2]> msetnx k1 v1 k4 v4 #注意,这时候k1 v1已经存在,但是k4 v4不存在,返回结果为0失败,说明该操作具有原子性(要么全部成功,要不全部失败)
(integer) 0 #操作失败
127.0.0.1:6379[2]> get k4 #查看key4
(nil) #没有key4
15msetnx 扩展
开发场景:一般创建对象 set user:1{name:“张三”,age:10}设置一个user:1对象.它的值为一个json字符串来保存一个对象
这里的key是一个巧妙的设计 user:{id}:{filed}如此设计在redis中是完全可以的
127.0.0.1:6379[2]> mset user:1:name zhangsan user:1:age 10 #设置一个user1对象,它的name是zhangsan,age是10
OK
127.0.0.1:6379[2]> mset user:2:name lisi user:2:age 20 #设置一个user2对象,它的name是lisi,age是20
OK
127.0.0.1:6379[2]> mget user:1:name user:1:age #获取user1对象的name和age
1) "zhangsan"
2) "10"
127.0.0.1:6379[2]> mget user:2:name user:2:age #获取user2对象的name和age
1) "lisi"
2) "20"
127.0.0.1:6379[2]> mset user1:name zhangsan user1:age 10 #创建一个user1多key,json字符串的值
OK
127.0.0.1:6379[2]> get user1:name #单个获取user1的name
"zhangsan"
127.0.0.1:6379[2]> get user1:age #单个获取user1的age
"10"
127.0.0.1:6379[2]> mget user1:age user1:name #一次性获取m1的name和age
1) "10"
2) "zhangsan"
16 getset getset(key)(value) 先get再set,如果不存在值则返回null,如果存在值则获取原来的值并且设置新的值
127.0.0.1:6379[2]> getset db redis #先get再get一个key(db)
(nil) #因为一开始没有db这个key
127.0.0.1:6379[2]> get db #获取db这个key
"redis"
127.0.0.1:6379[2]> getset db mysql #setdb这个key
"redis"
127.0.0.1:6379[2]> get db #重新获取db这个key发现它已经被set了
"mysql"
2 Hash(哈希)
map集合,key-map,这时候这个值是一个map集合
hash的命令都是以"h"开头的
hash本质和string没有多大区别
hash的应用场景:存储变更的对象,尤其是用户信息的保存,经常变动的信息.更适合对象的存储,String更加适合字符串的存储
127.0.0.1:6379[2]> hset user:1 name wyh #存储一个用户
(integer) 1
127.0.0.1:6379[2]> hget user:1 name #获取用户名
"wyh"
1 hset(hash)(key)(value)存键值对的hash数据
127.0.0.1:6379[2]> hset user 1 wyh #创建一个键值对的hash名为user,给它复制key为1value为wyh的元素
(integer) 1
127.0.0.1:6379[2]> hset user 2 yyt #创建一个键值对的hash名为user,给它复制key为2value为yyt的元素
(integer) 1
127.0.0.1:6379[2]> hget user 1 #从user这个hash中key为1的元素
"wyh"
127.0.0.1:6379[2]> hget user 2 #从user这个hash中key为1的元素
"yyt"
2 hget(hash)(key)(value)根据key取对应的hash里面的元素
127.0.0.1:6379[2]> hset user 1 wyh #创建一个键值对的hash名为user,给它复制key为1value为wyh的元素
(integer) 1
127.0.0.1:6379[2]> hset user 2 yyt #创建一个键值对的hash名为user,给它复制key为2value为yyt的元素
(integer) 1
127.0.0.1:6379[2]> hget user 1 #从user这个hash中key为1的元素
"wyh"
127.0.0.1:6379[2]> hget user 2 #从user这个hash中key为1的元素
"yyt"
3 hgetall(hash)获取hash的全部属性
127.0.0.1:6379[2]> hgetall user #获取user的全部对象
1) "1"
2) "wyh"
3) "2"
4) "yyt"
4 hmset(hash)(key1)(value1)(key2)(value2)一次性set多个key value
127.0.0.1:6379[2]> hmset hash 1 a 2 b #一次性存储多个值
OK
127.0.0.1:6379[2]> hmget hash 1 2 #一次性取多个值
1) "a"
2) "b"
5 hmget(hash)(key1)(key2)一次性根据key取好几个值
127.0.0.1:6379[2]> hmset hash 1 a 2 b #一次性存储多个值
OK
127.0.0.1:6379[2]> hmget hash 1 2 #一次性取多个值
1) "a"
2) "b
6 hdel(hash)(key)根据key删除hash中的属性,对应的value值也就没有了
127.0.0.1:6379[2]> hdel hash 1 #删除key为1的这个属性
(integer) 1
127.0.0.1:6379[2]> hgetall hash #查看全部的属性
1) "2"
2) "b"
7 hlen(hash)查看这个hash中有多个属性(键值对)
127.0.0.1:6379[2]> hlen hash #查看有多少个属性(键值对)
(integer) 3 #三个
8 hexists (hash)(key)判断hash中是否存在这个key,返回1说明存在,返回0说明不存在
127.0.0.1:6379[2]> hexists hash 3 #判断是不是存在3这个key
(integer) 1 #返回1代表存在
127.0.0.1:6379[2]> hexists hash 33 #判断是不是存在3这个key
(integer) 0 #返回0代表不存在
9 hkeys (hash)查看全部的key
127.0.0.1:6379[2]> hkeys hash #查看全部的key
1) "2"
2) "1"
3) "3"
10 hvals(hash)查看全部的key
127.0.0.1:6379[2]> hvals hash #查看全部的values
1) "b"
2) "a"
3) "c"
11 hincrby (hash)(key)(count)给hash指定key对应的值自增(指定增减量)key是正数也可以是负数,负数的话就是减
127.0.0.1:6379[2]> hset hash id 5 #创建一个hash存储id为5的属性
(integer) 1
127.0.0.1:6379[2]> hincrby hash id 1 #给id为1的属性自增1
(integer) 6 #自增后元素为6
127.0.0.1:6379[2]> hincrby hash id 2 #给id为1的属性自增2
(integer) 8 #自增后元素为8
127.0.0.1:6379[2]> hincrby hash id -1 #给id为1的属性自增1
(integer) 7 #自减后元素为8
12 hsetnx (hash)(key)(value)添加hash元素, 如果这个hash存在就添加不了别的元素, 如果这个hash不存在就可以添加别的元素
127.0.0.1:6379[2]> hsetnx user id 1 #创建一个hash
(integer) 1
127.0.0.1:6379[2]> hsetnx user id 2 #如果这个hash存在就添加不了别的元素
(integer) 0
127.0.0.1:6379[2]> hsetnx user id 1 #如果这个hash存在就添加不了别的元素
(integer) 0
127.0.0.1:6379[2]> hsetnx user1 id 1 #如果这个hash不存在就可以添加别的元素
(integer) 1
3 List(列表)
list使用场景:可以把它用作成栈(lpush rpop左边进去 右边出来),队列,阻塞队列 ,消息排队,消息队列(lpush rpop)
所有的list命令都是以"l(L)"开头的
1 lpush lpush (list) (v1)(v2)(v3) 从左边一次性或者多次插入.也就是头部
127.0.0.1:6379[2]> lpush list 1 2 3 #创建一个新的list的list一次性添加三个值
(integer) 3 #返回当前这个list里面有几个元素
127.0.0.1:6379[2]> lrange list 0 -1 #查看这个list名为list里面的全部元素(0,-1查看全部)
1) "3"
2) "2"
3) "1"
127.0.0.1:6379[2]> lrange list 0 1 #查看最后两个元素(0,1)
1) "3"
2) "2"
127.0.0.1:6379[2]> lrange list 0 2 #查看最后三个个元素(0,1,2)
1) "3"
2) "2"
3) "1"
127.0.0.1:6379[2]> lrange list 0 0 #查看最后一个元素
1) "3"
2 lrange Lrange key (index)(index) 获取指定区间的值,去如果是0,-1的话就是查看全部的 想要得到第一个元素使用0 0
127.0.0.1:6379[2]> lrange list 0 -1 #查看这个list名为list里面的全部元素(0,-1查看全部)
1) "3"
2) "2"
3) "1"
127.0.0.1:6379[2]> lrange list 0 1 #查看最后两个元素(0,1)
1) "3"
2) "2"
127.0.0.1:6379[2]> lrange copylist 0 0 #查看第一个元素
1) "wyh4"
3 rpush r(right)push (list)(v1)(v2)(v3)从右边一次性或者多次插入.也就是尾部
127.0.0.1:6379[2]> rpush list four #给list从右边添加"four"这个元素
(integer) 4
127.0.0.1:6379[2]> rpush list five #给list从右边添加"five"这个元素
(integer) 5
127.0.0.1:6379[2]> lrange list 0 -1 #查看全部的list
1) "3"
2) "2"
3) "1"
4) "four"
5) "five"
4 lpop lpop(list)从左边删除一个值
127.0.0.1:6379[2]> lpush list 1 2 3 #创建一个多元素的list
(integer) 3
127.0.0.1:6379[2]> lrange list 0 -1 #查看全部的值
1) "3"
2) "2"
3) "1"
127.0.0.1:6379[2]> lpop list # 从左边删除一个值
"3"
127.0.0.1:6379[2]> rpop list #从右边删除一个值
"1"
127.0.0.1:6379[2]> lrange list 0 -1 #查看全部的值
1) "2"
5 rpop rpop(list)从右边删除一个值
127.0.0.1:6379[2]> lpush list 1 2 3 #创建一个多元素的list
(integer) 3
127.0.0.1:6379[2]> lrange list 0 -1 #查看全部的值
1) "3"
2) "2"
3) "1"
127.0.0.1:6379[2]> lpop list # 从左边删除一个值
"3"
127.0.0.1:6379[2]> rpop list #从右边删除一个值
"1"
127.0.0.1:6379[2]> lrange list 0 -1 #查看全部的值
1) "2"
6 lindex index(list)(index) 通过下标获取值,注意,下标也是从0开始的
127.0.0.1:6379[2]> lrange list 0 -1 #查看全部的值
1) "3"
2) "2"
3) "1"
4) "2"
127.0.0.1:6379[2]> lindex list 0 #查看左边第一个值
"3"
127.0.0.1:6379[2]> lindex list 1 #查看左边第二个值
"2"
7 llen llen(list) 查看元素的长度
127.0.0.1:6379[2]> lrange list 0 -1 #查看全部的元素
1) "3"
2) "2"
3) "1"
4) "2"
127.0.0.1:6379[2]> llen list #查看元素的长度
(integer) 4
8 lrem lrem(list)(count)(value)移除几个什么样的元素(精确匹配)
127.0.0.1:6379[2]> lrange list 0 -1 #查看全部的元素
1) "3"
2) "2"
3) "1"
4) "2"
127.0.0.1:6379[2]> lrem list 1 1 #移除1个值为1的元素
(integer) 1
127.0.0.1:6379[2]> lrem list 2 2 #移除2个值为2的元素
(integer) 2
127.0.0.1:6379[2]> lrange list 0 -1 #查看全部的元素
1) "3"
9 ltrim ltrim(list)(index)(index)在java中是修剪,在redis是截取的意思,通过下标截取某一段key的元素,但是会改变这个list,只剩下我们要截取的元素
127.0.0.1:6379[2]> lpush list 1 2 3 4 5 #创建元素
(integer) 5
127.0.0.1:6379[2]> lrange list 0 -1 #查看元素
1) "5"
2) "4"
3) "3"
4) "2"
5) "1"
127.0.0.1:6379[2]> ltrim list 0 1 #截取key为list的元素,从第一位截取到第二位
OK
127.0.0.1:6379[2]> lrange list 0 -1 #查看元素
1) "5"
2) "4"
10 rpoplpush rpoplpush(list1)(list2) 移出列表的最后一个元素,将它移动到新的列表
127.0.0.1:6379[2]> rpush list wyh1 wyh2 wyh3 #创建一个listy一次性添加多个值
(integer) 3
127.0.0.1:6379[2]> rpoplpush list copylist #复制一个list命名为copylist
"wyh3"
127.0.0.1:6379[2]> lrange copylist 0 -1 #查看copylist的元素
1) "wyh3"
127.0.0.1:6379[2]> lrange list 0 -1 #查看list的元素
1) "wyh1"
2) "wyh2"
11 exists exists(list) 判断list是否存在 存在返回1 不存在返回0
127.0.0.1:6379[2]> exists list #判断list这个列表是否存在
(integer) 1 #存在返回1 不存在返回0
127.0.0.1:6379[2]> exists list1 #判断list1这个列表是否存在
(integer) 0 #存在返回1 不存在返回0
12 lset lset(list)(index)(value) 相当于修改替换replace 将列表中指定下标的值替换成另外一个值,如果不存在会报错,存在会替换当前值
127.0.0.1:6379[2]> lset list 0 item #把list这个列表第0个下表的值替换成item
(error) ERR no such key #因为list这个列表不存在或者list这个列表没有值
127.0.0.1:6379[2]> lpush list value1 #给list这个列表进行赋值
(integer) 1
127.0.0.1:6379[2]> lrange list 0 0 #查看list这个列表全部的值
1) "value1"
127.0.0.1:6379[2]> lset list 0 item #将list这个列表中的下标为0的值替换成item
OK
127.0.0.1:6379[2]> lrange list 0 0 #查看list这个列表全部的值
1) "item"
127.0.0.1:6379[2]> lrange list 1 value2 #将list这个列表中的下标为1的值替换成value
(error) ERR value is not an integer or out of range ##因为llist这个列表没有下标为1的值
13 linsert linsert(key)(before前或者after后)(value)(value) 将某个具体的value插入列表中某个元素的前面或者后面,在list里面之前value位置(之前或者之后)插入另外一个value的元素
127.0.0.1:6379[2]> lpush list 1 2 3 #创建list集合
(integer) 3
127.0.0.1:6379[2]> linsert list before 2 0 #在list里面value为2之前插入一个value为0的元素
(integer) 4
127.0.0.1:6379[2]> lrange list 0 -1 #查看list的全部元素
1) "3"
2) "0"
3) "2"
4) "1"
127.0.0.1:6379[2]> linsert list after 2 0 #在list里面value为2之后插入一个value为0的元素
(integer) 5
127.0.0.1:6379[2]> lrange list 0 -1 #查看list的全部元素
1) "3"
2) "0"
3) "2"
4) "0"
5) "1"
list小结
1 list实际上是一个链表,before node after,left,right都可以插入值
2 如果插入的这个key不存在,需要创建一个新的链表
3 如果key存在,就在它的基础上新增内容
4 如果移出了key,所有的value都消失了,如果移除了所有值,空链表,也代表不存在
5 在两边插入或者改动值,效率最高,如果执行对中间元素的处理,效率会比较低
4 Set(集合)
set中的值是不能重复的,set是无序的 set中的命令都是以"S"开头的
1 sadd (set) (value) 给set添加元素
127.0.0.1:6379[2]> sadd myset hello #创建一个set里面的值为hello
(integer) 1
127.0.0.1:6379[2]> sadd myset hello #给set再添加一个重复的值,成功元素的个数为0,因为不能重复
(integer) 0
127.0.0.1:6379[2]> smembers myset #查看myset这个set
1) "hello"
2 smembers (set)查看set里面的元素
127.0.0.1:6379[2]> sadd myset hello #创建一个set里面的值为hello
(integer) 1
127.0.0.1:6379[2]> sadd myset hello #给set再添加一个重复的值,成功元素的个数为0,因为不能重复
(integer) 0
127.0.0.1:6379[2]> smembers myset #查看myset这个set
1) "hello"
3 sismember (value)判断set里面有没有这个元素,有的话返回1,没有的话返回0
127.0.0.1:6379[2]> sismember myset hello #判断myset中有没有hello这个元素
(integer) 1 #有的话返回1
127.0.0.1:6379[2]> sismember myset hellohe #判断myset中有没有hellohe这个元素
(integer) 0 #没有的话返回0
4 scard(set)查看set里面的元素的个数
127.0.0.1:6379[2]> scard myset #查看set里面元素的个数
(integer) 3
5 srem(set)(value)从set中删除指定的元素
127.0.0.1:6379[2]> srem myset hello #从myset中删除hello这个元素
(integer) 1 #返回1表示删除成功
127.0.0.1:6379[2]> srem myset hello2232 #从myset中删除hello2232 这个元素
(integer) 0 #返回0表示删除失败,因为没有这个元素
127.0.0.1:6379[2]> smembers myset #查看myset里面的元素
1) "hello1"
2) "hello2
6 smembers (set)(count)随机冲set取出几个(count)个元素 场景:随机数
127.0.0.1:6379[2]> smembers myset #查看myset中的全部元素
1) "hello1"
2) "hello3"
3) "hello2"
4) "hello4"
5) "hello5"
127.0.0.1:6379[2]> srandmember myset 1 #随机从myset中取出一个元素
1) "hello3"
127.0.0.1:6379[2]> srandmember myset 2 #随机从myset中取出2个元素
2) "hello2"
127.0.0.1:6379[2]> srandmember myset 3 #随机从myset中取出3个元素
1) "hello1"
2) "hello3"
3) "hello2"
7 spop (set)(count)随机移出几个元素
127.0.0.1:6379[2]> spop myset #随机移出1个元素
"hello4"
127.0.0.1:6379[2]> spop myset 2 #随机移出2个元素
1) "hello5"
2) "hello3"
127.0.0.1:6379[2]> smembers myset #查看全部的元素
1) "hello1"
2) "hello2"
8 smove(需要移出的set)(移出到哪的set)(需要移出的元素)把一个set中具体的元素移到另外一个set中
127.0.0.1:6379[2]> sadd set1 a #创建新的set添加元素
(integer) 1
127.0.0.1:6379[2]> sadd set1 b #创建新的set添加元素
(integer) 1
127.0.0.1:6379[2]> sadd set1 c #创建新的set添加元素
(integer) 1
127.0.0.1:6379[2]> sadd set2 d #创建新的set添加元素
(integer) 1
127.0.0.1:6379[2]> smove set1 set2 a #把set1中的a元素指定移出到set2中
(integer) 1
127.0.0.1:6379[2]> smembers set1 #查看set1
1) "b"
2) "c"
127.0.0.1:6379[2]> smembers set2 #查看set2
1) "a"
2) "d"
9 sdiff(set11)(set2) sinter(set11)(set2) sunion (set11)(set2) 求连个set的差集 交集 并集,那个set在前面就是以哪个set作为参照物
使用场景:
以下场景都是可以通过交集实现的
1 共同关注(A和B共同关注点人)
2 共同好友(我们之间的共同好友 )
3微博,A用户把所有关注点人放在一个set里,将它的粉丝也放在一个集合中 共同关注,共同好友,共同爱好,二度好友,推荐好友 数字集合类:
1 差集 sdiff(两个集合相差的元素)
2 交集 sinte (两个集合都有的元素)
3 并集 sunion (两个集合一共的元素)
127.0.0.1:6379[2]> sadd set a #创建set添加元素
(integer) 1
127.0.0.1:6379[2]> sadd set b #创建set添加元素
(integer) 1
127.0.0.1:6379[2]> sadd set c #创建set添加元素
(integer) 1
127.0.0.1:6379[2]> sadd set2 c #创建set添加元素
(integer) 1
127.0.0.1:6379[2]> sadd set2 d #创建set添加元素
(integer) 1
127.0.0.1:6379[2]> sadd set2 e #创建set添加元素
(integer) 1
127.0.0.1:6379[2]> sdiff set set2 #查看set和set2的差集
1) "a"
2) "b"
127.0.0.1:6379[2]> sinter set set2 #查看set和set2的交集
1) "c"
127.0.0.1:6379[2]> sunion set set2 #查看set和set2的并集
1) "c"
1) "b"
2) "c"
3) "a"
4) "d"
5) "e"
5 Zset(有序集合) 在set基础上增加了一个值可以进行排序
zet语法:set k1 v1
zset语法:zset k1 score v1
zset的命令都是z开头的
zset的使用场景:
1 排序,存储一组成绩表,工资表排序
2 普通消息,1,重要消息,
3 排行榜应用 Top N
1 zadd(zset)(k1)(v1) (k2)(v2)给zset添加多个元素
127.0.0.1:6379[2]> zadd myzset 1 one #给myzset这个zset添加元素
(integer) 1
127.0.0.1:6379[2]> zadd myzset 2 two 3 three #给myzset这个zset添加多个元素
(integer) 2
127.0.0.1:6379[2]>
2 zrange(zset)(index)(index)#查看zset中的元素 如果是0 -1这种区级的话就是查询全部的意思
127.0.0.1:6379[2]> zrange myzset 0 -1 #查看myzset的全部元素
1) "one"
2) "two"
3) "three"
3 zrangebyscore(zset)(min)(max)(withscore显示信息)用来进行正序或者倒叙排序并追加显示信息 -inf +inf就是负无穷到正无穷
127.0.0.1:6379[2]> zadd sal 100 weiyihe 200 yangyuting 300 weiyiming #新建一个工资的zset集合 添加三个用户数据 意味每个人的薪水
(integer) 3
127.0.0.1:6379[2]> zrangebyscore sal -inf +inf #查看这个zset的负无穷到正无穷也就是全部数据 显示全部的用户从小到大排序
1) "weiyihe"
2) "yangyuting"
3) "weiyiming"
127.0.0.1:6379[2]> zrangebyscore sal -inf +inf withscores #查看这个zset的负无穷到正无穷也就是全部数据,加上工资的参数
1) "weiyihe"
2) "100"
3) "yangyuting"
4) "200"
5) "weiyiming"
6) "300"
127.0.0.1:6379[2]> zrangebyscore sal -inf 100 #查看工资小于100的降序排列
1) "weiyihe"
127.0.0.1:6379[2]> zrangebyscore sal -inf 100 withscores #查看工资小于100的员工信息降序排序,外加工资 并且附带工资
1) "weiyihe"
2) "100"
127.0.0.1:6379[2]> zrangebyscore sal -inf 200 #查看工资小于200的员工信息降序排序
1) "weiyihe"
2) "yangyuting"
127.0.0.1:6379[2]> zrangebyscore sal -inf 300 #查看工资小于300的员工信息升序排序
1) "weiyihe"
4 zrem(zset)(key)根据指定的key从zset中删除
127.0.0.1:6379[2]> zrange sal 0 -1 #查看全部的sal的元素
1) "weiyihe"
2) "yangyuting"
3) "weiyiming"
4) "zhangsan"
127.0.0.1:6379[2]> zrem sal zhangsan #删除zhangsan这个key
(integer) 1
127.0.0.1:6379[2]> zrange sal 0 -1 #查看全部的元素
1) "weiyihe"
2) "yangyuting"
3) "weiyiming"
5 zcard(zset)查看zset中元素的数量
127.0.0.1:6379[2]> zcard sal #查看zset中元素的个数
(integer) 3
6 zrevrange(zset)(max)(min)从大到小进行排序
127.0.0.1:6379[2]> zrevrange sal 0 -1 #从大到小进行排序
1) "weiyiming"
2) "yangyuting"
3) "weiyihe"
7 zcount (zset)(start)(sotp)获取指定区间的成员数量
127.0.0.1:6379[2]> zadd myzset 1 a 2 b 3 c #一次性设置三个值
(integer) 3
127.0.0.1:6379[2]> zcount myzset 1 1 #查看取件1-1直接有几个值
(integer) 1
127.0.0.1:6379[2]> zcount myzset 1 2 #查看取件1-2直接有几个值
(integer) 2
127.0.0.1:6379[2]> zcount myzset 1 3 #查看取件1-3直接有几个值
(integer) 3
8. Redis的三种特殊数据类型
1 Geospatial(地理位置,在Redis3.2就已经推出来,它可以推算地理位置的信息) 比如说:朋友圈,附近的人,打车距离计算,两地之间的距离,方圆几里的人
Geospatial它只有6个命令
1 Geoadd 添加地理位置(纬度,经度,名称)
首先添加几个地理位置
127.0.0.1:6379[1]> geoadd china:city 116.40 39.90 beijing #添加一些城市的定位信息
(integer) 1
127.0.0.1:6379[1]> geoadd china:city 121.47 31.23 shanghai
(integer) 1
127.0.0.1:6379[1]> geoadd china:city 106.50 29.53 chongqing
(integer) 1
127.0.0.1:6379[1]> geoadd china:city 114.05 22.52 shenzhen
(integer) 1
127.0.0.1:6379[1]> geoadd china:city 120.16 30.24 hangzhou
(integer) 1
127.0.0.1:6379[1]> geoadd china:city 108.96 34.26 xian
(integer) 1
注意:添加地理位置的时候有一些规则
- 两级是无法添加的(南极,北极),我们一般会下载城市数据,直接用Java程序进行导入
- 参数 key 值(纬度,经度,名称)
- 有效的经度从-180度到180度
- 有效的纬度从-85.05112878到85.05112878
这个错误是因为坐标位置超出了指定的范围
127.0.0.1:6379[1]> geoadd china:city 39.90 116.40 beijing
(error) ERR invalid longitude,latitude pair 39.900000,116.400000
127.0.0.1:6379[1]>
这个错误是因为少了个.
127.0.0.1:6379[1]> geoadd china:city 106.50 2953 chongqing
(error) ERR invalid longitude,latitude pair 106.500000,2953.000000
127.0.0.1:6379[1]> geoadd china:city 106.50 29.53 chongqing
2 Geopos 获取指定的经度和纬度,可以一次性获取一个或者多个
获得当前定位:一定是一个坐标值
127.0.0.1:6379[1]> geopos china:city beijing #查看北京的距离
1) 1) "116.39999896287918091"
2) "39.90000009167092543"
127.0.0.1:6379[1]> geopos china:city beijing shanghai #查看北京,上海的距离
1) 1) "116.39999896287918091"
2) "39.90000009167092543"
2) 1) "121.47000163793563843"
2) "31.22999903975783553"
127.0.0.1:6379[1]> geopos china:city beijing shanghai chongqing
1) 1) "116.39999896287918091"
2) "39.90000009167092543"
2) 1) "121.47000163793563843"
2) "31.22999903975783553"
3) 1) "106.49999767541885376"
2) "29.52999957900659211"
127.0.0.1:6379[1]>
3 Geodist 返回两个定位之间的距离(绝对路径)
我们经常用到的两个人之间的距离就是它实现的
unit单位:
- M 米
- KM 千米
- Mi 英里
- Ft 英尺
127.0.0.1:6379[1]> geodist china:city beijing shanghai #查看北京到上海的距离
"1067378.7564"
127.0.0.1:6379[1]> geodist china:city beijing shanghai km #查看北京到上海的距离
"1067.3788"
127.0.0.1:6379[1]> geodist china:city beijing chongqing #查看北京到重庆的距离
"1464070.8051"
4 Georadius 以给定的经纬度中心,找出某一半径内的元素
场景:附近的人
首先获得所有附近的人的地址,手里上一般都会开启定位,通过半径来查询
一般都是给一个具体的位置或者城市
所有的数据都应该录入china:city中才会让结果更加清晰,没有符合的返回(empty list or set)
127.0.0.1:6379[1]> georadius china:city 110 30 1000 km #以110 30这个经纬度为中心查看附近1000km内的(城市)
1) "chongqing"
2) "xian"
3) "shenzhen"
4) "hangzhou"
127.0.0.1:6379[1]> georadius china:city 110 30 10 km #以110 30这个经纬度为中心查看附近10km的城市
(empty list or set)
127.0.0.1:6379[1]> georadius china:city 110 30 500 km #以110 30这个经纬度为中心查看附近500km的城市
1) "chongqing"
2) "xian"
127.0.0.1:6379[1]> georadius china:city 110 30 500 km withdist #显示到中心距离的位置
1) 1) "chongqing"
2) "341.9374"
2) 1) "xian"
2) "483.8340"
127.0.0.1:6379[1]> georadius china:city 110 30 500 km withcoord
1) 1) "chongqing"
2) 1) "106.49999767541885376"
2) "29.52999957900659211"
2) 1) "xian"
2) 1) "108.96000176668167114"
2) "34.25999964418929977"
127.0.0.1:6379[1]> georadius china:city 110 30 500 km withcoord count 1 #筛选出指定的结果 1个
1) 1) "chongqing"
2) 1) "106.49999767541885376"
2) "29.52999957900659211"
127.0.0.1:6379[1]> georadius china:city 110 30 500 km withcoord count 2 #2个
1) 1) "chongqing"
2) 1) "106.49999767541885376"
2) "29.52999957900659211"
2) 1) "xian"
2) 1) "108.96000176668167114"
2) "34.25999964418929977"
127.0.0.1:6379[1]>
5 georadiusbymember 找出位于指定元素周围的其他元素
127.0.0.1:6379[1]> GEORADIUSBYMEMBER china:city beijing 1000 km #找出位于指定元素周围的其他元素 距离北京1000km的其他元素
1) "beijing"
2) "xian"
127.0.0.1:6379[1]> GEORADIUSBYMEMBER china:city beijing 1000 km count 1 #找出位于指定元素周围的其他元素 距离北京1000km的其他元素中的一个元素
1) "beijing"
6 Geohash 返回一个或者多个位置元素的geohash标识
返回一个11位geohash字符的字符串
如果两个字符串越接近(长的越像)标识距离越接近
127.0.0.1:6379[1]> geohash china:city beijing chongqing #将二维经纬度转换成一个11位的字符串
1) "wx4fbxxfke0"
2) "wm5xzrybty0"
该命令返回一个11个字符的geohash字符串
Geo底层实现原理其实就是zset(有序集合),我们可以使用zset命令来操作geo
127.0.0.1:6379[1]> zrange china:city 0 -1 #查看全部的china:city全部的元素
1) "chongqing"
2) "xian"
3) "shenzhen"
4) "hangzhou"
5) "shanghai"
6) "beijing"
127.0.0.1:6379[1]> zrem china:city beijing #删除北京
(integer) 1
127.0.0.1:6379[1]> zrange china:city 0 -1 #查看全部的china:city
1) "chongqing"
2) "xian"
3) "shenzhen"
4) "hangzhou"
5) "shanghai"
2 Hyperloglog(基数统计的算法,Redis2.8.9版本就已经更新出来了Hyperloglog 的数据结构,Hyperloglog有着0.81%的错误率,是可以忽略不计的)
学习之前先知道,什么是基数?
比如我有两个数据集 一个为A{1,3,5,7,8,7} 一个为B{1,3,5,7,8}
那么A和B的基数(不重复的元素)为5(1.3,5,7,8),可以接受误差
Hyperloglog的优点
占用的内存是固定的(比如说我想放2的64次方不同元素的基数,只需要费12KB的内存),如果要从内存角度来比较的话,Hyperloglog肯定是我们的首选
应用场景
网页的UV(页面访问量,一个人访问一个网站多次,但是还是算作一个人)Hyperloglog有着0.81%的错误率,用作统计UV任务时,是可以忽略不计的(如果允许容错,那么一定可以使用Hyperloglog,如果不允许容错的话可以使用下面的传统set方式或者自己的数据类型即可)
传统的方式,使用set集合的方式保存用户的id等信息(因为set是不允许重复的,然后就可以统计出set中的元素数量作为标准判断),这个方式如果保存大量的用户id,就会比较麻烦(比较占用内存,我们的目的是为了计数,而不是保存用户信息)
Hyperloglog的命令
1 pfadd(name)(v1)(v2)(v3)…创建一组元素并且可以一次性添加多个值
2 pfcount(name)统计一组元素中的基数数量
3 pfmerge (new)(od1) (od2)统计两个老元素中的并集结果集并创建返回给新元素
127.0.0.1:6379[2]> pfadd mykey a b c d e f g h i j #创建第一组元素一次性添加多个值多个值
(integer) 1 #创建成功
127.0.0.1:6379[2]> pfadd mykey2 i j z x c v b n m #创建第二组元素一次性添加多个值多个值
(integer) 1 #创建成功
127.0.0.1:6379[2]> pfcount mykey #统计mykey中基数数量
(integer) 10 #有10个元素
127.0.0.1:6379[2]> pfcount mykey2 #统计mykey2中基数数量
(integer) 9 #有9个元素
127.0.0.1:6379[2]> pfmerge mykey3 mykey mykey2 #合并mykey mykey2把结果集(并集)赋给新元素mykey3
OK
127.0.0.1:6379[2]> pfcount mykey3 #统计mykey3中基数数量
(integer) 15 #有15个元素
127.0.0.1:6379[2]> pfadd k 1 #一次性添加一个值
(integer) 1
127.0.0.1:6379[2]> pfadd k 2 #一次性添加二个值
(integer) 1
127.0.0.1:6379[2]> pfcount k #统计k中基数数量
(integer) 2
3 Bitmap(位图,数据结构,都是操作二进制来进行记录,只有两个状态)
按位存储
Bitmap的使用场景:
1 统计用户信息,活跃,不活跃
2 打卡,两个状态的,都可以使用Bitmap
Bitmap的方法:
场景:使用bitmaps来实现周一到周五的打卡,下表从0开始打卡:1 不打卡:0,判断打卡天数,只需要统计状态为1的即可,例如:周一(0)打卡(1),周二(1)不打卡(0)
1 setbit(name)(sign1) (sign2) 设置元素,一般有两个状态
2 getbit(name)(sign) 根据状态获取某一个值
3 bitcount(name)统计全部的元素个数
1127.0.0.1:6379[2]> setbit sign 0 1 #添加元素例如:周一(0)打卡(1),周二(1)不打卡(0)
(integer) 0
127.0.0.1:6379[2]> setbit sign 1 0 #添加元素例如:周一(0)打卡(1),周二(1)不打卡(0)
(integer) 0
127.0.0.1:6379[2]> setbit sign 2 0 #添加元素例如:周一(0)打卡(1),周二(1)不打卡(0)
(integer) 0
127.0.0.1:6379[2]> setbit sign 3 1 #添加元素例如:周一(0)打卡(1),周二(1)不打卡(0)
(integer) 0
127.0.0.1:6379[2]> setbit sign 4 1 #添加元素例如:周一(0)打卡(1),周二(1)不打卡(0)
(integer) 0
127.0.0.1:6379[2]> getbit sign 0 #查看周一是否打卡
(integer) 1 #打卡
127.0.0.1:6379[2]> getbit sign 1 #查看周二是否打卡
(integer) 0 #没打卡
127.0.0.1:6379[2]> bitcount sign #查看周一到周五打卡的全部天数
(integer) 3
8. Redis基础的一些知识和命令
命令
- ping(有没有连接到.返回pong表示连接成功)
- set get(set name wyh get name)
- 清除全部的数据库内容 flushall
- 清除当前数据库flushdb
- Keys(*)查看全部的键
- exists(key) 查看键是否存在,存在的话返回1,不存在返回0
- select进行切换数据库
- clear清屏
- move key numb (键,移动到那个数据库) 移动属性到指定的数据库
- expire (name 10 )设置某个键的过期时间(单位是秒s)
- ttl (key) 查看当前某个键的剩余时间,过期的话返回负数
- type 查看数据类型 type(key)
127.0.0.1:6379[5]> ping #测试连接
PONG #连接成功
127.0.0.1:6379[1]> select 0 #切换数据库0
OK
127.0.0.1:6379> select 1 #切换数据库0
OK
127.0.0.1:6379[1]> set name wyh #设置key键
OK
127.0.0.1:6379[1]> set age 22 #设置key键
OK
127.0.0.1:6379[1]> keys * #查看全部的键
1) "name"
2) "age"
127.0.0.1:6379[1]> get name
"wyh"
127.0.0.1:6379[1]> exists age #判断是否存在
(integer) 1
127.0.0.1:6379[1]> flushdb # 刷新当期数据库
OK
127.0.0.1:6379[1]> flushall #刷新全部数据库
OK
127.0.0.1:6379[1]> clear #清空
127.0.0.1:6379[2]> set animal dog #设置属性
OK
127.0.0.1:6379[2]> move animal 5 #把animal键移动到数据库5
(integer) 1
127.0.0.1:6379[2]> keys * #查看全部的键
1) "age"
127.0.0.1:6379[2]> select 5 #切换到数据库5
OK
127.0.0.1:6379[5]> keys * #查看全部的键
1) "animal"
127.0.0.1:6379[2]> expire name 10 #设置过期时间10秒
(integer) 1 #成功的话返回1 不成功返回0
127.0.0.1:6379[2]> ttl name #查看剩余过期时间
(integer) 6
127.0.0.1:6379[2]> ttl name #查看当前key的剩余过期时间
(integer) 1
127.0.0.1:6379[2]> ttl name #查看当前key的剩余过期时间
(integer) -2 #已经过期2秒
127.0.0.1:6379[2]> get name #查看name,已经过期
(nil) #没有了,说明已经过期
127.0.0.1:6379[2]> type age #查看key的类型
string
127.0.0.1:6379[2]> type name #查看key的类型
string
知识
- redis默认有16个数据库
- 默认的使用的是第0个
- 可以使用select进行切换数据库!
- 清除全部的数据库内容 flushall
- 清除当前数据库
- …
127.0.0.1:6379[1]> select 0 #切换数据库
OK
127.0.0.1:6379> select 1
OK
127.0.0.1:6379[1]> set name wyh
OK
127.0.0.1:6379[1]> set age 22
OK
127.0.0.1:6379[1]> keys * #查看全部的键
1) "name"
2) "age"
127.0.0.1:6379[1]> get name
"wyh"
127.0.0.1:6379[1]> exists age #判断是否存在
(integer) 1
127.0.0.1:6379[1]> flushdb # 刷新当期数据库
OK
127.0.0.1:6379[1]> flushall #刷新全部数据库
OK
127.0.0.1:6379[1]> clear #清空3
9. Redis为什么单线程还这么快?
Redis是C语言写的,官方提供的数据为:100000W+的QPS,完全不比同样是使用key-value的Memecached差
10. Redis是单线程的,为什么?
明白第一个目标:Redis是很快的,官方标识,Redis是基于内存进行操作的,CPU不是Redis的性能瓶颈,Redis的性能瓶颈是根据机器的内容和网络带宽.既然可以使用单线程来实现,所以就使用单线程了
误区:
1 高性能的服务器一定是多线程的
2. 多线程一定比单线程(CPU上下文切换,非常耗时的操作)快,效率高 CPU>内存>硬盘
核心:
redis是将全部的数据存放到内存中的,所以使用单线程去操作效率就是最高的.对于内存数据来说,如果没有上下文切换效率就是最高的!多次读写都是在一个CPU上的,在内存情况下,这个就是最佳的方案
11. Redis的事务
首先回顾:关系型数据库的事务(ACID)
Redis加锁是使用watch这个命令
Redis事务的本质:
一组命令的集合,一个事物中的所有命令都会被序列化,在事务执行过程中会按照顺序执行(一次性,顺序性,排他性)
注意
Redis单条命令是保证原子性的,但是事务不保证原子性,Redis事务没有隔离级别的概念(不会出现关系型数据库的脏读,幻读,重复度这种情况),所有的命令在事务中,并没有被直接执行,只有发起执行命令的时候才会执行!Exec(执行)
Redis事务和关系型数据库的事务区别
Redis事务没有隔离级别的概念(不会出现关系型数据库的脏读,幻读,重复度这种情况)
Redis事务分为三个阶段
1 开启事务(multi)
2 命令入队(…命令入队(进入队列)…)
3 执行事务(exec)每一次执行事务,执行完的时候这个事务就没了,需要重新开启事务
锁:redis可以实现乐观锁,通过watch(监视器)实现
Redis事务的几个常用场景
1 正常开启执行事务(multi … exec )
127.0.0.1:6379[2]> multi #开启事务
OK
127.0.0.1:6379[2]> set k1 v1 #设置键值
QUEUED #
127.0.0.1:6379[2]> set k2 v2 #设置键值
QUEUED #命令入队(进入队列)
127.0.0.1:6379[2]> get k1 #根据键获得值
QUEUED
127.0.0.1:6379[2]> exec #执行事务,输出队列结果
1) OK
2) OK
3) "v1"
2 放弃事务(multi … discard)事务中的队列都不会被执行
127.0.0.1:6379[2]> multi #开启事务
OK
127.0.0.1:6379[2]> set k1 v1 #设置键值
QUEUED #命令入队(进入队列)
127.0.0.1:6379[2]> set k2 v2 #设置键值
QUEUED #命令入队(进入队列)
127.0.0.1:6379[2]> set k3 v3 #设置键值
QUEUED #命令入队(进入队列)
127.0.0.1:6379[2]> discard #放弃事务
OK
#获取不到,因为事务中的队列都不会被执行(事务被放弃了并没有被执行)
127.0.0.1:6379[2]> get k3
(nil)
127.0.0.1:6379[2]> get k2
(nil)
127.0.0.1:6379[2]> get k1
(nil)
3 事务异常(以Java举例)
1 编译型异常(Java:代码有问题!命令有错),Redis:事务中所有的命令都不会执行
127.0.0.1:6379[2]> multi #开启事务
OK
127.0.0.1:6379[2]> set k1 v1 #设置键值
QUEUED
127.0.0.1:6379[2]> set k2 v2 #设置键值
QUEUED
127.0.0.1:6379[2]> getset k2 #这里发生错误,使用错误的命令
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379[2]> set k3 v3 #设置键值
QUEUED
127.0.0.1:6379[2]> exec #执行事务
(error) EXECABORT Transaction discarded because of previous errors. #这个事务含有错误代码
#获取不到,因为事务中代码发生了错误,执行事务报错,所以的命令都不会执行
127.0.0.1:6379[2]> get k1
(nil)
127.0.0.1:6379[2]> get k3
(nil)
2 运行时异常(比如Java 1/0) Redis:如果事务队列中存在语法性,那么执行命令的时候,其他命令可以正常执行的,错误命令会抛异常,但是没有原子性这么一说
127.0.0.1:6379[2]> multi #开启事务
OK
127.0.0.1:6379[2]> set k1 v1 #设置键值
QUEUED
127.0.0.1:6379[2]> incr k1 #这里执行错误代码(给字符串自增1), incr 只能给整形元素用,
QUEUED
127.0.0.1:6379[2]> set k2 v2 #设置键值
QUEUED
127.0.0.1:6379[2]> set k3 v3 #设置键值
QUEUED
127.0.0.1:6379[2]> exec #执行事务
1) OK
2) (error) ERR value is not an integer or out of range #虽然它报错了,但是后面的命令都会被执行成功
3) OK
4) OK
127.0.0.1:6379[2]> get k2
"v2"
127.0.0.1:6379[2]> get k3
"v3"
12. Redis的锁(一般用于分布式)(取钱这种场景都要加锁(watch))
悲观锁
很悲观,认为什么时候都会出问题,无论做什么(操作)都会加锁(这种情况会非常影响性能,无论干什么都会加锁,用完之后再去解锁)
乐观锁
很乐观,认为什么时候都不会出现问题,每次去拿数据的时候都会觉得不会进行修改,所以不会上锁,不过更新数据的时候会判断在此期间有没有进行数据的修改(是否有人修改这个数据 )
乐观锁在Redis中主要有两个操作步骤
1 获取version
2 更新的时候比较version
使用场景:秒杀业务
Redis的监视测试(监视:watch Redis加锁也是使用watch这个命令)
场景1(单线程操作),我有100元,另外一个人0元,我给另外一个人转20,我剩80,他有20
127.0.0.1:6379[2]> set money 100 #我有100
OK
127.0.0.1:6379[2]> set out 0 #他有0
OK
127.0.0.1:6379[2]> watch money #监视money对象(我的钱)
OK
127.0.0.1:6379[2]> multi #开始事务 事务正常结束 数据期间没有发生变动 这个时候就正常执行成功
OK
127.0.0.1:6379[2]> decrby money 20 #我自建少20
QUEUED
127.0.0.1:6379[2]> incrby out 20 #他自增20
QUEUED
127.0.0.1:6379[2]> exec #执行事务
1) (integer) 80 #我有80
2) (integer) 20 #他有20
场景2(多线程操作),我有100元,另外一个人0元,我给另外一个人转20,这时候我工资到账我的钱发送改动watch会告诉事务我的钱发生变化,所有的事务都会操作失败
主线程转钱
127.0.0.1:6379[2]> set money 100 #我有100
OK
127.0.0.1:6379[2]> set out 0 #他有0
OK
127.0.0.1:6379[2]> watch money #监视对象
OK
127.0.0.1:6379[2]> multi #开启事务
OK
127.0.0.1:6379[2]> decrby money 20 #我的钱自减20,但是这时候副线程执行,我的钱发生变化
QUEUED
127.0.0.1:6379[2]> incrby out 20 #他的钱自增20
QUEUED
127.0.0.1:6379[2]> exec #执行事务 执行之前,副线程修改了值,这个时候就会导致事务执行失败
(nil) #执行失败,因为在转账后我的钱发生变化,事务执行失败
副线程工资到账(改变钱)
127.0.0.1:6379[2]> get money #查看当前的钱
"100"
127.0.0.1:6379[2]> set money 1000 #资金在转账过之后发生改动
OK
场景3(解锁 unwatch),放弃当前对象重新获取最新对象进行操作
如果修改失败,获取最新的值即可
步骤如下
1 如果发现事务执行失败,就先解锁
2 获取最新的值,再次监视
3 获取到最新的值之后再去执行事务
4 对监视的值有没有发生变化,如果没有变化,那么可以执行成功,如果发生变化就会执行失败,然后重新解锁再获取最新的锁
127.0.0.1:6379[2]> unwatch #解锁(放弃对象) 如果发现事务执行失败,就先解锁
OK
127.0.0.1:6379[2]> watch money #重新获取对象 获取最新的值,再次监视
OK
127.0.0.1:6379[2]> multi #开启事务 获取到最新的值之后再去执行事务
OK
127.0.0.1:6379[2]> decrby money 20
QUEUED
127.0.0.1:6379[2]> incrby out 20
QUEUED
127.0.0.1:6379[2]> exec #执行事务 比对监视的值有没有发生变化,如果没有变化,那么key执行成功,如果发生变化就会执行失败
1) (integer) 960
2) (integer) 30
多线程操作乐观锁结论:多线程修改值,使用watch可以当做Redis的乐观锁操作
13. Jedis(Java操作Redis的客户端)
1 什么是Jedis?
Jedis是官方推荐的Java连接开发工具,使用Java操作Redis的中间件,Jedis中的命令和Redis中是完全一致的
2 如何操作Jedis?
1 创建一个maven项目,或者空项目引入依赖也行(开发工具以IDEA为例)
2 引入Jedis的包(Jedis和fastjson)
<!--导入Jedis的包(依赖)-->
<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>
3 编写代码进行测试(大概步骤如下三条)
1 连接数据库
2 操作命令
3 结束测试(断开连接 shutdown)
4 基本使用Jedis连接Redis操作的代码
package com.wyh;
import redis.clients.jedis.Jedis;
/**
* @program: redis
* @description: 连接Redis数据库
* @author: 魏一鹤
* @createDate: 2021-11-11 23:20
**/
public class RedisTest {
public static void main(String[] args){
// 1.new 一个Jedis
Jedis jedis = new Jedis("127.0.0.1",6379); //本地服务加端口号
//jedis中的所有命令和redis中的命令都是完全相同的
System.out.println(jedis.ping());//返回pong表示本地redis连接成功
String set = jedis.set("name", "wyh"); //设置一个键值对
String name = jedis.get("name"); //获取name这个键对应的值
System.out.println(name);
}
}
5 Jedis操作事务
package com.wyh;
import com.alibaba.fastjson.JSONObject;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
/**
* @program: redis
* @description: redis实现事务
* @author: 魏一鹤
* @createDate: 2021-11-14 17:00
**/
public class RedisTransaction {
public static void main(String[] args){
//连接redis
Jedis jedis = new Jedis("127.0.0.1", 6379);
//刷新redis数据库
jedis.flushDB();
JSONObject jsonObject = new JSONObject();
jsonObject.put("key1","value1");
jsonObject.put("key2","value2");
//开启事务
Transaction multi = jedis.multi();
//转化json数据
String jsonString = jsonObject.toJSONString();
//命令入队(进入队列)
try {
multi.set("user1",jsonString);
multi.set("user2",jsonString);
//代码抛出异常,事务执行失败
int num=1/0;
//成功就执行事务
multi.exec();
} catch (Exception e) {
//失败就放弃事务
multi.discard();
//自动捕获异常并打印出来
e.printStackTrace();
} finally {
System.out.println(jedis.get("user1"));
System.out.println(jedis.get("user2"));
//关闭redis
jedis.close();
}
}
}
14. SpringBoot整合Redis
SpringBoot操作数据:spring-data jpa jdbc mongodb redis
SpringData是和SpringBoot齐名的项目
如果是SpringBoot2.0以上配置集群的话一定要使用lettuce.pool下面的属性
在SpringBoot2.x之后 我们的jedis被替换成了lettuce
jedis和lettuce有什么区别呢?
jedis:
用作于2.0之前,底层采用的是直连的serve,多个线程操作的话是不安全的,如果想要避免这种情况,需要使用jedis的连接池来解决 更像BIO(阻塞)模式(dubbo)
lettuce:
底层采用netty(高性能的网络框架,异步请求),实例key在线程中进行共享,不存在线程不安全的情况,可以减少线程数据,性能更加的高 更像NIO模式
源码分析:
//原文:如果不存在bean才生效 也就是告诉我们可以自己定义一个RedisTemplate来替换这个默认的
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
//默认的RedisTemplate 没有过多的设置,redis对象的保存都是需要序列化的,尤其是使用netty NIO这种异步的
//两个泛型都是object类型,后面使用需要强制转换,我们期望的数据类型应该是<string,object>
RedisTemplate<Object, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
//由于string类型是redis最长使用的数据类型,所以单独提出来了一个方法(bean)
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
综上所述,我们整合测试一下
1.导入依赖(pom)
<!--redis依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2.配置连接(application.properties(yml))
# SpringBoot所有的配置类,都有一个自动配置类 RedisAutoConfiguration
# 自动配置类都会绑定一个properties配置文件 RedisProperties
#配置redis的ip 如果是本机的话可以写localhost或者127.0.0.1 如果是远程的话就写远程的ip即可
spring.redis.host=localhost
#配置redis的端口号 默认都是6379
spring.redis.port=6379
#配置redis使用的数据库 注:redis共有16个数据库 默认使用第0个
spring.redis.database=2
#配置redis的集群 (如果是SpringBoot2.0以上配置集群的话一定要使用lettuce.pool下面的属性)
spring.redis.lettuce.pool
3.测试
package com.wyh;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisTemplate;
@SpringBootTest
class SpringBootRedis01ApplicationTests {
//注入RedisTemplate
@Autowired
private RedisTemplate redisTemplate;
@Test
void contextLoads() {
//很多数据类型api命令是以OpsFor开头的,操作不同的数据类型,api用法和redis类似
//redisTemplate.opsForSet(); // 集合
//redisTemplate.opsForZSet(); //有序集合
//redisTemplate.opsForList(); //list列表
//redisTemplate.opsForHash(); //Hash哈希
//redisTemplate.opsForValue(); //字符串
//redisTemplate.opsForGeo(); //地理位置
//redisTemplate.opsForHyperLogLog(); //基数统计
//除了基本的操作,我们常用的方法都可以通过RedisTemplate操作,比如事务和基本的CRUD(增删改查)
//redisTemplate.multi(); //事务
//获取Redis连接
//RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
// connection.flushDb(); //刷新数据库
// connection.close(); //关闭数据库
//设置字符串 尽量不要用中文(转义)
redisTemplate.opsForValue().set("name","wyh");
redisTemplate.opsForValue().set("name1","魏一鹤");
//获取字符串
System.out.println( redisTemplate.opsForValue().get("name"));
System.out.println( redisTemplate.opsForValue().get("name1"));
}
}
4 序列化配置
默认的序列化方式是JDK序列化,对字符串转义,我们可能会使用JSON来进行序列化,这时候我们就需要自己进行配置一个redisTemplate类了
5 自定义RedisTemplate(自带的不能存储中文和对象,需要我们进行更改)
例子1:创建user实体,name(中文存储到redis),age存储到redis,不进行序列化
user对象实体
package com.wyh.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Component;
import java.io.Serializable;
/**
* @program: SpringBoot-Redis-01
* @description: User实体
* @author: 魏一鹤
* @createDate: 2021-11-18 23:30
**/
@Data //自动get set生成(lombok依赖)
@Component //进行注入到spring工厂成为一个组件,方便我们使用
@AllArgsConstructor //生成有参构造函数(lombok依赖)
@NoArgsConstructor //生成无参构造函数(lombok依赖)
//目前我们的User实体是没有被序列化的
//想要实现序列化只需在User后加implements Serializable(实现序列化接口)
public class User {
private String name;
private int age;
}
测试方法
//注入RedisTemplate
@Autowired
private RedisTemplate redisTemplate;
@Test//表示该方法是测试方法(junit依赖)
public void setUser() throws JsonProcessingException {
//真实的开发一般传递json来传递对象,而不是直接new出来对象
User user = new User("魏一鹤", 22);
//利用ObjectMapper把我们的User对象进行json格式化
String jsonUser = new ObjectMapper().writeValueAsString(user);
//把json格式化后的user对象以键值对应的方式存在redis中
redisTemplate.opsForValue().set("user",jsonUser);
//通过key获取user对象
System.out.println( redisTemplate.opsForValue().get("user"));
}
打印输出
{"name":"魏一鹤","age":22}
redis中查看中文乱码
127.0.0.1:6379> keys *
1) "\xac\xed\x00\x05t\x00\x04user" #中文乱码
例子2:创建user实体,name(中文存储到redis),age存储到redis,进行序列化
代码和上面类似,只不过传的对象从json变成实体,也就是没有经过json字符串转化直接传对象
@Test//表示该方法是测试方法(junit依赖)
public void setUser() throws JsonProcessingException {
//真实的开发一般传递json来传递对象,而不是直接new出来对象
User user = new User("魏一鹤", 22);
//利用ObjectMapper把我们的User对象进行json格式化
// String jsonUser = new ObjectMapper().writeValueAsString(user);
//把json格式化后的user对象以键值对应的方式存在redis中
//直接存储对象,而不是存储json数据类型
redisTemplate.opsForValue().set("user",user);
//redisTemplate.opsForValue().set("user",jsonUser);
//通过key获取user对象
System.out.println( redisTemplate.opsForValue().get("user"));
}
此时会报错我们的实体User没有进行序列化
org.springframework.data.redis.serializer.SerializationException: Cannot serialize; nested exception is org.springframework.core.serializer.support.SerializationFailedException: Failed to serialize object using DefaultSerializer; nested exception is java.lang.IllegalArgumentException: DefaultSerializer requires a Serializable payload but received an object of type [com.wyh.entity.User]
at org.springframework.data.redis.serializer.JdkSerializationRedisSerializer.serialize(JdkSerializationRedisSerializer.java:96)
原因:我们传递的所有对象,都需要序列化(实现 implements Serializable接口),传session是可以直接传的不用经过序列化
解决:把我们的user实体进行序列化 目前企业中 我们所以的pojo(Java对象)都会进行序列化
@Data //自动get set生成(lombok依赖)
@Component //进行注入到spring工厂成为一个组件,方便我们使用
@AllArgsConstructor //生成有参构造函数(lombok依赖)
@NoArgsConstructor //生成无参构造函数(lombok依赖)
//想要实现序列化只需在User后加implements Serializable(实现序列化接口)
//目前企业中 我们所以的pojo(Java对象)都会进行序列化
public class User implements Serializable {
private String name;
private int age;
}
再次运行发现会成功
不过此时的redis中我们存储的user依然是乱码
我们如何解决我们的序列化问题呢?
redis默认的序列化是jdk序列化,我们需要更换我们的redis模板(redis Template)
自定义的模板redisTemplate
我们自己编写一个自定义的模板redisTemplate,可以在企业中直接使用,进行各种序列化操作
package com.wyh.condig;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @program: SpringBoot-Redis-01
* @description: redis配置类,这是一个固定模板,可以在企业中直接使用
* @author: 魏一鹤
* @createDate: 2021-11-14 18:56
**/
@Configuration //配置类一般都会用到的注解,注入到spring工厂
public class RedisConfig {
//编写我们自己的redisTemplate类
//自己定义了一个redisTemplate 只要有几下几个改动操作
//1把我们的双泛型从<Object,Object>改成了<String,Object>方便我们后续使用
@Bean
//简介:java.lang.SuppressWarnings是J2SE5.0中标准的Annotation之一。可以标注在类、字段、方法、参数、构造方法,以及局部变量上。
//作用:告诉编译器忽略指定的警告,不用在编译完成后出现警告信息。
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplateWYH(RedisConnectionFactory redisConnectionFactory) {
//我们一般为了自己开发方便 一般直接使用<String,Object>
RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
//连接工厂
template.setConnectionFactory(redisConnectionFactory);
//序列化配置
//创建jackson序列化方式,并对jackson进行序列化配置
Jackson2JsonRedisSerializer<Object> objectJackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
//对我们的jackson对象进行转义
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectJackson2JsonRedisSerializer.setObjectMapper(objectMapper);
//序列化String类型
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
//key采用String的序列化方式
template.setValueSerializer(stringRedisSerializer);
//hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
//value序列化采用jaskson
template.setValueSerializer(objectJackson2JsonRedisSerializer);
//hash的value序列化方式采用jackson
template.setHashValueSerializer(objectJackson2JsonRedisSerializer);
//配置具体的序列化方式
template.setKeySerializer(objectJackson2JsonRedisSerializer);
//初始化参数和初始化工作
template.afterPropertiesSet();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
然后使用我们自己写的配置模板进行序列化
@Qualifier("redisTemplateWYH") //如果有多个名称相同的 指定具体是哪一个 redisTemplateWYH是我的方法名 下面这个是我自己写的而不是框架的 字符串里面的值要和用到的那个保持一致
private RedisTemplate redisTemplate;
调用刚才的方法直接存储对象成功,然后去redis中查看,已经序列化成功
企业应用:自定义封装我们的redisUtil
package com.wyh.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
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;
/**
* @program: SpringBoot-Redis-01
* @description: redis工具类
* @author: 魏一鹤
* @createDate: 2021-11-21 22:26
**/
@Component
@Qualifier("redisTemplateWYH") //如果有多个名称相同的类 指定具体是哪一个
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;
}
}
}
测试我们的redisutil
//注入我们的redisUtil
@Autowired
private RedisUtil redisUtil;
//测试我们的redisUtil
@Test
public void testUtil() {
redisUtil.set("name","wyh");
System.out.println(redisUtil.get("name"));
}
redis的作者以前迷恋一个女明星,她的名称缩写用九键进行输入就是6379
14 Redis.conf配置文件的一些配置
启动的时候,就通过配置文件来启动的
1 配置文件 util单位 kg kG Kg 对大小写不敏感
# 1k => 1000 bytes
# 1kb => 1024 bytes
# 1m => 1000000 bytes
# 1mb => 1024*1024 bytes
# 1g => 1000000000 bytes
# 1gb => 1024*1024*1024 bytes
2 include包含 就好比我们学习spring中的import jsp中的include标签
网络
bind 127.0.0.1 #绑定ip 默认是本机 当然也可以换成远程的
protected-mode yes #保护模式 默认是开启的
port 6379 #端口 默认6379 集群一定是要修改端口的 至少搭建三个redis
3 通用配置GENERAL(全体的)
daemonize yes #以守护进程的方式运行,默认是no,我们需要自己开启为yes 不然我们一退出进程就结束了,那肯定是不行的
pidfile/var/run/redis_6379.pid #如果以后台的方式运行,我们就需要指定一个pid文件(进程文件)
loglevel notice #日志级别 还有debug,verbose,warning这几种
#debug(排查)一般用于测试开发阶段
#verbose记录较多的日志信息
#notice(通知)只会记录一些重要的日志,仅适用于生产环境,也是我们默认使用的级别
#warning(警告)打印关键的信息
日志打印出来生成文件
logfile "" #日志的文件位置名 如果为空就是一个标准的输出
database 16 #数据库的数量 默认是16个数据库
always-show-log yes #是否总是显示log 也就是我们redis客户端的那个log
4 持久层(RDB) 快照
SNAPSHOTTING 做持久化会用到,在规定的时间内执行了多少次操作,则会持久化,也就是生成一个快照到我们的文件 一般文件格式有两种 .rdb文件 .aof文件
redis是内存数据库,如果不进行持久化数据会丢失
save 900 1 #如果900秒内 如果至少1 个key进行了修改 我们就进行持久化操作
save 300 10 #如果300秒内 如果至少10个key进行了修改 我们就进行持久化操作
save 60 10000 #如果60秒内 如果至少10000个key进行了修改 我们就进行持久化操作 一般用于高并发
#之后学习持久化 会自己定这个并做相关的测试
stop-writes-on-bgsave-error yes #持久化出现错误之后(是否继续进行工作) 也会继续工作 默认开启yes
rdbcompression yes #是否压缩我们的rdb文件 默认也是yes开启的 压缩的话就会消耗浪费我们的cpu资源
rdbchecksum yes #保存rdb文件的时候进行错误的检查校验 是否校验(检查)rdb文件 如果出错了就会自动进行修复 默认也是yes开启的
dir ./ #rdb文件默认保存的目录! 默认是当前目录
5 REPLICATION主从复制 一般用于搭建多个redis
6 SECURITY安全
可以设置redis的密码.redis默认是没有密码的,但是一般服务器都要设置密码的
requirepass foobared #设置密码 redis默认是没有密码的
查看密码,以及设置密码的命令或者方式
127.0.0.1:6379> config get requirepass #查看密码
1) "requirepass"
2) "" #redis默认是没有密码的
设置密码方式1:在conf文件插入密码(不推荐)
清除密码的话就把新增密码那行删除
设置密码方式2:用命令(推荐)
127.0.0.1:6379> config set requirepass wyh19991101 #设置redis密码
OK #设置成功
127.0.0.1:6379> config get requirepass #查看密码
(error) NOAUTH Authentication required. #查看失败 发现所有的命令都没有权限 应该先登录
127.0.0.1:6379> ping #默认情况下我们的ping命令是key不带密码就能使用的,但是如果我们给redis设置了密码,那么我们的redis就会把重要的命令后面都要带上我们的密码才能执行
(error) NOAUTH Authentication required. #没有密码不能执行
127.0.0.1:6379> auth wyh19991101 #输入验证密码
OK #验证成功
127.0.0.1:6379> ping #此时的ping就能正常使用了
PONG
127.0.0.1:6379> config get requirepass #查看我们的密码
1) "requirepass"
2) "wyh19991101"
清除密码的话就使用命令把密码设置为空
127.0.0.1:6379> config set requirepass "" #设置redis密码为空 也就是取消密码
OK #设置成功 我们一些命令比如ping就可以直接使用而不用跟上密码使用
7 限制client客户端
maxclients 10000 #设置能连上redis最大客户端的数量 默认10000个
maxmemory <bytes> #redis配置最大的内存容量 默认是字节
maxmemory-policy noeviction #内存到大上限之后的处理策略
#移出一些过期的key
#报错
#。。。。。。。。
#一般有以下六种处理策略
maxmemory-policy 六种方式
1、volatile-lru:只对设置了过期时间的key进行LRU(默认值)
2、allkeys-lru : 删除lru算法的key
3、volatile-random:随机删除即将过期key
4、allkeys-random:随机删除
5、volatile-ttl : 删除即将过期的
6、noeviction : 永不过期,返回错误
8 APPEND ONLY MODE模式 持久层(AOF)
appendonly no #默认是不开启AOF模式的,默认是使用RDB方式持久化的,在大部分情况下,RDB完全够用了
appendfilename "appendonly.aof" #持久化文件的名字 RDB持久化文件袋的名字yes后缀.rdb
# appendfsync always #每次修改都会同步写入sync,消耗性能速度比较慢,因为每次都要读写
appendfsync everysec #默认使用这个 每秒执行一次sync,可能会丢失这1s点数据,万一这1s宕机了
# appendfsync no #不执行 sync 这个时候操作系统自己同步数据,速度最快,但是一般不用
15 Redis持久化
前言:Redis是内存数据库,如果不将内存中的数据库状态保存到磁盘,那么一旦服务器进程退出,服务器中的数据库状态也会随之消失(断电及失),所以Redis提供了持久化功能,Redis 默认是使用RDB方式持久化的,在大部分情况下,RDB完全够用了
1 RDB(Redis DataBase)
在主从复制中,RDB就是备用的,一般放在从机上面而不是主机,相对来说比较方便,AOF基本不使用
RDB的大概执行流程
在指定的时间间隔内将内存中的数据集(集体)快照写入到磁盘内,恢复的时候是将快照文件直接读到内存中
Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个近视文件中,等持久化过程都结束了,再用这个临时文件替换上次持久化好的文件,整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能,如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那么RDB的方式会比AOF更加的高效,RDB的缺点是最后一次持久化后的数据可能丢失(最后一次持久化有可能宕机),我们默认的就是使用RDB,不需要修改这个配置
RDB保存的文件是 dump.rdb 都是在我们conf配置文件中快照区域(SNAPSHOTTING)进行配置的
RDB在conf配置文件中的相关配置截图,快照部分(SNAPSHOTTING)
dbfilename dump.rdb #redisRDB持久化默认的保存的文件名叫dump.rdb
#redis中RDB持久化默认策略
# save 900 1 #在900s内修改1次key 这些都是默认的规则(策略),大多数情况下不需要我们修改
# save 300 10 #在300s内修改10次key
# save 60 10000 #在60s内修改10000次key
save 60 5 #在60s内修改了5次key
自定义测试RDB持久化,只要我们在60s内修改了5次key,就会触发RDB操作,一旦触发RDB操作就会生成dump.rdb文件
save 60 5 #在60s内修改了5次key
首先我们把dump.rdb文件删除
然后在我们规定的60s内修改5此次key再次查看会发现dump.rdb文件自动生成
#修改下面5个键都是在我们自定义规则一分钟内操作完成的 所以会自动生成dump.rdb文件
127.0.0.1:6379> set k1 v1 #随便set几个key测试
OK
127.0.0.1:6379> set k2 v2 #随便set几个key测试
OK
127.0.0.1:6379> set k3 v3 #随便set几个key测试
OK
127.0.0.1:6379> set k4 v4 #随便set几个key测试
OK
127.0.0.1:6379> set k5 v5 #随便set几个key测试
OK
127.0.0.1:6379> shutdown #宕机redis
not connected> ping #重新连接
PONG
127.0.0.1:6379> get k1 #key发现我们redis就算宕机了也会把数据进行持久化
"v1"
发现目录dump.rdb文件自动生成 触发我们的规则
RDB持久化触发机制(生成dump.rdb文件)
1 save的规则满足的情况下(符合我们在conf配置文件中自定义的规定时间内进行多少key的修改),会触发RDB规则
2 执行flushall命令,也会触发我们的RDB规则
3 退出我们的redis,redis宕机,使用shutdown命令,也会触发我们的RDB规则
备份就会自动生成一个dump.rdb文件
如何恢复RDB文件(dump.rdb)
1 只需要将RDB文件放在我们redis启动目录下就可以了,redi启动的时候就会自动检查dump.rdb文件恢复其中的数据(全自动的)
2 查看需要存放的位置
127.0.0.1:6379> config get dir #查看查看需要存放的位置
1) "dir"
2) "D:\\Tools\\Java\\Redis" #需要放在的目录,和它在同一级即可,如果存在这个文件(dump.rdb)那么启动时就会自动恢复其中的数据
RDB这种全自动配置,几乎就按照它自动默认的配置就够用了,但是我们还是要学习
RDB的优点
1 适合大规模的数据恢复(比如线上服务器宕机了,千万不要删除dump.rdb)(原理:创建的时候,回fork一个子进程,父进程去正常出来客户端请求,而子进程进行fork操作去进行持久化数据,效率非常高)
2 对数据的完整性不高
3 恢复数据效率会非常快
RDB的缺点
1 需要一定的时间间隔进行操作(我们可以自己的conf配置文件进行设置),如果redis意外宕机,那么这个最后一次修改的数据就没了
2 fork一条子进程去进行持久化操作的时候,会占用一定的内存空间,需要我们去考虑内存空间的问题
2 AOF(Append only file)默认就是文件的无限追加
将我们的所有命令都记录下来,好比一个history文件,恢复的时候就把这个文件全部再执行一遍
以日志的形式来记录每一个写操作,将Redis执行过的所有指令记录下来(读操作不记录),只许追加文件但是不可以改写(修改文件),Redis启动的时候的时候会读取该文件重新构建数据,换一句话来说,redis重启的话就根据日志文件的内容将写的指令从前到后执行一次以完成数据的恢复, 如果是大数据情况下,效率会比较低,因为大量读写操作需要进行很久
AOF保存的数appendonly.aof文件,以下截图来自conf配置文件中APPEND ONLY MODE位置
AOF在conf中的相关配置
appendonly no #默认是不开启的,Redis默认开启的事RDB持久化方式,我们需要手动更换成yes表示开启AOF持久化方式
appendonly yes
# The name of the append only file (default: "appendonly.aof")
appendfilename "appendonly.aof" #默认的保存文件名叫appendonly.aof,一般情况下不要改
#redis中AOF的默认策略
# appendfsync always #每一次都去修改
appendfsync everysec #每一秒写一次 默认这个即可
# appendfsync no #不修改
no-appendfsync-on-rewrite no #重写的规则 是否进行一些重写 重写的时候是否用append重写,默认no 不使用 可以保证我们的数据安全性
#AOF重写的规则 保持默认即可
auto-aof-rewrite-percentage #重写的进度条是100
auto-aof-rewrite-min-size 64mb #重写的最大值是64兆B 超过这个会重写一些其他文件
例子:自定义测试AOF持久化
首先把conf文件中的appendonly 从no改为yes,默认是no不开启,有的版本或者操作系统在配置文件把appendonly no改为appendonly yes,但是不生效(我现在用的是windows),不会产生appendonly.aof文件,需要在cli客户端输入命令(注:想要生成appendonly.aof文件,必须要执行redis命令才会生成对应的命令记录,aof文件其实就是记录你执行过的redis命令)
redis-cli config set appendonly yes #把aof持久化开启 默认关闭的 这样就同时支持我们的rdb和aof两种,一般这个命令我们用的比较多
redis-cli config set save "" #关闭 RDB 功能。 这一步是可选的, 如果你愿意的话, 也可以同时使用 RDB 和 AOF 这两种持久化功能,执行了之后,服务器上面就生成了对应的appendonly.aof,一般这个文件主要是用来恢复redis数据库的
配置完成之后,重启我们的redis(shutdown)即可生效,(因为redis每秒都在进行生成),生成我们的appendonly.aof文件, 一般我们的aof和rdb都会进行生成
appendonly.aof其实就是个文件,以日志的形式记录我们的所有写操作,每秒都会记录我们的写操作,启动的时候去调用这个文件恢复数据,一般这个文件主要是用来恢复redis数据库的
如如对我们的aof文件有误或者被进行篡改,我们的redis是启动不起来的,我们需要修复这个配置文件,redis给我们提供了一个工具:redis-check-aof
故意篡改文件
使用redis-check-aof命令进行修复
redis-check-aof --fix appendonly.aof
3 这样就是成功修复了
4 修复成功后,再次查看我们的aof文件,发现已经被修复这时候再重启redis会正常启动
AOF默认就是文件的无限追加.如果文件过于大,就会触发我们的重写策略
AOF重写的参数是rewirte,默认不打开(no), rewirte重写的配置,重写规则说明如果AOF文件大于64m.就会fork一个新的进程来将我们的文件进行重写
#redis中AOF的默认策略
# appendfsync always #每一次都去修改
appendfsync everysec #每一秒写一次 默认这个即可
# appendfsync no #不修改
AOF的优点
1 每一次修改都同步,文件和数据的管理会更加安全
2 根据redis的配置,因为redis是默认开启每秒同步一次,这样的话可能会丢失最后一秒的数据,如果一直同步会非常消耗性能,从不同步,效率是最高的
AOF的缺点
1 相对于数据文件来说,AOF远远大于RDB,修复的数据也比RDB慢
2 AOF是一个读写的IO操作.运行效率也要比RDB慢,所以我们Redis默认的配置就是RDB持久化
扩展:
1 如果只做缓存,希望数据在服务器运行的时候存在,不需要做任何持久化操作
2 如果同时开启两种持久化方式,首先启动redis的话会优先载入AOF文件来恢复原始的数据,因为通常情况下AOF文件保存的数据集要比RDB完整, 两者同时开启消耗的性能也会很大
3 RDB的数据不实时.同时使用两者服务器也只会找AOF文件,那要不要只使用AOF呢,还是建议不要,因为RDB更适合用于备份数据库(AOF不断变化不好备份),而且AOF有潜在的BUG
4 性能建议:RDB文件一般只用作于备份数据,建议只在slave(从机)上持久化RDB文件,而且只要15分钟备份一次就够了,只保留save 900 1这一条规则
5 如果开启AOF持久化方式,最差的情况也只是丢失不超过两秒的数据,如果把AOF关掉的话,仅靠Mater-Slave Repllcation(主从复制)实现高可用也可以,key剩下一大笔IO消耗,也减少了rewrite(重写)时带来的系统波动,代价是Master/Slave同时宕机(最暴力的方式就是断电),会丢失十几分钟的数据,启动脚本的时候会比较Master/Slave(主机从机)中的RDB文件,哪个更新,哪个文件更大,就是我们最新的文件(数据),微博就是采用这种架构
16 Redis的发布订阅
前沿:一种通信机制–>一般由队列来实现,会存在两个东西,发送者,订阅者,发送者通过消息管道发送内容给订阅者,订阅者通过消息管道获取发送者的信息
Redis发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息,我们的微信公众号,微博.以及关注up主,这种关注系统 都是类似的机制和场景
Redis客户端可以订阅任意数量的频道(比如我们bilibili一个用户可以关注很多up主)
必须有以下三个条件才能实现基本的消息订阅
1:消息发送(发布)者
2:频道(中间件),发送者通过它传给订阅者,订阅者通过它得到发送者发送到内容
3:消息(订阅)接受者
订阅/发布消息流程图
简单的模型
下图展示了频道channel1,以及订阅这个频道的三个客户端client1,client2,client4他们两者直接的关系
当有新的消息publish命令发送给频道channel1时,这个消息就会被发送给订阅它的三个客户端
Redis中发布订阅的相关命令
这些命令被广泛的用于构建即时通信应用,比如网络聊天室(chatroom)和实时广播,实时提醒等场景
做一个简单的例子测试,订阅者订阅发送者,发送者发送内容给订阅者
1 订阅端:订阅者订阅频道方便接收发送者以后给频道发送的内容
127.0.0.1:6379> subscribe weiyihe #订阅一个叫weiyihe的频道
Reading messages... (press Ctrl-C to quit) #订阅成功 按ctrl+c退订频道(退出)
#订阅成功,等待读取推送的信息
1) "subscribe"
2) "weiyihe" #订阅了一个叫weiyihe的频道
3) (integer) 1 #当前订阅了一个频道
1) "message" #消息
2) "weiyihe" #来自那个频道名的消息
3) "hello,weiyihe" #消息的具体内容,可能是一片文章,也可能是一个对象.频道接送出来的消息
1) "message" #消息
2) "weiyihe" #频道名
3) "hello,redis" #频道接送出来的消息
127.0.0.1:6379> subscribe CCTV #订阅CCTV
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "CCTV" #频道名
3) (integer) 1
1) "message"
2) "CCTV"
3) "studyTimeNow" #频道发送到内容
#当然 也是可以同时订阅多个频道的
127.0.0.1:6379> subscribe pindao1 pindao2 #订阅两个频道
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "pindao1"
3) (integer) 1
1) "subscribe"
2) "pindao2"
3) (integer) 2 #当前订阅了两个频道
1) "message"
2) "pindao1"
3) "one"
1) "message"
2) "pindao2"
3) "two"
1) "message"
2) "pindao1"
3) "oneone"
1) "message"
2) "pindao2"
3) "twotwo"
2 发布端:发布(送)者给频道频道发布内容,通过频道传给订阅者
#发布者发送消息到频道
127.0.0.1:6379> publish weiyihe hello,weiyihe #给频道名叫weiyihe的频道发送内容
(integer) 1 #发送成功
127.0.0.1:6379> publish weiyihe hello,redis #给频道名叫weiyihe的频道发送内容
(integer) 1 #发送成功
127.0.0.1:6379> publish weiyihe1 hello,redis1 ##给频道名叫weiyihe1的频道发送内容,注意:这个频道不存在
(integer) 0 #发送失败,因为频道不存在
127.0.0.1:6379> publish CCTV studyTimeNow #给CCCTV这个频道发送内容
(integer) 1
#两个频道一起发送信息
127.0.0.1:6379> publish pindao1 one
(integer) 1
127.0.0.1:6379> publish pindao2 two
(integer) 1
127.0.0.1:6379> publish pindao1 oneone
(integer) 1
127.0.0.1:6379> publish pindao2 twotwo
(integer) 1
原理
使用场景
1.实时消息系统
2.实时聊天,聊天室(频道当做聊天室,将信息回显给所有人即可)
3.订阅,关注系统
稍微复杂的场景我们就会使用我们的消息中间件(MQ)来做
17 Redis主从复制(master/slave)
1 概念
一个主人有好几个仆从
2 一台Redis服务器(主master)的数据复制到其他多个Redis服务器(从slave),主机主要进行写的操作,从记进行读的操作
3 主从复制主要为了解决读写分离,80%的情况下都是在进行读操作,我们把所有读操作的压力分配到从机上,写的操作请求交给主机进行处理,这样的模式可以减缓服务器压力,在架构中经常使用,最低的配置一般是三个,即为一主二从
我们在开发中最低的配置一般是三个,即为一主二从
4 基本的一主二从模型
5 主从复制的作用
6 为什么使用主从复制?
如果只有一台redis服务器的话,如果发生宕机会导致数据丢失,而且读写不分离会给redis造成过大的压力,在公司中,主从复制是必须使用的
7 配置主从复制环境(注:目前没有装Linux系统,在windows上进行操作,有的是截图)
1 默认情况下,每台Redis服务器都是主节点 ,Redis配置主从复制记住一句话:只配置从库,不用配置主库,Redis默认自己本身就是一个主库
2 查看当前库的信息的命令
127.0.0.1:6379> info replication #查看当前库的信息
# Replication
role:master #角色 从机 当前是个主机
connected_slaves:0 #当前连接的从机个数 当前有0个从机,没有从机
master_replid:2f88f386e76af7e9b23fcd6909e49529e48492e6 #随机生成的ID
master_replid2:0000000000000000000000000000000000000000 #随机生成的ID
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
3 linux下配置需要修改复制三个配置文件,修改对应的信息
1.端口号
2.pid 名字
3.log文件 日志名字
4.dump.rdb备份文件名字
4 修改完毕之后,启动我们的3个redis服务器,可以通过进程命令查看信息 ps-ef|grep redis
5 默认情况下,每台Redis服务器都是主节点,我们一般情况下只需要去配置从机就好了
认老大 一主二从
1 从机进行配置
真实的主从复制配置应该是在文件中进行配置的,这样的话就是永久的.我们这里使用的是命令,是暂时的
1 使用配置文件配置
需要把注释解开,把主机的ip+端口进行填入,如果主机有密码的话,还需要写入主机的密码
使用命令配置(slaveof)
给从机进行配置并且查看从机信息
127.0.0.1:6379> slaveof 127.0.0.1 6379 #从机进行配置主机 ip 端口 找谁当自己老大
OK
127.0.0.1:6379> info replication #查看本机信息
# Replication
role:slave #当前角色是从机
#可以看到主机信息
master_host:127.0.0.1 #主机ip
master_port:6379 #主机端口
master_link_status:down
master_last_io_seconds_ago:-1
master_sync_in_progress:0
slave_repl_offset:1
master_link_down_since_seconds:1638625163
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:9a6a05f442945636e5a8bd3623a7d398efa43f1f
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
查看主机配置
如果继续在从机上进行slaveof配置主机信息,那么从机的数量也会继续进行变化
主机从机配置以及数据共享的细节
主机可以写(设置值),从机只能读,不能写,主机中的所有信息和数据,都会自动被从机保存, 就算主机蹦了,但是数据不会丢失,会保存在从机中
主机写数据
从机也可以读取到相同的数据,但是从机不能写
如果主机断开连接宕机,从机依旧是可以连接到主机的读取数据的,但是没有写操作了(因为主机断了),这个时候如果主机回来了(正常运行),从机依旧可以直接获取到主机的信息(保证了我们的高可用性,当然我们目前这个策略还不行,高级点的策略应该是,就算我主机断了,从机们也会立马选举出来一个从机作为主机)
如果是命令行配置我们的主从复制,我们的从机断了(宕机)后再次连接,不进行配置的话就会变成主机(默认是一个主机),不会读取到我们配置的那个主机的数据,如果我们收到使用slaveof命令配置主机的话,还是可以正常读取到我们主机的数据
8 复制原理(全量复制,增量复制)
9 另外一种主从配置方式
主机-》从机/主机-》从机
层层链路,上一个master连接下一个slave,依次这样连接,这种的方式也是可以完成我们的主从复制的
10 如果没有老大了(主机),那么能不能选择在我们的从机中选择一个老大出来呢?
在哨兵模式没有出来前,我们一般通过手动来完成的 (slaveof no one),如果主机断开连接了,那么我们可以使用slaveof no one使自己作为主机,其他的节点就可以手动连接到最新的那个主机(主节点),如果这个时候老大(主机)恢复了,那就只能重新配置连接, 哨兵模式出来以后,我们就不用手动来配置,可以自动完成这一系列的操作
127.0.0.1:6379> slaveof no one #让自己做主机
OK
127.0.0.1:6379> info replication #查看本机信息
# Replication
role:master #主机
connected_slaves:0 #没有从机 从机个数为0
master_replid:2bd621cb6661097489977e9547aae78ad351d7e4
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
127.0.0.1:6379>
18 哨兵模式(自动选取主机的模式)
1 概述
哨兵模式主要通过redis-sentinel进行配置
比如我们有一个主机6379,还有两个从机6380,6381,不仅要配置基本的一主多从,还要单独的配置一个哨兵进程,需要不定时发请求判断我们三个服务器是否存活,哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个redis实例(服务),如果没有回复响应,那么就是断开连接宕机
这里的哨兵有两个作用
1 通过发送命令,让Redis服务器返回监控它的运行状态,包括主机和从机(主服务器和从服务器)
2 当哨兵检测到master宕机,会自动将slave变为master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机
然而一个哨兵进程对Redis服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控,各个哨兵直接还会进行检测,这样就形成了多哨兵模式(集群),起步就是6个进程 3哨兵 3服务器 一主二从
2上述例子是一个单机哨兵,如果这个哨兵宕机就无法保证我们的服务正常运行,正常情况下我们也会给哨兵配置集群,就是以下流程图
3 哨兵模式的工作流程
假设服务器宕机,哨兵1检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认为主服务器不是决定性的(一个哨兵的判断不会立马决定结果),这个现象称为主观下线,当后面的哨兵也检测到主服务器不可用,并且数量达到一定值的时候,那么哨兵就会判断这个主服务器宕机, 这时哨兵直接会进行选票(投票的发起者是随机的),投票的结果由一个哨兵发起,进行failover(故障移除操作),切换成功后,就会通过发布订阅(比如给从机1投了一票,给从机2投了两票,那么从机2就会胜出称为新的主机),让各个哨兵把自己监控的从服务器实现切换主服务器(投票胜出的那个),这个过程称为客观下线
哨兵模式的投票是一个算法
4 配置哨兵模式
首先声明:现在我们主从复制的模式是一主(6379)二从(6380,6381),而不是层层链入
1 配饰哨兵配置文件(sentinel.conf),因为哨兵启动的时候会检测这个文件
#注意单词不要写错
#配置的哨兵不止于此,但是这个 最核心基本的配置
#哨兵 监控 被监控的名称 主机的ip和端口
#后面这个数字1,代表主机挂了,哨兵投票看让谁接替成为新的主机,票数最多的,就会成为主机
sentinel moitor myredis 127.0.0.1 6379
2 配置完成之后启动哨兵(redis-sentinel)
3 这样我们的哨兵模式就启动成功了,上图所示,哨兵模式正在检测127.0.0.1 6379这个主机,还检测到6380和6381两台从机,主机里面有1票,是它们的老大
5 测试主机宕机会怎么样
1 首先给主机set一个值 set k1 v1然后宕机
2 需要等待哨兵模式一会,因为它要发送信息监控服务器等待响应,等哨兵模式得到监控信息,此时查看哨兵模式,发现79宕机,进行数据迁移然后选举新的主机
3 此时查看6380这个从机,发现它还是从机
4 查看我们的6381,发现它自动成为了主机
哨兵模式也有说明,6379已经shutdown,经过哨兵们的选举6381成为我们新的主机
结论:如果master节点断开了,就会从从机中随机选出一个从机成为新的主机(这里面有一个投票算法)
哨兵日志的几个重要部分,故障转移(数据迁移)failover,主机宕机down,选举推送新的主机
6 那么,现在把6379这个主机连回来有用吗?
1 答案是没用的,虽然它还是一个主机,但是其他是光杆司令,没有任何从机,需要我们手动去配置
2 查看哨兵日志,convert(转换)6379转换成为了6381的从机
3 再次查看6381的信息,发送它有两个从机6379,6380,所以就算6379这个曾经的主机回来,也只能给新的主机做从机
4 查看曾经的主机6379,也只能给新的主机6381做从机
如果主机此时正常连接回来了,只能归并到新的从机下,当作新的主机的从机,这就是哨兵模式的规则
7 哨兵模式的优点和缺点
优点
1 哨兵模式,基于主从复制,所有的主从配置的优点,它全都有,而且它还会自动配置
2 主从可以切换,数据可以转移,系统的可用性就会更好(高可用)
3 哨兵模式就是主从模式的升级版,从手动升级到自动,更加完善健壮
缺点
1 Redis不好在线扩容的,如果集群容量一旦到达上限(集群的容量满了), 在线扩容就非常麻烦(可以做横向扩展)
2 实现哨兵模式的配置其实是很麻烦的,里面有很多选择,我们配置的只是一个最基础的
8 哨兵模式的全部配置
1 哨兵模式的端口
2 定义哨兵的工作目录
3 默认主机的节点
4 配置主机密码,如果没有可以不进行配置
5 默认的延迟操作,也就是我们等待哨兵监控服务器等待的时间,默认30s,可以不用配置
6 故障转移的操作,并行同步的时间
7 设置一些其他操作,比如说默认的三分钟的故障转移,如果觉得时间长,可以进行更改
8 执行脚本 出现故障可以发送到我们的邮件或者服务器上面,来通知管理员(需要编写sheet脚本)
9 主节点改变的时候,脚本被调用
19 Redis缓存穿透,击穿,雪崩
缓存穿透,击穿,雪崩都归属于服务器的高可用的问题
缓存流程
1 缓存穿透(查不到导致的)
如果查询一个id为-1的值,此时缓存中没有,数据库也没有,就会频繁的去数据库中查询,给数据库造成过大的压力
缓存穿透的概念很简单,用户想要查询一个数据,发现redis内存数据库中没有,也就是缓存没有命中,于是向数据库查询,发现数据库也没有,于是本次查询失败,当用户很多的时候,缓存都没有命中(秒杀场景),于是都去数据库查询,这时候给数据库造成过大的压力
场景:秒杀
缓存穿透解决方案:
1 布隆过滤器(Bloom Filter)
布隆过滤器是一种数据结构,对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合就进行丢弃(比如id为-1),从而避免了对数据库的查询压力
使用布隆过滤器也很简单,导入布隆过滤器的包
2 缓存空对象
当缓存没有被命中,即使返回的空对象也将其存储起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了数据库
但是这种方法会有两个问题:
1 如果空值能够被存储起来,这就意味着需要更多的空间去存储更多的键,因为这当中可能会有很多的空值的键
2 即使对空值设置了过期时间,还是会存在缓存层的数据会有一段时间窗口的不一致,这对于业务会有一定的影响(缓存中有了,存储中没有)
2 缓存击穿(查得到但是量太大,缓存过期)
缓存击穿可以想象:一堵墙,把全部的火力点集中到某个位置,造成单方面突破
缓存击穿是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key失效的瞬间,持续的大并发就击穿了缓存,直接请求数据库瞬间压力过大
当某个key过期的瞬间,有大量的请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访问数据库来查询最新数据,并且会写缓存,导致数据库瞬间压力过大
场景:热搜微博:微博服务器宕机,有一个热搜新闻我们会共同访问它,把所有的流量到攻击到这个点上,比如设置60s过期,但是我们60.1秒把它恢复,巨大的请求会直接访问我们的数据库
缓存击穿解决方案
1 设置热点数据永不过期,
从缓存的层面上看,没有设置过期的时间,所以不会出现热点key过期后产生的问题(不过这种方案也是万能的,redis有内存上限的问题,需要考虑删除过期的key)
2 加互斥锁(分布式锁)setnx,使用分布式锁,
保证对于每一个key同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只需要等待即可,这种方式将高并发的压力转移到了分布式锁(使用分布式锁也非常麻烦,对分布式锁的考验很大)
3 缓存雪崩
缓存雪崩是指:在某一个时间段,缓存集中过期失效(redis集群宕机 停电)
场景:,比如在写文本的时候,马上就要到双12的零点,很快就会迎来一波抢购,把热点数据全部放在缓存中,设置时候为1小时,但是过了凌晨一点,这些缓存就过期了,而对这批商品的访问查询,都落在了数据库上面,对于数据库而言,就会产生周期性的压力,于是所有的请求都会到达存储层,存储层的调用者会暴增,造成存储层也会挂掉的情况
其实集中过期,倒不是非常致命,比较致命的缓存雪崩,是服务器的某个节点和断网,因为自然形成的缓存雪崩,一定是个某个时间段集中创建缓存,这个时候,数据库也是可以顶住压力大,无非就是对数据库产生周期性的压力而已,而缓存服务节点的宕机,对数据库造成的压力是不可预知的,很有可能把数据库瞬间就把数据库压垮
比如:双十一会停掉一些服务(保证主要的服务可用),比如双11当天退款是很困难的,就是因为相关服务被停了,为我们的主要服务去让路,保证服务器的高可用
缓存雪崩解决方案
1 redis高可用:,这个思想的含义是,既然redis有可能挂掉,那我就多准备几台redis,这样一台挂掉之后就可以继续工作,其实就是搭建的集群(异地多活)
2 限流降级: 这个解决方案的思路是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量,比如对某个key只允许一个线程查询数据和写缓存,其他线程进行等待(降级就是把一些不是主要的服务停下来给我们的主要的服务让路,双十一不能退款就是例子)
3 数据预热:数据预热的含义就是在正式部署之前,我先把可能的数据先预热先访问一遍,这样部分可能大量的访问的数据就会加载到缓存中,在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间尽量均匀一些,避免同时失效这种情况
Nosql
为什么要用Nosql?
用户的个人信息,社交网络,地理位置,用户自己的数据,用户日志等等爆发增长
这时候我们关系型数据库满足不了,就需要使用Nosql数据库,Nosql数据库可以很好的处理这些情况!
什么是Nosql
Nosql == not only sql 不仅仅是sql
泛指非关系型数据库,随之web2.0互联网的诞生!传统的关系型数据库很难对付web2.0时代,尤其是超大规模的高并发的社区,暴露出难以克服的问题,Nosql在当今大数据环境下发展的十分迅速,Redis是发展最快的,而且是我们当下必须要掌握的一个技术!
很多的数据类型用户的个人信息,社交网络,地理位置,这些数据类型的存储不需要一个固定的格式(行和列)!,不需要多余的操作就可以横向或者纵向扩展的(多台机器来实现,比如说集群)
Map<String,Object>使用键值对来控制任意数据类型(典型代表是redis)
Nosql的特点
解耦(没有关系)
1 方便扩展(数据之间无关系,没有关系,很好扩展(随意扩展))
2 大数据量高性能(Redis一秒写8万次,读取11万,Nosql的缓存记录级,是一种细粒度的缓存,性能会比较高!)
3 数据类型是多样型的(不需要事先设计数据库,随取随用,get set,如果是数据量十分大的表,很多人无法设计了)
传统的RDBMS(关系型数据库)和Nosql的区别
传统的RDBMS(关系型数据库)
1 结构化组织(表和列)
2 结构化查询sql
3 数据和关系都存储在单独的表中 row col
4 数据操作语言(增删改查),数据的定义语言
5 严格的一致性(事务)
6 基础的事务操作
Nosql
1 不仅仅是数据
2 没有固定的查询语言
3 很多的存储方式(键值对,列存储,文档存储…),图形数据库(社交关系)
4 最终一致性(最终的结果一致就行了)
5 CAP定理和BASE理论(异地多活)
6 高可扩展性
扩展
大数据时代的3v:主要是来描述问题的
1 海量(Volume)
2 多样(Variety)
3 实时(Velocity)
大数据时代的3高:主要是对程序的要求
1 高并发(用户量大)
2 高可扩(随时水平拆分,机器不够了,可以扩展机器来解决)
3 高性能(保证用户体验和性能)
真正的实践:RDBMS(关系型数据库)+Nosql才是最强的
相关文章
- redis全面解析
- Redis学习(8)-redis持久化
- Redis学习(2)-redis安装
- 3类数据库的联动:mysql、mongodb、redis
- Redis学习笔记
- Redis命令:EXPIREAT key timestamp(设置key在某一时间过期)
- Redis缓存穿透、缓存雪崩、redis并发问题 并发竞争key的解决方案 (阿里)
- [Bug]redis问题解决(MISCONF Redis is configured to save RDB snapshots)
- 【SpringBoot笔记24】SpringBoot框架结合Redis实现分布式锁
- 〖Python 数据库开发实战 - Python与Redis交互篇④〗- 利用 redis-py 实现集合与有序集合的常用指令操作
- 〖Python 数据库开发实战 - Python与Redis交互篇⑥〗- redis-py 的事务函数
- redis基础操作概念等笔记
- Redis 学习笔记
- 深入浅出Redis-redis哨兵集群
- Redis使用示例及在PHP环境中用redis存储session
- SpringBoot学习笔记(6) SpringBoot数据缓存Cache [Guava和Redis实现]
- Redis、Memcache和MongoDB的区别
- 【Redis笔记03】Redis运行环境之Cluster集群模式
- 【Redis】redis大key和大value的危害,如何处理?
- redis和memcached的区别是什么?
- Docker学习笔记17:docker实例之安装 Node.js、PHP、MySQL、Tomcat、Python、Redis、MongoDB、Apache
- Redis学习笔记
- 【redis源码分析】Redis Sentinel 是如何实际解决分布式共识问题的