视频地址
我们现在已经在2022年,已经到达了大数据的时代!
在90年代的时候一个网站的访问量一般不会太大,当个数据完全足够!
那个时候,更多的去使用静态网页静态html,服务器不会有太大的压力
思考一下这种情况下整个网站的瓶颈时什么?
1、数据量如果太大一个机器放不下
2、数据的索引(B+Tree),一个机器内存也放不下
3、访问量(读写混合),一个服务器承受不了
只要你开始出现以上的三种情况之一,那么你就必须要晋级!
网站80%的情况都是在读,每次都要去查询数据库的话就十分麻烦!所以说我们希望减轻数据的压力,我们可以使用缓存来保证效率!
发展过程中:优化数据结构和索引–>文件缓存(IO)–>Memcached(当时最流行的技术)
技术和业务在发展的同时,对人的要求也越来越高
本质:数据库(读,写)
早些年MyISAM:表锁,十分影响效率!高并发下就会出现严重的锁问题
转战innodb:行锁
慢慢的就开始使用分库分表来解决写的压力!Mysql在那个年代推出来表分区!这个并没有多少公司使用!
2010–2020十年的时间互联网发生了翻天覆地的变化;(定位,也是一种数据,音乐,热榜)
Mysql等关系型数据库就不够用; !数据量很多,变化很快!
Mysql有的使用它来储存一些比较大的文件,博客,图片,数据就低了!如果有一种数据库来专门处理这种数据,
Mysql压力就变的十分小(研究如何处理这些问题)大数据的IO压力下,表几乎没法更大一亿,表几乎没法更大!
用户的个人信息!社交网络,地理位置。用户 自己产生的数据,用户日志等等爆发式增长!!
这时候我们就需要使用Nosql数据库的,Nosql可以很好的处理以上情况!
NoSQL=Not Only SQL(不仅仅是SQL)
关系型数据库:表格,行,列
泛指非关系型数据库的,随着web2.0互联网的诞生!传统的关系型数据库很难对付web2.0时代!尤其是超大规模的高并发 的社区!暴露出来很多难以克服的问题,NoSQL在当今大数据环境下发展的十分迅速,Redis是发展很快的,而且是我们当下必须要掌握一个技术!
很多的数据类型用户的个人信息,社交网络,地理位置。这些数据类型的储存不需要一个固定的格式!不需要多余的操作就可以横向扩展的!
1、 方便扩展(数据之间没有关系,很好扩展!)
2、大数据高性能(Redis一秒写8万次,读取11万,NoSQL的缓存记录级,是一种细粒度的缓存,性能会比较高!)
3、数据类型是多样性的!(不需要事先先设计数据库!随取随用!如果是数据了十分大的表,很多人就无法设计列!)
4、传统RDBMS和NoSQL
传统的RDBMS
NoSQL
大数据时代的3V,主要描述问题的
大数据时代的三高:主要是对程序的要求
真正的公司的实践:NoSQL+RDBMS一起使用才是最强的
KV键值对:
Redis(Remote Dictionary Server)远程字典服务
是一个开源使用ANSI C语言编写,支持网络,可基于内存持久化的日志型、key-value储存系统,并提供多种语言的API
redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了主从同步。
Redis能干嘛
1、内存储存、持久化、内存中断电即失,所以说持久化很重要(rdb,aof)
2、效率高,可以用于高速缓存
3、发布订阅系统
4、地区信息分析
5、计时器、计数器
特性
1、多样的数据类型
2、持久化
3、集群
4、事务
学习中需要用到的东西
Redis
如果是Windows需要在github下载,当是不建议使用windows上安装使用
以下安装教程转载于菜鸟教程
**下载地址:**https://github.com/tporadowski/redis/releases。
Redis 支持 32 位和 64 位。这个需要根据你系统平台的实际情况选择,这里我们下载 Redis-x64-xxx.zip压缩包到 C 盘,解压后,将文件夹重新命名为 redis。
打开文件夹,内容如下:
打开一个 cmd 窗口 使用 cd 命令切换目录到 C:\redis 运行:
redis-server.exe redis.windows.conf
如果想方便的话,可以把 redis 的路径加到系统的环境变量里,这样就省得再输路径了,后面的那个 redis.windows.conf 可以省略,如果省略,会启用默认的。输入之后,会显示如下界面:
这时候另启一个 cmd 窗口,原来的不要关闭,不然就无法访问服务端了。
切换到 redis 目录下运行:
redis-cli.exe -h 127.0.0.1 -p 6379
设置键值对:
set myKey abc
取出键值对:
get myKey
**下载地址:**http://redis.io/download,下载最新稳定版本。
本教程使用的最新文档版本为 2.8.17,下载并安装:
# wget http://download.redis.io/releases/redis-6.0.8.tar.gz # tar xzf redis-6.0.8.tar.gz # cd redis-6.0.8 # make
执行完 make 命令后,redis-6.0.8 的 src 目录下会出现编译后的 redis 服务程序 redis-server,还有用于测试的客户端程序 redis-cli:
下面启动 redis 服务:
# cd src # ./redis-server
注意这种方式启动 redis 使用的是默认配置。也可以通过启动参数告诉 redis 使用指定配置文件使用下面命令启动。
# cd src # ./redis-server ../redis.conf
redis.conf 是一个默认的配置文件。我们可以根据需要使用自己的配置文件。
启动 redis 服务进程后,就可以使用测试客户端程序 redis-cli 和 redis 服务交互了。 比如:
# cd src # ./redis-cli redis> set foo bar OK redis> get foo "bar"
序号 | 选项 | 描述 | 默认值 |
1 | -h | 指定服务器主机名 | 127.0.0.1 |
2 | -p | 指定服务器端口 | 6379 |
3 | -s | 指定服务器 socket | |
4 | -c | 指定并发连接数 | 50 |
5 | -n | 指定请求数 | 10000 |
6 | -d | 以字节的形式指定 SET/GET 值的数据大小 | 2 |
7 | -k | 1=keep alive 0=reconnect | 1 |
8 | -r | SET/GET/INCR 使用随机 key, SADD 使用随机值 | |
9 | -P | 通过管道传输 请求 | 1 |
10 | -q | 强制退出 redis。仅显示 query/sec 值 | |
11 | –csv | 以 CSV 格式输出 | |
12 | *-l*(L 的小写字母) | 生成循环,永久执行测试 | |
13 | -t | 仅运行以逗号分隔的测试命令列表。 | |
14 | *-I*(i 的大写字母) | Idle 模式。仅打开 N 个 idle 连接并等待。 |
来简单测试下
测试100个并发请求 请求100000请求
redis-benchmark -h localhost -p 6379 -c 100 -n 100000
127.0.0.1:6379> select 3 OK 127.0.0.1:6379[3]> dbsize (integer) 0 127.0.0.1:6379[3]>
127.0.0.1:6379[3]> keys * 1) "name"
127.0.0.1:6379[3]> flushdb OK 127.0.0.1:6379[3]> keys * (empty list or set)
127.0.0.1:6379[3]> flushall OK
Redis为什么单线程
明白Redis是最快的,官方表示,Redis是基于内存操作,CPU不是Redis性能瓶颈,Redis的瓶颈是根据机器的内存和网络带宽,既然可以使用单线程来实现,就使用单线程了!所以就使用单线程了!
Redis是C语言写的,官方提供的数据为100000+QPS,完全不必用样使用key-value的Memecache差!
Redis为什么使用单线程还这么快?
误区1:高性能的服务器一定是多线程的?
误区2:多线程(CPU上下文会切换浪费时间)一定比单线程效率高!先去CPU>内存>硬盘的速度有锁了解!
核心:redis是所有的数据全部放在内存中的,所以说使用单线程去操作效率是最高的,多线程(CPU上下文切换:耗时的操作),对于内存系统来说,如果没有上下文切换效率就是最高的!多次读写都在一个CPU上的,在内存情况下,这个就是最佳方案!
127.0.0.1:6379> set name qileyun #添加值 OK 127.0.0.1:6379> type name # 获取类型 string 127.0.0.1:6379> exists name # 判断是否有这个key (integer) 1 127.0.0.1:6379> move name 1 #移除当前的key (integer) 0 127.0.0.1:6379> expire name 10 #设置过期时间 单位是秒 (integer) 1 127.0.0.1:6379> ttl name (integer) 5 127.0.0.1:6379> ttl name # 获取过期时间 (integer) 2 127.0.0.1:6379> get name #获取值 (nil)
追加数据
127.0.0.1:6379> set key1 v1 # 设置值 OK 127.0.0.1:6379> get key1 #获取值 "v1" 127.0.0.1:6379> clear # 清空 127.0.0.1:6379> append key1 "hello" # 追加字符串 (integer) 7 127.0.0.1:6379> get key1 "v1hello" 127.0.0.1:6379> strlen key1 # 查看字符串的长度 (integer) 7 127.0.0.1:6379> append key1 ",wrold" (integer) 13
可以自增加自减少功能比如应用在点赞功能上
127.0.0.1:6379> set views 0 OK 127.0.0.1:6379> get views "0" 127.0.0.1:6379> incr views # 自增一 (integer) 1 127.0.0.1:6379> incr views (integer) 2 127.0.0.1:6379> decr views # 自减一 (integer) 1 127.0.0.1:6379> incrby views 10 # 自增10 (integer) 11 127.0.0.1:6379> decrby views 5 # 自减5 (integer) 6 127.0.0.1:6379>
字符串范围截取
127.0.0.1:6379> set key1 "hello,qileyun" #设置key1的值 OK 127.0.0.1:6379> get key1 "hello,qileyun" 127.0.0.1:6379> GETRANGE key1 0 3 #截取字符串 [0,3] "hell" 127.0.0.1:6379> GETRANGE key1 0 -1 # 获取全部的字符串 "hello,qileyun"
替换
127.0.0.1:6379> set key2 abcdefg OK 127.0.0.1:6379> get key2 "abcdefg" 127.0.0.1:6379> SETRANGE key2 1 xx # 替换制定位置的字符串 (integer) 7 127.0.0.1:6379> get key2 "axxdefg"
setex 设置过期时间
setnx 不存在在设计
127.0.0.1:6379> setex key3 30 "hello" 设置一个 30秒过期的数据 OK 127.0.0.1:6379> ttl key3 (integer) 26 127.0.0.1:6379> setnx mykey "redis" #不存在设计值 (integer) 1 127.0.0.1:6379> keys * 1) "mykey" 2) "name" 3) "key1" 4) "views" 5) "age" 6) "key2" 127.0.0.1:6379> setnx mykey "MongoDB" # 已经存在不设置新的值 (integer) 0 127.0.0.1:6379> get mykey "redis"
mset 批量设置值
mget 批量获取值
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3 OK 127.0.0.1:6379> keys * 1) "key1" 2) "k1" 3) "k3" 4) "views" 5) "age" 6) "mykey" 7) "name" 8) "key2" 9) "k2" 127.0.0.1:6379> mget k1 k2 k3 1) "v1" 2) "v2" 3) "v3" 127.0.0.1:6379> msetnx k1 v1 k4 v3 #批量设置 但是是原子性操作 要么全部成功要么全部失败 (integer) 0
对象
127.0.0.1:6379> set user:1 {name:zhangsan,age:3} #设置一个user:1 对象 值为json 字符来保持一个对象 OK 127.0.0.1:6379> mset user:1:name zhangsan user:1:age 2 OK
getset 先获取在设置值
127.0.0.1:6379> getset db redis # 如果值不存在,则返回nil (nil) 127.0.0.1:6379> get db "redis" 127.0.0.1:6379> getset db mongodb # 如果值存在,获取原来的值,并设置新的值 "redis" 127.0.0.1:6379> get db "mongodb"
String类型使用场景:value除了我们的字符串还可以是我们数字!
基本的数据类型,列表
在redis里面我们可以把list完成,栈、队列、堵塞队列!
所有的list命令都是l开头
*添加值
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 # 查看所有的值 1) "three" 2) "two" 3) "one" 127.0.0.1:6379> lrange list 0 1 # 查看0到1区间的值 1) "three" 2) "two" 127.0.0.1:6379> Rpush list righr (integer) 4 127.0.0.1:6379> lrange list 0 -1 #向尾部插入一个值 1) "three" 2) "two" 3) "one" 4) "righr"
移除值
127.0.0.1:6379> lpop list "three" 127.0.0.1:6379> rpop list "righr" 127.0.0.1:6379> lrange list 0 -1 1) "two" 2) "one"
通过下标获取制定的值
127.0.0.1:6379> lindex list 1 "one"
获取list的长度
127.0.0.1:6379> llen list (integer) 2
移除指定的值
127.0.0.1:6379> lrem list 1 one # 移除list集合指定的值 (integer) 1 127.0.0.1:6379> lpush list three# (integer) 2 127.0.0.1:6379> lpush list three (integer) 2 127.0.0.1:6379> lpush list three (integer) 3 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个指定的值 (integer) 2 127.0.0.1:6379> lrange list 0 -1 1) "two"
trim修剪
127.0.0.1:6379> Rpush mylist "hello" (integer) 1 127.0.0.1:6379> Rpush mylist "hello1" (integer) 2 127.0.0.1:6379> Rpush mylist "hello2" (integer) 3 127.0.0.1:6379> Rpush mylist "hello3" (integer) 4 127.0.0.1:6379> ltrim mylist 1 2 # 通过下标截取指定的长度,这个list已经被改变了,截断只剩下截取的元素 OK 127.0.0.1:6379> lrange list 0 -1 1) "two" 127.0.0.1:6379> lrange mylist 0 -1 1) "hello1" 2) "hello2"
rpoplpush 移除列表最后一个元素,将他移动到新的列表中
127.0.0.1:6379> rpush mylist "hello" (integer) 1 127.0.0.1:6379> rpush mylist "hello1" (integer) 2 127.0.0.1:6379> rpush mylist "hello2" (integer) 3 127.0.0.1:6379> rpoplpush mylist myotherlist # 移除列表最后一个元素,将它移动的到新的列表中! "hello2" 127.0.0.1:6379> lrange mylist 0 -1 # 查看原来的列表 1) "hello" 2) "hello1" 127.0.0.1:6379> lrange myotherlist 0 -1 #查看目标的列表,确实存在改值 1) "hello2"
lset 把列表指定下标的值替换为另外一个值,更新操作
127.0.0.1:6379> lpush llist value1 (integer) 1 127.0.0.1:6379> lrange list 0 0 (empty list or set) 127.0.0.1:6379> lrange llist 0 0 1) "value1" 127.0.0.1:6379> lset llist 0 item # 如果存在,更新当前下标的值 如果不存在,会报错哦 OK 127.0.0.1:6379> lrange llist 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" "other" #插入一个other 放在world前面 (integer) 3 127.0.0.1:6379> lrange mylist 0 -1 1) "hello" 2) "other" 3) "world" 127.0.0.1:6379> linsert mylist after "world" "new" #插入一个new 放在world后面 (integer) 4 127.0.0.1:6379> lrange mylist 0 -1 1) "hello" 2) "other" 3) "world" 4) "new"
set中都值是不可以重复的
127.0.0.1:6379> sadd myset "hello" # set集合中添加元素 (integer) 1 127.0.0.1:6379> sadd myset "qileyun" (integer) 1 127.0.0.1:6379> sadd myset "lele" (integer) 1 127.0.0.1:6379> smembers myset # 查看指定set的所有值 1) "lele" 2) "qileyun" 3) "hello" 127.0.0.1:6379> sismember myset hello # 判断某一个值是不是set集合中 (integer) 1 127.0.0.1:6379> sismember myset wrold (integer) 0
获取set元素的个数
scard myset #获取set集合中内容元素的个数
移除一个元素
127.0.0.1:6379> srem myset hello (integer) 1 127.0.0.1:6379> scard myset (integer) 2 127.0.0.1:6379> smembers myset 1) "lele" 2) "qileyun"
set 无序不重复集合,抽随机!
127.0.0.1:6379> SRANDMEMBER myset # 随机抽选一个元素 "lele" 127.0.0.1:6379> SRANDMEMBER myset "qileyun" 127.0.0.1:6379> SRANDMEMBER myset "qileyun" 127.0.0.1:6379> SRANDMEMBER myset "lele" 127.0.0.1:6379> SRANDMEMBER myset 2 # 随机抽选出指定个数的元素 1) "qileyun" 2) "lele"
删除指定的key or 删除随机的key
127.0.0.1:6379> spop myset # 随机送出一些set集合的元素 "qileyun" 127.0.0.1:6379> spop myset "lele"
将一个指定的值,移动到另外一个set集合
127.0.0.1:6379> sadd myset "hello" (integer) 1 127.0.0.1:6379> sadd myset "world" (integer) 1 127.0.0.1:6379> sadd myset "qileyun" (integer) 1 127.0.0.1:6379> sadd myset2 "set2" (integer) 1 127.0.0.1:6379> smove myset myset2 "qileyun" (integer) 1 127.0.0.1:6379> SMEMBERS myset 1) "world" 2) "hello" 127.0.0.1:6379> SMEMBERS myset2 1) "qileyun" 2) "set2"
数字集合类
127.0.0.1:6379> sadd key1 a (integer) 1 127.0.0.1:6379> sadd key1 b (integer) 1 127.0.0.1:6379> 127.0.0.1:6379> sadd key1 c (integer) 1 127.0.0.1:6379> sadd key2 c (integer) 1 127.0.0.1:6379> sadd key2 d (integer) 1 127.0.0.1:6379> sadd key2 e (integer) 1 127.0.0.1:6379> sdiff key1 key2 # 差集 1) "a" 2) "b" 127.0.0.1:6379> SINTER key1 key2 # 交集 1) "c" 127.0.0.1:6379> sunion key1 key2 # 并集 1) "b" 2) "c" 3) "d" 4) "e" 5) "a"
Map集合,key-map!时候这个值一个map集合!
127.0.0.1:6379> hset myhash field1 qileyun # set 一个key-value (integer) 1 127.0.0.1:6379> hget myhash field1 # 获取一个字段值 "qileyun" 127.0.0.1:6379> hmset field1 hello field2 world (error) ERR wrong number of arguments for HMSET 127.0.0.1:6379> hmset myhash field1 hello field2 world # set 多个key-value OK 127.0.0.1:6379> hmget myhash field1 field2 # 获取多个值 1) "hello" 2) "world" 127.0.0.1:6379> hgetall myhash # 获取hash中全部到数据 1) "field1" 2) "hello" 3) "field2" 4) "world"
删除指定的值
127.0.0.1:6379> hdel myhash field1 # 删除指定的key字段 (integer) 1
获取长度
127.0.0.1:6379> hlen myhash # 获取长度 (integer) 1
判断hash中指定字段是否存在
127.0.0.1:6379> hexists myhash field1 (integer) 0 127.0.0.1:6379> hexists myhash field2 (integer) 1
获取所有的key和值
127.0.0.1:6379> hkeys myhash 1) "field2" 2) "field1" 127.0.0.1:6379> hvals myhash 1) "world" 2) "hello"
自增自减
127.0.0.1:6379> hset myhash field3 -1 #指定增量 (integer) 1 127.0.0.1:6379> HINCRBY myhash field3 1 (integer) 0 127.0.0.1:6379> HINCRBY myhash field3 -1 (integer) -1 127.0.0.1:6379> hsetnx myhash field4 hello # 如果不存在则可以设置 (integer) 1 127.0.0.1:6379> hsetnx myhash field4 world# 如果存在则不能设置 (integer) 0
hash变更的数据user name age,尤其是用户信息之类的,经常变动的信息!
在set的基础上,增加 了一个值,set k1 v1 zset k1 score1 v1
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 25000 lele # 添加三个用户 (integer) 1 127.0.0.1:6379> zadd salary 25030 aiai (integer) 1 127.0.0.1:6379> zadd salary 35030 ez (integer) 1 127.0.0.1:6379> zrangebyscore salary -inf +inf # 显示全部数据 从小到大 inf 最大值 -inf 最小值 1) "lele" 2) "aiai" 3) "ez" 127.0.0.1:6379> zrangebyscore salary -inf inf withscores # 显示全部数据 从小到大 并且显示用于排序的数据 1) "lele" 2) "25000" 3) "aiai" 4) "25030" 5) "ez" 6) "35030" 127.0.0.1:6379> zrangebyscore salary -inf 25000 withscores # 显示最小值到25000之间排序的值 1) "lele" 2) "25000" 127.0.0.1:6379> 127.0.0.1:6379> zrevrangebyscore salary inf -inf withscores # 从大到小 1) "ez" 2) "35030" 3) "aiai" 4) "25030" 5) "lele" 6) "25000"
移除元素
127.0.0.1:6379> zrange salary 0 -1 1) "lele" 2) "aiai" 3) "ez" 127.0.0.1:6379> zrem salary lele # 移除指定元素 (integer) 1 127.0.0.1:6379> zrange salary 0 -1 1) "aiai" 2) "ez"
获取集合中个数
127.0.0.1:6379> zcard salary (integer) 2
区间包含了多少个数据
比如可以提交公司1000到2000工资之间人员个数
127.0.0.1:6379> zadd myset 1 hello # 添加一个元素 (integer) 1 127.0.0.1:6379> zadd myset 2 hello 2 world 3 qileyun # (integer) 2 127.0.0.1:6379> zcount myset 1 3 # 获取[1,3] 区间的个数 (integer) 3 127.0.0.1:6379> zcount myset 1 2 (integer) 2
朋友的定位,附近的人,打车距离等计算?
Redis的Geo在Redis3.2 版本就推出,这个功能可以推算地理的信息,两地之间的距离
geoadd
添加地理位置
规则:两级无法直接添加,我们一般会下载城市数据,总结通过java程序一次性导入!
参数key 值(纬度、经度、名称)
127.0.0.1:6379> geoadd china:city 116.40 39.90 beijing (integer) 1 127.0.0.1:6379> geoadd china:city 121.47 31.23 sanghai (integer) 1 127.0.0.1:6379> geoadd china:city 106.50 29.53 chongqing (integer) 1 127.0.0.1:6379> geoadd china:city 114.05 22.52 shengzheng (integer) 1 127.0.0.1:6379> geoadd china:city 120.16 30.24 hangzhou (integer) 1 127.0.0.1:6379> geoadd china:city 108.96 34.26 xian (integer) 1
getpos
获取当前的定位:一定是一个坐标值
127.0.0.1:6379> geopos china:city beijin # 获取制定的城市的经度和纬度 1) 1) "166.40000134706497192" 2) "39.90000009167092543" 127.0.0.1:6379> geopos china:city chongqing 1) 1) "106.49999767541885376" 2) "29.52999957900659211"
getodist
计算2个人的距离
127.0.0.1:6379> GEODIST china:city beijin chongqing "5493356.2575" 127.0.0.1:6379> GEODIST china:city beijin chongqing km # 查看重庆到北京的直线距离 "5493.3563"
georadius
我附近的人?(获取所有附近的人地址,定位)
127.0.0.1:6379> georadius china:city 110 30 1000 km # 查询坐标110 30 半径1000公里的城市 1) "chongqing" 2) "xian" 3) "shengzheng" 4) "hangzhou" 127.0.0.1:6379> georadius china:city 110 30 500 km # 查询坐标110 30 半径500公里的城市 1) "chongqing" 2) "xian" 127.0.0.1:6379> 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> 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> georadius china:city 110 30 500 km withdist withcoord count 1 #赛选制定的结果 1) 1) "chongqing" 2) "341.9374" 3) 1) "106.49999767541885376" 2) "29.52999957900659211"
georadiusbymember
127.0.0.1:6379> georadiusbymember china:city chongqing 1000 km # 查询重庆半径1000公里的城市 1) "chongqing" 2) "xian"
geohash
geohash 用于获取一个或多个位置元素的 geohash 值。
如果2个字符串越接近,那么距离越近
127.0.0.1:6379> geohash china:city chongqing 1) "wm5xzrybty0" 127.0.0.1:6379> geohash china:city chongqing hanghai 1) "wm5xzrybty0" 2) "wtw3sj5zbj0"
GEO 底层的实现原理其实就是Zset!我们可以使用Zset 命令操作geo
127.0.0.1:6379> zrange china:city 0 -1 1) "chongqing" 2) "xian" 3) "shengzheng" 4) "hangzhou" 5) "hanghai" 6) "beijin" 127.0.0.1:6379> zrem china:city xian # 移除指定元素 (integer) 1 127.0.0.1:6379> zrange china:city 0 -1 1) "chongqing" 2) "shengzheng" 3) "hangzhou" 4) "hanghai" 5) "beijin"
什么是基数
A{1,3,5,7,8,7}
B{1,3,5,7,8}
基数(不重复的元素) =5 ,可以接受误差!
Redis 2.8.9 版本就更新了Hyperloglog数据结构!
Redis Hyperloglog 基数统计的算法!
优点:占用的内存是固定,2^64不同的元素的技术,值需要废12kb内存,如果要从内存角度来判断比较的话hyperloglog首选!
网页UV(一个人访问英国网站多次,当是还是算作一个人)
传统的方式,set保持用户的id,然后就可以统计元素数量作为标准判断!
这个方式如果保存大量用户id,就会比较麻烦!我们目标是为两计数,而不是保存用户id;
测试使用
127.0.0.1:6379> PFadd mykey a b c d e f g h i j # 添加第一组元素 mykey (integer) 1 127.0.0.1:6379> pfcount mykey # 统计mykey元素基数数量 (integer) 10 127.0.0.1:6379> pfadd mykey2 i j z x c v b n m# 添加第二组元素 mykey2 (integer) 1 127.0.0.1:6379> pfcount mykey2 (integer) 9 127.0.0.1:6379> pfmerge mykey3 mykey mykey2 # 合并两组 mykey mykey2 => mykey3 并集 OK 127.0.0.1:6379> pfcount mykey3 # 看并集的数量 (integer) 15
如果允许容错,那么一定可以使用hyperloglog
如果不允许就使用set作为数据类型即可
位储存
统计用户信息、活跃、登录、未登录、365打卡!两个状态,都可以使用Bitmaps!
Bitmaps位图 ,数据结构!都是操作二进制位进行记录,就只有0和1两个状态!
365天 = 365bit 1字节 = 8bit 46个字节左右!
使用bitmap来记录 周一到周日都打卡!
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 0 (integer) 0 127.0.0.1:6379> setbit sign 6 1 (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 127.0.0.1:6379> bitcount sign 0 3 (integer) 4
Redis事物本质 :一组命令的集合!一个事物的所有命令都会被序列化,在事物执行过程中,会安装顺序执行!
一次性、顺序性、排他性!执行一系列的命令!
Redis事物没有隔离级别的概念!
所有的命令在事物中,并没有直接被执行!只有发起执行的命令的时候才会执行!
-----队列 set set set 执行------
Redis单条命令式保存原子性,但是事物不保证原子性
Redis的事物:
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> get k2 QUEUED 127.0.0.1:6379> set k3 v3 QUEUED 127.0.0.1:6379> exec #执行事物 1) OK 2) OK 3) "v2" 4) OK
放弃事物
127.0.0.1:6379> multi OK 127.0.0.1:6379> set k1 v1 QUEUED 127.0.0.1:6379> set k2 v2 QUEUED 127.0.0.1:6379> set k4 v4 QUEUED 127.0.0.1:6379> discard # 放弃执行事物 事物队列命令都不会处理 OK 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> set k4 v4 QUEUED 127.0.0.1:6379> set k5 v5 QUEUED 127.0.0.1:6379> exec# 提交事务一样报错 (error) EXECABORT Transaction discarded because of previous errors. 127.0.0.1:6379> get 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 k3 v3 QUEUED 127.0.0.1:6379> get k3 QUEUED 127.0.0.1:6379> exec #虽然第一条命令报错,运行时异常依然可以执行成功 1) (error) ERR value is not an integer or out of range 2) OK 3) OK 4) "v3" 127.0.0.1:6379> get k2 "v2" 127.0.0.1:6379> get k3 "v3"
悲观锁:
乐观锁:
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 对象 相当于那money当作来乐观锁的version 用来判断 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> unwatch # 解除监视 OK 127.0.0.1:6379> watch money # 重新添加监视最新的值 OK 127.0.0.1:6379> multi #如果这个时候没有其他线程去修改money就可以提交成功 OK 127.0.0.1:6379> decrby money 10 QUEUED 127.0.0.1:6379> incrby out 10 QUEUED 127.0.0.1:6379> exec 1) (integer) 990 2) (integer) 30
使用java操作redis
什么是jedis 是redis官方推荐的java连接工具!使用java操作redis中间件!如果你要使用java操作redis,那么一定要对jedis十分熟悉!
1、导入依赖
<dependencies> <!-- jedis --> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>4.0.1</version> </dependency> <!--fastjson--> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.79</version> </dependency> </dependencies>
2、编码测试:
//1、 new Jedis对象 Jedis jedis = new Jedis("127.0.0.1",6379); //jedis 所有命令就是我们之前学习对所有指令 System.out.println(jedis.ping());//输出PONG 表示连接成功 jedis.close();//关闭连接
采用的API
System.out.println("清空数据库"+jedis.flushDB()); System.out.println("判断某个key是否存在:"+ jedis.exists("name")); System.out.println("新增username,qileyun键值对:"+jedis.set("username","qileyun")); System.out.println("新增password,123456键值对:"+jedis.set("password","123456")); System.out.println("系统中所有的key 如下:"); Set<String> keys = jedis.keys("*"); System.out.println(keys); System.out.println("删除key password:"+jedis.del("password")); System.out.println("判断key password是否存在:"+jedis.exists("password")); System.out.println("查看key username所存储的类型:"+jedis.type("username")); 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());
判断某个key是否存在:false 新增username,xy键值对:OK 新增password,123键值对:OK 系统中所有的key 如下: [password, username] 删除key password:1 判断key password是否存在:false 查看key username所存储的类型:string 重命名key OK 取出修改后的name:qileyun 按索引查询:OK 删除当前数据库中的所有key :OK 返回数据库中所有的key 数目:0 删除所有数据库中的key:OK
jedis.flushDB(); jedis.set("key1","val1"); jedis.set("key2","val2"); jedis.set("key3","val3"); 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("在key3后面加入数据:"+jedis.append("key3","Emd")); System.out.println("key3的值:"+jedis.get("key3")); System.out.println("增加多个键值对:"+jedis.mset("key01","value01","key02","value02","key03","value03","key04","value04")); 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")); 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 (Exception e){ e.printStackTrace(); } System.out.println(jedis.get("key3")); System.out.println("==========获取原值,更新为新值================"); System.out.println(jedis.getSet("key2","keyGetSet")); System.out.println(jedis.get("key2")); System.out.println("获得key2的值的字符串:"+jedis.getrange("key2",2,4));
输出:
删除key2:1 获取key2:null 修改key1:OK 在key3后面加入数据:7 key3的值:val3Emd 增加多个键值对:OK 获取多个键值对:[value01, value02, value03] 获取多个键值对:[value01, value02, value03, value04] 删除多个键值对:2 获取多个键值对:[null, null] ==========新增键值对并设置有效时间================ 1 1 0 value1 value2 ==========设置有效时间================ OK value3 null ==========获取原值,更新为新值================ value2 keyGetSet 获得key2的值的字符串:yGe
System.out.println("添加一个List"); jedis.lpush("collections","ArrayList","LinkedList","Vector","Stack","Map","HashMap"); jedis.lpush("collections","Set"); jedis.lpush("collections","HashSet"); jedis.lpush("collections","TreeMap"); System.out.println("Collection集合内容:"+jedis.lrange("collections",0,-1));//-1表示最后一个元素 System.out.println("Collection区间0-3的元素"+jedis.lrange("collections",0,3)); System.out.println("=============================="); //删除列表指定的值,第二个参数为删除的个数(有重复时),后面add进行的值先被删除,类似出栈 System.out.println("删除指定元素个数:"+jedis.lrem("collections",2,"HashMap")); System.out.println("collections内容:"+jedis.lrange("collections",0,-1)); System.out.println("删除下标0-3区间之外的元素:"+jedis.ltrim("collections",0,3)); System.out.println("collections内容:"+jedis.lrange("collections",0,-1)); System.out.println("collections列表出栈(左端):"+jedis.lpop("collections")); System.out.println("collections内容:"+jedis.lrange("collections",0,-1)); System.out.println("collections添加元素,从列表右端,与lpush对应"+jedis.rpush("collections","Java")); System.out.println("collections内容:"+jedis.lrange("collections",0,-1)); System.out.println("修改collections指定下标1的内容:"+jedis.lset("collections",1,"newValue")); System.out.println("collections内容:"+jedis.lrange("collections",0,-1)); System.out.println("================================="); System.out.println("collections长度"+jedis.llen("collections")); System.out.println("获取collections下标为2的长度"+jedis.lindex("collections",2)); System.out.println("================================="); System.out.println(jedis.lpush("sortedList","3","6","2","0","7","4")); System.out.println("sortedList排序前:"+jedis.lrange("sortedList",0,-1)); //排序 List<String> sortedList = jedis.sort("sortedList"); System.out.println("sortedList排序后:"+sortedList);
输出:
添加一个List Collection集合内容:[TreeMap, HashSet, Set, HashMap, Map, Stack, Vector, LinkedList, ArrayList] Collection区间0-3的元素[TreeMap, HashSet, Set, HashMap] ============================== 删除指定元素个数:1 collections内容:[TreeMap, HashSet, Set, Map, Stack, Vector, LinkedList, ArrayList] 删除下标0-3区间之外的元素:OK collections内容:[TreeMap, HashSet, Set, Map] collections列表出栈(左端):TreeMap collections内容:[HashSet, Set, Map] collections添加元素,从列表右端,与lpush对应4 collections内容:[HashSet, Set, Map, Java] 修改collections指定下标1的内容:OK collections内容:[HashSet, newValue, Map, Java] ================================= collections长度4 获取collections下标为2的长度Map ================================= 6 sortedList排序前:[4, 7, 0, 2, 6, 3] sortedList排序后:[0, 2, 3, 4, 6, 7] Process finished with exit code 0
jedis.flushDB(); System.out.println("=================往集合里添加元素(不重复)========================"); System.out.println(jedis.sadd("eleSet","e1","e2","e3","e4","e6","e5","e0","e8","e7")); System.out.println(jedis.sadd("eleSet","e6")); System.out.println(jedis.sadd("eleSet","e6")); System.out.println("eleSet的所有元素为:"+jedis.smembers("eleSet")); System.out.println("删除一个元素e0:"+jedis.srem("eleSet","e0")); System.out.println("eleSet的所有元素为:"+jedis.smembers("eleSet")); System.out.println("删除两个元素e7,e6:"+jedis.srem("eleSet","e7","e6")); System.out.println("eleSet的所有元素为:"+jedis.smembers("eleSet")); System.out.println("随机移除集合中的一个元素:"+jedis.spop("eleSet")); System.out.println("随机移除集合中的一个元素:"+jedis.spop("eleSet")); System.out.println("eleSet的所有元素为:"+jedis.smembers("eleSet")); System.out.println("eleSet的所有元素的个数为:"+jedis.scard("eleSet")); System.out.println("e3是否在eleSet中:"+jedis.sismember("eleSet","e3")); System.out.println("e1是否在eleSet中:"+jedis.sismember("eleSet","e1")); System.out.println("e5是否在eleSet中:"+jedis.sismember("eleSet","e5")); System.out.println("============================================================="); System.out.println(jedis.sadd("eleSet1","e1","e2","e3","e4","e5","e8","e7")); System.out.println(jedis.sadd("eleSet2","e1","e2","e3","e4","e8")); System.out.println("将eleSet1中删除e1并存入eleSet3中:"+jedis.smove("eleSet1","eleSet3","e1")); System.out.println("将eleSet2中删除e1并存入eleSet3中:"+jedis.smove("eleSet1","eleSet3","e2")); System.out.println("eleSet1中的元素:"+jedis.smembers("eleSet1")); System.out.println("eleSet3中的元素:"+jedis.smembers("eleSet3")); System.out.println("===========================集合运算=================================="); System.out.println("eleSet1中的元素:"+jedis.smembers("eleSet1")); System.out.println("eleSet2中的元素:"+jedis.smembers("eleSet2")); System.out.println("eleSet1和eleSet2的并集:"+jedis.sinter("eleSet1","eleSet2")); System.out.println("eleSet1和eleSet2的并集:"+jedis.sunion("eleSet1","eleSet2")); System.out.println("eleSet1和eleSet2的差集:"+jedis.sdiff("eleSet1","eleSet2")); //求并集并将交集保存到dstkey集合 jedis.sinterstore("eleSet4","eleSet1","eleSet2"); System.out.println("eleSet4的元素:"+jedis.smembers("eleSet4"));
输出:
=================往集合里添加元素(不重复)======================== 9 0 0 eleSet的所有元素为:[e5, e6, e7, e8, e0, e1, e2, e3, e4] 删除一个元素e0:1 eleSet的所有元素为:[e5, e6, e7, e8, e1, e2, e3, e4] 删除两个元素e7,e6:2 eleSet的所有元素为:[e5, e8, e1, e2, e3, e4] 随机移除集合中的一个元素:e5 随机移除集合中的一个元素:e1 eleSet的所有元素为:[e2, e3, e4, e8] eleSet的所有元素的个数为:4 e3是否在eleSet中:true e1是否在eleSet中:false e5是否在eleSet中:false ============================================================= 7 5 将eleSet1中删除e1并存入eleSet3中:1 将eleSet2中删除e1并存入eleSet3中:1 eleSet1中的元素:[e5, e7, e8, e3, e4] eleSet3中的元素:[e1, e2] ===========================集合运算================================== eleSet1中的元素:[e5, e7, e8, e3, e4] eleSet2中的元素:[e8, e1, e2, e3, e4] eleSet1和eleSet2的并集:[e3, e4, e8] eleSet1和eleSet2的并集:[e5, e7, e8, e1, e2, e3, e4] eleSet1和eleSet2的差集:[e7, e5] eleSet4的元素:[e3, e4, e8]
Jedis jedis = new Jedis("127.0.0.1", 6379); //测试是否链接成功,返回pong则代表链接成功 System.out.println("jedis.ping():" + jedis.ping()); //添加元素以及关联分数到集合 System.out.println("jedis.zadd(\"score\", 60, \"a\"):" + jedis.zadd("score", 60, "a")); //批量添加元素 Map<String, Double> map = new HashMap<>(); map.put("b", 65.0); map.put("c", 75.0); map.put("d", 90.5); System.out.println("jedis.zadd(\"score\", map):" + jedis.zadd("score", map)); //获取集合中元素的个数 System.out.println("jedis.zcard(\"score\"):" + jedis.zcard("score")); //按照定义的起始下标与结束下标正序遍历出相应元素,结束下标为-1则表示zset的最后元素 System.out.println("jedis.zrange(\"score\", 1, 3):" + jedis.zrange("score", 1, 3)); //按照定义的起始下标与结束下标倒序遍历出相应元素,结束下标为-1则表示zset的最后元素 System.out.println("jedis.zrevrange(\"score\", 1, 3):" + jedis.zrevrange("score", 1, 3)); //按照定义的分数区间正序遍历元素 System.out.println("jedis.zrangeByScore(\"score\", 1, 3):" + jedis.zrangeByScore("score", 1, 3)); //按照定义的分数区间倒序遍历元素 System.out.println("jedis.zrevrangeByScore(\"score\", 1, 3):" + jedis.zrevrangeByScore("score", 1, 3)); //按照定义的分数区间正序遍历元素,同时带出相应分数 System.out.println("jedis.zrangeByScoreWithScores(\"score\", 1, 3):" + jedis.zrangeByScoreWithScores("score", 1, 3)); //获取定义的分数区间的元素数量 System.out.println("jedis.zcount(\"score\", 1, 3):" + jedis.zcount("score", 1, 3)); //获取元素的score值 System.out.println("jedis.zcount(\"score\", 1, 3):" + jedis.zscore("score", "a")); //正序获取元素的下标 System.out.println("jedis.zrank(\"score\", \"a\"):" + jedis.zrank("score", "a")); //倒序获取元素的下标 System.out.println("jedis.zrevrank(\"score\", \"a\"):" + jedis.zrevrank("score", "a")); //删除元素 System.out.println("jedis.zrem(\"score\", \"a\"):" + jedis.zrem("score", "a")); //通过下标范围删除元素 System.out.println("jedis.zremrangeByRank(\"score\", 1, 2):" + jedis.zremrangeByRank("score", 1, 2)); //通过分数范围删除元素 System.out.println("jedis.zremrangeByScore(\"score\", 60, 85):" + jedis.zremrangeByScore("score", 60, 85)); //增加指定分数 System.out.println("jedis.zremrangeByScore(\"score\", 60, 85):" + jedis.zincrby("score", 6, "a")); jedis.close();
运行结果:
jedis.ping():PONG jedis.zadd("score", 60, "a"):1 jedis.zadd("score", map):3 jedis.zcard("score"):4 jedis.zrange("score", 1, 3):[b, c, d] jedis.zrevrange("score", 1, 3):[c, b, a] jedis.zrangeByScore("score", 1, 3):[] jedis.zrevrangeByScore("score", 1, 3):[] jedis.zrangeByScoreWithScores("score", 1, 3):[] jedis.zcount("score", 1, 3):0 jedis.zcount("score", 1, 3):60.0 jedis.zrank("score", "a"):0 jedis.zrevrank("score", "a"):3 jedis.zrem("score", "a"):1 jedis.zremrangeByRank("score", 1, 2):2 jedis.zremrangeByScore("score", 60, 85):1 jedis.zremrangeByScore("score", 60, 85):6.0
Map<String,String> hash = new HashMap<String, String>(); hash.put("k1","v1"); hash.put("k2","v2"); hash.put("k3","v3"); hash.put("k4","v4"); //添加名称为hash(key)的元素 jedis.hmset("hash",hash); //向名称为hash的hash中添加key k5 value 为v5 jedis.hset("hash","k5","v5"); System.out.println("散列hash的所有键值对为:"+jedis.hgetAll("hash")); System.out.println("散列hash的所有键:"+jedis.hkeys("hash")); System.out.println("散列hash的所有值:"+jedis.hvals("hash"));
运行结果:
散列hash的所有键值对为:{k3=v3, k4=v4, k5=v5, k1=v1, k2=v2} 散列hash的所有键:[k3, k4, k5, k1, k2] 散列hash的所有值:[v1, v2, v3, v4, v5]
//开启事务 Transaction transaction = jedis.multi(); JSONObject jsonObject = new JSONObject(); jsonObject.put("hello", "world"); jsonObject.put("name", "xy"); String result = jsonObject.toJSONString(); try { transaction.set("user1", result); transaction.set("user2", result); //模拟异常 int i = 1 /0; transaction.exec(); } catch (Exception e) { //发生异常 放弃事务 transaction.discard(); e.printStackTrace(); }finally{ System.out.println(jedis.get("user1")); System.out.println(jedis.get("user2")); jedis.close();//关闭连接 }
输出:
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder". SLF4J: Defaulting to no-operation (NOP) logger implementation SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details. java.lang.ArithmeticException: / by zero at cn.qileyun.TestPing.main(TestPing.java:28) null null
SpringBoot操作数据:spring-data
说明:在springBoot2.x之后,原来使用的jedis被替换为两lettuce
jeids:采用的直连,多个线程操作的话,是不安全的,如果想避免不安全的,使用jedis pool连接池!BIO
lettuce:采用netty,实例可以在多个线程中进行共享,不存在线程不安全的情况!可以减少线程数据来,更像NIO模式
源码分析:
@Bean @ConditionalOnMissingBean(name = {"redisTemplate"})//我们可以自定义一个redisTemplate来替换这个默认的! public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException { //默认的redisTemplate 没有过多的设置 redis对象都是需要序列化! //两个泛型都是object的类型,我们后使用强制转换<string,object> RedisTemplate<Object, Object> template = new RedisTemplate(); template.setConnectionFactory(redisConnectionFactory); return template; } @Bean @ConditionalOnMissingBean //由于string是redis中最常用使用的类型,所以单独提出来一个bean public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException { StringRedisTemplate template = new StringRedisTemplate(); template.setConnectionFactory(redisConnectionFactory); return template; }
1、导入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
2、配置
#配置redis spring.redis.host=127.0.0.1 spring.redis.port=6379
3、测试
@SpringBootTest class Redis02SpringbootApplicationTests { @Autowired private RedisTemplate redisTemplate; @Test void contextLoads() { //redisTemplate 操作不同的数据类型,api和我们指令是一样 //opsForValue 操作字符串 //opsForList 操作list //openForSet //openForHash //openForGeo // 除了基本的操作,我们采用的方法都可以直接通过redisTemplate操作,比如事物和CRUD //获取redis的连接对象 //RedisClusterConnection connection = redisTemplate.getConnectionFactory().getClusterConnection(); //connection.flushDb(); //connection.flushAll(); redisTemplate.opsForValue().set("mykey", "我爱Java"); System.out.println(redisTemplate.opsForValue().get("mykey")); } }
@Configuration public class RedisConfig { /** * 自己定义了一个 RedisTemplate */ @Bean @SuppressWarnings("all") public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { // 我们为了自己开发方便,一般直接使用 <String, Object> RedisTemplate<String, Object> template = new RedisTemplate<String,Object>(); template.setConnectionFactory(factory); // 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; } }
在我们真实的开发中,一般都会封装一个自己的redisutils
@Component public final class RedisUtil { @Resource private RedisTemplate<String, Object> redisTemplate; // =============================common============================ /** * 指定缓存失效时间 * * @param key 键 * @param time 时间(秒) */ public boolean expire(String key, long time, TimeUnit timeUnit) { try { if (time > 0) { redisTemplate.expire(key, time, timeUnit); } 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, TimeUnit timeUnit) { try { redisTemplate.opsForHash().putAll(key, map); if (time > 0) { expire(key, time, timeUnit); } 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, TimeUnit timeUnit) { try { redisTemplate.opsForHash().put(key, item, value); if (time > 0) { expire(key, time, timeUnit); } 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, TimeUnit timeUnit, Object... values) { try { Long count = redisTemplate.opsForSet().add(key, values); if (time > 0) { expire(key, time, timeUnit); } 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,TimeUnit timeUnit) { try { redisTemplate.opsForList().rightPush(key, value); if (time > 0) { expire(key, time,timeUnit); } 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, TimeUnit timeUnit) { try { redisTemplate.opsForList().rightPushAll(key, value); if (time > 0) { expire(key, time,timeUnit); } 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; } } // ===============================HyperLogLog================================= public long pfadd(String key, String value) { return redisTemplate.opsForHyperLogLog().add(key, value); } public long pfcount(String key) { return redisTemplate.opsForHyperLogLog().size(key); } public void pfremove(String key) { redisTemplate.opsForHyperLogLog().delete(key); } public void pfmerge(String key1, String key2) { redisTemplate.opsForHyperLogLog().union(key1, key2); } }
测试看看
@Test public void test1(){ redisUtil.set("name","qileyun"); System.out.println(redisUtil.get("name"));//qileyun }
配置文件unit是单位 ,是对大小不敏感的 Gb GB gb
就是好比我们学习spring、improt,iclude
bind 127.0.0.1 ::1 #绑定对ip protected-mode yes #保护模式 port 6379 #端口设置
daemonize no #是否以守护线程运行,默认是no pidfile /var/run/redis_6379.pid #如果以后台方式运行,我们就需要指定一个pid文件
# Specify the server verbosity level. # This can be one of: # debug (a lot of information, useful for development/testing) # verbose (many rarely useful info, but not a mess like the debug level) # notice (moderately verbose, what you want in production probably) 生产环境 # warning (only very important / critical messages are logged) loglevel notice #配置日志级别 logfile "" 日志对文件位置名 databases 16 # 数据库的数量,默认是16个 always-show-log yes #是否开始显示logo
持久化:在规定的时间内,执行了多少次操作,则会持久化wjian .rdb .aof
Redis 是内存数据库,如果没有持久化,那么数据断电即失
save 900 1 # 900 秒内,如果至少有一个key进行了操作就执行持久化操作 save 300 10 #300秒 如果10次就持久化 save 60 10000 # 我们之后学习持久化,会自定义定义 stop-writes-on-bgsave-error yes #持久化如果出错是否继续工作 rdbcompression yes #是否压缩rdb文件,需要消耗一些cpu资源 rdbchecksum yes #保存rdb文件的时候,进行错误的检查校验 dir ./ #rdb文件保存的路径
replicaof <masterip> <masterport> #配置主机的ip和端口 masterauth <master-password> #如果有主机有密码配置
默认是没有密码
127.0.0.1:6379> config set requirepass "123456" #设置redis的密码 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> config get requirepass # 获取redis密码 1) "requirepass"
maxclients 10000 # 设置最大连接数量 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 : 永不过期,返回错误
appendonly no #默认是不开启aof模式的,默认是使用rdb方式持久化的,在大部分所有情况下 ,rdb完全够用 appendfilename "appendonly.aof" #持久化文件的名字 appendfsync everysec# 每秒执行一次 sync ,可能会丢失这一秒的数据 #appendfsync always #每次修改都会 sync,速度比较慢 #appendfsync no #不执行 sync,这个时候操作系统自己同步数据,速度最快!
由于Redis是基于内存的数据库,需要将数据由内存持久化到文件中
持久化方式:
在指定的时间间隔内将内存中的数据集快照写入磁盘,行话将的:Snapshot 快照,它恢复是将快照文件直接读到内存里。
Redus会单独创建(Fork) 一个子进程来进程持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO 操作的。这就确保了极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF 方式更加的高效,RDB 缺点是最后一次持久化后的数据库可能丢失。我们默认的就是RDB,一般情况下不需要修改这个配置。
dbfilename dump.rdb # 保存的文件名 save 60 5 #测试60秒保存5次触发rdb
127.0.0.1:6379> set k1 v1 OK 127.0.0.1:6379> set k2 v2 OK 127.0.0.1:6379> set k3 v3 OK 127.0.0.1:6379> set k4 bv4 OK 127.0.0.1:6379> set k5 k5
文件已经被保存在dump.rdb文件中来
关闭在开启redis服务端 发现数据依然在
127.0.0.1:6379> shutdown not connected> # imac @ iMac in ~ [16:55:16] $ redis-cli 127.0.0.1:6379> keys * 1) "k2" 2) "user" 3) "k6" 4) "k5" 5) "k4" 6) "k3" 7) "name" 8) "k1" 127.0.0.1:6379>
由于 save 命令是同步命令,会占用Redis的主进程。若Redis数据非常多时,save命令执行速度会非常慢,阻塞所有客户端的请求。
如果需要恢复数据,只需将备份文件 (dump.rdb) 移动到 redis 安装目录并启动服务即可。获取 redis 目录可以使用 CONFIG 命令,如下所示:
如果在 这个 /Users/imac或者dir 目录下存放 dump.rdb文件,启动就会自动恢复其中的数据。
127.0.0.1:6379> config get dir 1) "dir" 2) "/Users/imac"
几乎他自己默认的配置就够用了!
flushall命令
flushall 命令也会触发持久化 ;
触发持久化规则
满足配置条件中的触发条件 ;
可以通过配置文件对 Redis 进行设置, 让它在“ N 秒内数据集至少有 M 个改动”这一条件被满足时, 自动进行数据集保存操作。
bgsave
bgsave 是异步进行,进行持久化的时候,redis 还可以将继续响应客户端请求 ;
bgsave和save对比
命令 | save | bgsave |
---|---|---|
IO类型 | 同步 | 异步 |
阻塞? | 是 | 是(阻塞发生在fock(),通常非常快) |
复杂度 | O(n) | O(n) |
优点 | 不会消耗额外的内存 | 不阻塞客户端命令 |
缺点 | 阻塞客户端命令 | 需要fock子进程,消耗内存 |
几乎就他自己默认的配置就够用来,但是我们还是需要去学习!
优点
缺点
将我们所有的命令都记录下来,history,恢复的时候就把这个文件全部再执行一遍
以日志的形式来记录每个写的操作,将Redis执行过的所有指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。
什么是AOF
快照功能(RDB)并不是非常耐久(durable): 如果 Redis 因为某些原因而造成故障停机, 那么服务器将丢失最近写入、以及未保存到快照中的那些数据。 从 1.1 版本开始, Redis 增加了一种完全耐久的持久化方式: AOF 持久化。
如果要使用AOF,需要修改配置文件:
默认是不开启的,我们需要手动配置,然后重启redis,就可以生效了!
appendonly no yes则表示启用AOF
appendonly yes #开启aof appendfilename "appendonly.aof" #保存的文件名 appendfsync everysec # appendfsync everysec 每秒执行一次 sync ,可能会丢失这一秒的数据 #appendfsync always 每次修改都会 sync,速度比较慢 #appendfsync no 不执行 sync,这个时候操作系统自己同步数据,速度最快!
重启即可生效
127.0.0.1:6379> set k1 v1 OK 127.0.0.1:6379> set k2 v2 OK 127.0.0.1:6379> set k3 v3 OK 127.0.0.1:6379> set k4 v4 OK 127.0.0.1:6379> set k5 v5 OK
查看aof文件
如果这个aof 文件有错位,这个时候redis是启动不起来的,需要修复这个aof 文件
redis 给我们提供了一个工具 redis-check-aof --fix
redis-check-aof --fix appendonly.aof
一种是全丢,只丢错误的数据!
优点
缺点
数据文件来说,aof 远远大于rdb,修改的速度也比rdb慢!
aof 运行效率也要比rdb慢,所有redis 默认配置是rdb 持久化。
重写规则说明
默认就是文件的无限追加,文件越来越大
如果aof 文件大于64m ,太大了!fork 一个新的进来帮我们的文件进行重写!
auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb
RDB | AOF | |
---|---|---|
启动优先级 | 低 | 高 |
体积 | 小 | 大 |
恢复速度 | 快 | 慢 |
数据安全性 | 丢数据 | 根据策略决定 |
如何选择使用哪种持久化方式?
一般来说, 如果想达到足以媲美 PostgreSQL 的数据安全性, 你应该同时使用两种持久化功能。
如果你非常关心你的数据, 但仍然可以承受数分钟以内的数据丢失, 那么你可以只使用 RDB 持久化。
有很多用户都只使用 AOF 持久化, 但并不推荐这种方式: 因为定时生成 RDB 快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快。
Redis 发布订阅(pub/sub)是一种消息通讯模式:发送者(pub)发送消息,订阅者(sub)接受消息。微信、微博、关注系统!
Redis客户端可以订阅容易数量的频道。
订阅/发布消息图
第一个:消息发送者
第二个:频道
第三个:消息接收者
Redis 发布订阅 (pub/sub) 是一种消息通信模式:发送者 (pub) 发送消息,订阅者 (sub) 接收消息。
Redis 客户端可以订阅任意数量的频道。
下图展示了频道 channel1 , 以及订阅这个频道的三个客户端 —— client2 、 client5 和 client1 之间的关系:
当有新消息通过 PUBLISH 命令发送给频道 channel1 时, 这个消息就会被发送给订阅它的三个客户端:
下表列出了 redis 发布订阅常用命令:
序号 | 命令及描述 |
---|---|
1 | [PSUBSCRIBE pattern pattern …] 订阅一个或多个符合给定模式的频道。 |
2 | [PUBSUB subcommand argument [argument …]] 查看订阅与发布系统状态。 |
3 | PUBLISH channel message 将信息发送到指定的频道。 |
4 | [PUNSUBSCRIBE pattern [pattern …]] 退订所有给定模式的频道。 |
5 | [SUBSCRIBE channel channel …] 订阅给定的一个或多个频道的信息。 |
6 | [UNSUBSCRIBE channel [channel …]] 指退订给定的频道。 |
订阅段端:
127.0.0.1:6379> subscribe qileyun #订阅qileyun频道 Reading messages... (press Ctrl-C to quit) 1) "subscribe" 2) "qileyun" 3) (integer) 1 1) "message" #消息 2) "qileyun" #那个频道的消息 3) "aaa" #消息的具体内容 1) "message" 2) "qileyun" 3) "bbb"
发送端:
127.0.0.1:6379> publish qileyun "aaa" #向qileyun频道发送一个消息 (integer) 1 127.0.0.1:6379> publish qileyun "bbb" (integer) 1 127.0.0.1:6379>
Redis是使用C实现的,通过分析 Redis 源码里的 pubsub.c 文件,了解发布和订阅机制的底层实现,籍 此加深对 Redis 的理解。
Redis 通过 PUBLISH 、SUBSCRIBE 和 PSUBSCRIBE 等命令实现发布和订阅功能。
微信:
通过 SUBSCRIBE 命令订阅某频道后,redis-server 里维护了一个字典,字典的键就是一个个 频道!, 而字典的值则是一个链表,链表中保存了所有订阅这个 channel 的客户端。SUBSCRIBE 命令的关键, 就是将客户端添加到给定 channel 的订阅链表中。
使用场景
1、实时消息系统!
2、事实聊天!(频道当做聊天室,将信息回显给所有人)
3、订阅、关注系统
稍微复杂的场景我们就会用专业的消息中间间 MQTT
概念
主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(Master/Leader),后者称为从节点(Slave/Follower), 数据的复制是单向的!只能由主节点复制到从节点(主节点以写为主、从节点以读为主)。
默认情况下,每台Redis服务器都是主节点,一个主节点可以有0个或者多个从节点,但每个从节点只能由一个主节点。
作用
为什么使用集群
主从复制,读写分离!80% 的情况下都是在进程读操作!减缓服务器的压力!
架构中经常使用! 一主 二从!(最低配)
只要在公司中,主从复制就是必须要使用的,因为在真实的项目中不可能单机使用Redis!
只配置从库,不用配置主库
127.0.0.1:6379> info replication #查看主机信息 # Replication role:master #角色 master connected_slaves:0 # 没有从机 master_replid:14cf4ecfb07ec70e39ed06cd9079ee5403a80e78 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>
复制三个配置文件
cp redis.conf redis79.conf cp redis.conf redis80.conf cp redis.conf redis81.conf
模拟配置第一台redis服务器
logfile "6379.log" dbfilename 6379dump.rdb pidfile /var/run/redis_6379.pid
模拟配置第二台redis服务器
port 6380 logfile "6380.log" dbfilename 6380dump.rdb pidfile /var/run/redis_6380.pid
模拟配置第三台redis服务器
port 6381 logfile "6381.log" dbfilename 6381dump.rdb pidfile /var/run/redis_6381.pid
测试看看
ps -ef | grep redis 501 53076 1 0 8:46下午 ?? 0:00.37 redis-server 127.0.0.1:6379 501 53128 1 0 8:46下午 ?? 0:00.34 redis-server 127.0.0.1:6380 501 53179 1 0 8:46下午 ?? 0:00.32 redis-server 127.0.0.1:6381
默认情况下每台都是主节点,我们一般情况下只需要配置从机就好了可以自己把其中一台当作主机。
主:79
127.0.0.1:6379> info replication # Replication role:master #主机 connected_slaves:2 # 有2台从机 slave0:ip=127.0.0.1,port=6380,state=online,offset=140,lag=0 #从机信息 slave1:ip=127.0.0.1,port=6381,state=online,offset=140,lag=0 #从机信息
从:80,81
127.0.0.1:6380> slaveof 127.0.0.1 6379 OK 127.0.0.1:6380> info replication # Replication role:slave #从机 master_host:127.0.0.1 master_port:6379
127.0.0.1:6381> slaveof 127.0.0.1 6379 OK 127.0.0.1:6381> info replication # Replication role:slave #从机 master_host:127.0.0.1 master_port:6379
真实的从主配置在配置文件中配置,这样的话就是永久的,我们使用的是命令,暂时的!
replicaof <masterip> <masterport> #配置主机的ip和端口 masterauth <master-password> #如果有主机有密码配置
主机可以写,从机不能写只能读!主机的所有信息和数据,都会被从机保存
主机
127.0.0.1:6379> set k1 v1 #主机写入k1 OK
从机
127.0.0.1:6380> keys * #从机可以读取到 1) "k1" 127.0.0.1:6380> set k2 v2# 但是从机不能进行写 (error) READONLY You can't write against a read only replica.
测试:主机断开连接,从机依旧连接到主机的,但是没有写操作,这个时候,主机如果回来了,从机依 旧可以直接获取到主机写的信息!
如果是使用命令行,来配置的主从,这个时候如果重启了,就会变回主机!只要变为从机,立马就会从 主机中获取值!
复制原理
Slave 启动成功连接到 master 后会发送一个sync同步命令
Master 接到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行 完毕之后,master将传送整个数据文件到slave,并完成一次完全同步。
增量复制:Master 继续将新的所有收集到的修改命令依次传给slave,完成同步
但是只要是重新连接master,一次完全同步(全量复制)将被自动执行! 我们的数据一定可以在从机中 看到!
127.0.0.1:6381> slaveof 127.0.0.1 6380 OK
这是时候也能完成主从复制,6380 即6379的 从节点,也是6381的主节点,但是该节点无法完成写入的。
如果没有老大了(主节点挂了),这个时候能不能选择一个老大出来呢?哨兵模式没出来之前,都是需要手动配置的
谋朝篡位(手动选老大)
如果主机断开了连接,可以使用 slaveof no one 让自己变成主机!其他的节点就可以手动连接到最新的这个主节点(手动)
如果这个时候老大修复了,只能重新配置
127.0.0.1:6380> slaveof no one #住过主节点挂了,可以设置此节点为主节点 OK 127.0.0.1:6380> INFO replication # Replication role:master connected_slaves:1 slave0:ip=127.0.0.1,port=6381,state=online,offset=3006,lag=0
好比谋朝篡位,皇帝下台了,你再回来你也做不了老大了,除非再次手动的把认79作为老大
127.0.0.1:6380> slaveof 127.0.0.1 6379 OK
概念:
主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工 干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑 哨兵模式。Redis从2.8开始正式提供了Sentinel(哨兵) 架构来解决这个问题。
谋朝篡位的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库。
哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。
这里的哨兵有两个作用
通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器。
当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服 务器,修改配置文件,让它们切换主机。
然而一个哨兵进程对Redis服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。 各个哨兵之间还会进行监控,这样就形成了多哨兵模式。
假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认 为主服务器不可用,这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一 定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover[故障转移]操作。 切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为 客观下线。
测试
我们目前的状态是 一主二从
1、配置哨兵配置文件 sentinel.conf
# sentinel monitor 被监控的名称 host port 1 sentinel monitor myredis 127.0.0.1 6379 1 #后面的这个数字1,代表主机挂了,从机投票看谁来成为主机,票数最多的,就成为主机。
2、启动哨兵
$ redis-sentinel /usr/local/etc/sentinel.conf
现在模拟主机断开了,这个时候随机选择一个服务器(这里面有一个投票算法)
127.0.0.1:6379> studown (error) ERR unknown command `studown`, with args beginning with: 127.0.0.1:6379> SHUTDOWN
他会重启主服务器
如果重启主服务器都失败
他就开始进行投票
127.0.0.1:6380> info replication # Replication role:master connected_slaves:1 slave0:ip=127.0.0.1,port=6381,state=online,offset=112772,lag=1 master_replid:fd4cd644ae11205c32770da238b13ec211ae7d54 master_replid2:572e4186cf866fb888e76ff7c2b736cc3e2622f0
如果主机此时回来了,只能归并到新的主机下,当做从机,这就是哨兵模式的规则!
127.0.0.1:6380> info replication # Replication role:master connected_slaves:2 slave0:ip=127.0.0.1,port=6381,state=online,offset=122847,lag=0 slave1:ip=127.0.0.1,port=6379,state=online,offset=122847,lag=0 master_replid:fd4cd644ae11205c32770da238b13ec211ae7d54 master_replid2:572e4186cf866fb888e76ff7c2b736cc3e2622f0
哨兵模式优点:
哨兵模式缺点:
哨兵模式的全部配置
# Example sentinel.conf # 哨兵sentinel实例运行的端口 默认26379 port 26379 # 哨兵sentinel的工作目录 dir /tmp #哨兵sentinel监控的redis主节点的 ip port # master-name 可以自己命名的主节点名字 只能由字母A-z、数字0-9 、 这三个字符".-_"组成。 # quorum 配置多少个sentinel哨兵统一认为master主节点失联 那么 这时客观上认为主节点失联了 sentinel monitor mymaster 127.0.0.1 6379 2 # 当在Redis实例中开启了requirepass foobared 授权密码 这样所有连接Redis实例的 客户端都要提供 密码 # 设置哨兵sentinel 连接主从的密码 注意必须为主从设置一样的验证密码 sentinel auth-pass mymaster MySUPER--secret-0123passw0rd # 指定多少毫秒之后 主节点没有应答哨兵sentinel 此时 哨兵主观上认为主节点下线 默认30秒 sentinel down-after-milliseconds mymaster 30000 # 这个配置项指定了在发生failover主备切换时最多可以有多少个slave同时对新的master 进行 同步, 这个数字越小,完成failover所需的时间就越长, 但是如果这个数字越大, 就意味着越 多的slave因为replication而不可用。 可以通过将这个值设为 1 来保证每次只有一个slave 处于不能处理命令请求的状态。 sentinel parallel-syncs mymaster 1 # 故障转移的超时时间 failover-timeout 可以用在以下这些方面: #1. 同一个sentinel对同一个master两次failover之间的间隔时间。 #2. 当一个slave从一个错误的master那里同步数据开始计算时间。直到slave被纠正为向 正确的master那 里同步数据时。 #3.当想要取消一个正在进行的failover所需要的时间。 #4.当进行failover时,配置所有slaves指向新的master所需的最大时间。不过,即使过了 这个超时, slaves依然会被正确配置为指向master,但是就不按parallel-syncs所配置的 规则来了 # 默认三分钟 sentinel failover-timeout mymaster 180000 # SCRIPTS EXECUTION #配置当某一事件发生时所需要执行的脚本,可以通过脚本来通知管理员, 例如当系统运行不正常时发邮件通知 相关人员。 #对于脚本的运行结果有以下规则: #若脚本执行后返回1,那么该脚本稍后将会被再次执行,重复次数目前默认为10 #若脚本执行后返回2,或者比2更高的一个返回值,脚本将不会重复执行。 #如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为1时的行为相同。 #一个脚本的最大执行时间为60s,如果超过这个时间,脚本将会被一个SIGKILL信号终止,之 后重新执行。 #通知型脚本:当sentinel有任何警告级别的事件发生时(比如说redis实例的主观失效和客观 失效等等), 将会去调用这个脚本,这时这个脚本应该通过邮件,SMS等方式去通知系统管理 员关于系统不正常运行的信 息。调用该脚本时,将传给脚本两个参数,一个是事件的类型, 一个是事件的描述。如果sentinel.conf配 置文件中配置了这个脚本路径,那么必须保证这 个脚本存在于这个路径,并且是可执行的,否则sentinel无 法正常启动成功。 #通知脚本 # shell编程 sentinel notification-script mymaster /var/redis/notify.sh # 客户端重新配置主节点参数脚本 # 当一个master由于failover而发生改变时,这个脚本将会被调用,通知相关的客户端 关于master地址已 经发生改变的信息。 # 以下参数将会在调用脚本时传给脚本: # # 目前总是“failover”, # 是“leader”或者“observer”中的一个。 # 参数 from-ip, from-port, to-ip, to-port是用来和旧的master和新的master(即旧 的slave)通 信的 # 这个脚本应该是通用的,能被多次调用,不是针对性的。 sentinel client-reconfig-script mymaster /var/redis/reconfig.sh # 一般都是由运维来配置!
Redis缓存的使用,极大的提升了应用程序的性能和效率,特别是数据查询方面。但同时,它也带来了一 些问题。其中,最要害的问题,就是数据的一致性问题,从严格意义上讲,这个问题无解。如果对数据 的一致性要求很高,那么就不能使用缓存。
另外的一些典型问题就是,缓存穿透、缓存雪崩和缓存击穿。目前,业界也都有比较流行的解决方案。
概念
缓存穿透的概念很简单,用户想要查询一个数据,发现redis内存数据库没有,也就是缓存没有命中,于 是向持久层数据库查询。发现也没有,于是本次查询失败。当用户很多的时候,缓存都没有命中(秒 杀!),于是都去请求了持久层数据库。这会给持久层数据库造成很大的压力,这时候就相当于出现了 缓存穿透。
布隆过滤器
布隆过滤器是一种数据结构,对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则 丢弃,从而避免了对底层存储系统的查询压力;
缓存空对象
当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数 据将会从缓存中获取,保护了后端数据源;
但是这种方法会存在两个问题:
1、如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多 的空值的键;
2、即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于 需要保持一致性的业务会有影响。
概述
这里需要注意和缓存击穿的区别,缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中 对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一 个屏障上凿开了一个洞。
当某个key在过期的瞬间,有大量的请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访 问数据库来查询最新数据,并且回写缓存,会导使数据库瞬间压力过大。
解决方案
设置热点数据永不过期
从缓存层面来看,没有设置过期时间,所以不会出现热点 key 过期后产生的问题。
加互斥锁
分布式锁:使用分布式锁,保证对于每个key同时只有一个线程去查询后端服务,其他线程没有获得分布 式锁的权限,因此只需要等待即可。这种方式将高并发的压力转移到了分布式锁,因此对分布式锁的考验很大。
缓存雪崩,是指在某一个时间段,缓存集中过期失效。Redis 宕机!
产生雪崩的原因之一,比如在写本文的时候,马上就要到双十二零点,很快就会迎来一波抢购,这波商品时间比较集中的放入了缓存,假设缓存一个小时。那么到了凌晨一点钟的时候,这批商品的缓存就都过期了。而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。
其实集中过期,倒不是非常致命,比较致命的缓存雪崩,是缓存服务器某个节点宕机或断网。因为自然 形成的缓存雪崩,一定是在某个时间段集中创建缓存,这个时候,数据库也是可以顶住压力的。无非就 是对数据库产生周期性的压力而已。而缓存服务节点的宕机,对数据库服务器造成的压力是不可预知 的,很有可能瞬间就把数据库压垮。
双十一:停掉一些服务,(保证主要的服务可用)
redis高可用
这个思想的含义是,既然redis有可能挂掉,那我多增设几台redis,这样一台挂掉之后其他的还可以继续 工作,其实就是搭建的集群。(异地多活!)
限流降级(在SpringCloud讲解过!)
这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对 某个key只允许一个线程查询数据和写缓存,其他线程等待。
数据预热
数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数 据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让 缓存失效的时间点尽量均匀。