90年代,一个基本的网站访问量一般不会太大,单个数据库完全足够!
那个时候,更多的去使用静态页面Html-服务器根本没有太大的压力!
整个网站的瓶颈是什么?
1.数据量如果太大,一个机器放不下了!
2.数据的索引(B+Tree),一个机器内存也放不下
3.访问量(读写混合),一个服务器承受不了
2.Memcached(缓存)+Mysql+垂直拆分(读写分离)
网站80%的情况都是在读,每次要去查询数据库的话就十分麻烦,所以我们希望减轻数据的压力,哦我们可以使用缓存来保证效率。
发展过程:优化数据结构和索引–>文件缓存(io)–>Memcached(当时最热门的技术!)
3.分库分表 + 水平拆分(集群)
技术和业务在发展的同时,对人的要求也越来越高了
本质:数据库(读,写)
早些年MyISAM:表锁(查询的时候锁住一张表)
转战Innodb:行锁
4.如今的年代
2010–2020,在这之间发生了的翻天覆地的变化,定位也是一种数据
mysql等关系数据库就不够用了,数据量很大,变化很快
MySQL有的人使用他来存储一些较大的文件,博客、图片!数据库很大,如果一种数据库有专门的数据库来处理这种数据,MySQL压力就变得十分小。
为什么要用NoSQL!
用户的个人信息,社交网络,地理位置,用户自己产生的数据,用户的日志等等爆发式增长!
这个时候我们就需要使用NoSQL数据库,NoSQL可以很好的处理以上的内容!
NoSQL
NoSQL = Not Only SQL,不仅仅是SQL
泛指非关系型数据库,随着web2.0互联网的诞生,传统的关系型数据库很难对付web2.0时代!尤其是超大规模的高并发的社区!暴露出许多难以克服的问题,NoSQL在当今大数据环境下发展的十分迅速,redis是发展最快的,而且是我们当下必须掌握的一个技术。
很多的数据类型用户的个人信息,社交网络,地理位置。这些数据类型的存储不需要一个固定的格式,不需要多余的操作就可以横向扩展的!map
NoSQL特点
解耦!
传统的RDBMS
-结构化组织
- SQL
- 数据和关系都存在单独的表中
- 数据操作,数据定义语言
- 严格的一致性
- 基础的事务
-...
NoSQL
- 不仅仅是数据
- 没有固定的查询语言
- 键值对储存,列存储,文档存储,图形数据库(社交关系)
- 最终一致性
- CAP定理和BASE(异地多活),
高性能,高可用,高可扩展性
-...
了解 : 3V和3高
大数据时代的3V:主要是描述问题的
大数据时代的3高:主要是解决问题的
真正的在公司中实践:NoSQL + RDBMS一起使用才是最强的,阿里巴巴的架构演进。
#1.商品的基本信息
名称、价格、商家信息:
关系型数据库就可以解决,MySQL / Oracle
#2.商品的描述、评论
文档型数据库,MongoDB
#3.图片
分布式文件系统 FastDFS
-淘宝自己的 TFS
-Google的 GFS
-Hadoop HDFS
-阿里云的 OSS
#4.商品的关键字(搜索)
-搜索引擎 solr elasticserach
-Iserach :阿里巴巴的
#5.商品热门的波段信息
-内存数据库
-Redis Tair、Memcache...
#6.商品的交易、外部的支付接口
-第三方应用
bson格式 和 json 一样
MongoDB(一般必须掌握)
ConthDB
不是存图形的,放的是关系,比如:朋友圈社交网络,广告推荐!
分类 | Examples举例 | 典型应用场景 | 数据模型 | 优点 | 缺点 |
---|---|---|---|---|---|
键值对(key-value) | Tokyo Cabinet/Tyrant, Redis, Voldemort, Oracle BDB | 内容缓存,主要用于处理大量数据的高访问负载,也用于一些日志系统等等。 | Key 指向 Value 的键值对,通常用hash table来实现 | 查找速度快 | 数据无结构化,通常只被当作字符串或者二进制数据 |
列存储数据库 | Cassandra, HBase, Riak | 分布式的文件系统 | 以列簇式存储,将同一列数据存在一起 | 查找速度快,可扩展性强,更容易进行分布式扩展 | 功能相对局限 |
文档型数据库 | CouchDB, MongoDb | Web应用(与Key-Value类似,Value是结构化的,不同的是数据库能够了解Value的内容) | Key-Value对应的键值对,Value为结构化数据 | 数据结构要求不严格,表结构可变,不需要像关系型数据库一样需要预先定义表结构 | 查询性能不高,而且缺乏统一的查询语法。 |
图形(Graph)数据库 | Neo4J, InfoGrid, Infinite Graph | 社交网络,推荐系统等。专注于构建关系图谱 | 图结构 | 利用图结构相关算法。比如最短路径寻址,N度关系查找等 | 很多时候需要对整个图做计算才能得出需要的信息,而且这种结构不太好做分布式的集群 |
Redis是什么
Redis(Remote Dictionary Server ),即远程字典服务
是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。
redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。
免费和开源!是当下最热门的NoSQL技术之一,也被人们称为结构化数据库
Redis能干嘛
特性
学习中需要用到的东西
window下的redis停更很久了
1.下载安装包
2.下载完毕得到压缩包
4.开启Redis,双击运行服务即可(redis-server.exe)
5.使用Redis客户端来连接Redis(redis-cli.exe)
window下使用确实简单,但是Redis推荐使用Linux来开发!
1.下载安装包
2.解压安装包
[root@izuf6afv1lgs65fa0h4egvz opt]# tar -zxvf redis-6.0.8.tar.gz
3.进入解压后的文件
[root@izuf6afv1lgs65fa0h4egvz opt]# cd redis-6.0.8
[root@izuf6afv1lgs65fa0h4egvz redis-6.0.8]# ls
00-RELEASENOTES INSTALL runtest src
BUGS Makefile runtest-cluster tests
CONTRIBUTING MANIFESTO runtest-moduleapi TLS.md
COPYING README.md runtest-sentinel utils
deps redis.conf sentinel.conf
4.基本的环境安装
【注意】这里如果redis的版本是6.0以上的,make报错:需要升级gcc
sudo yum install centos-release-scl
sudo yum install devtoolset-7-gcc*
scl enable devtoolset-7 bash
yum install gcc-c++
make #配置所有需要的文件
5.redis默认的安装路径 /usr/local/bin
6.将redis配置文件,复制到当前目录下。
[root@izuf6afv1lgs65fa0h4egvz bin]# cp /opt/redis-5.0.9/redis.conf myredisconf/ 复制文件
[root@izuf6afv1lgs65fa0h4egvz bin]# cd myredisconf/
[root@izuf6afv1lgs65fa0h4egvz myredisconf]# ls
redis.conf #复制过来的文件
10.查看redis的进程是否开启
11.如何关闭redis服务呢 shutdown
127.0.0.1:6379> shutdown
not connected> exit
[root@izuf6afv1lgs65fa0h4egvz bin]#
12.再次查看进程是否存在
redis-benchmark是一个压力测试工具
官方自带的性能测试工具
简单的测试:
#测试100个并发连接 100000请求
redis-benchmark -h localhost -p 6379 -c 100 -n 100000
#性能测试 主机名 端口号 并发连接数 请求数
默认使用的是第0个
127.0.0.1:6379> select 3 #切换数据库
OK
127.0.0.1:6379[3]> DBSIZE #查看DB大小
(integer) 0
127.0.0.1:6379> keys * #查看数据库所有的key
1) "mylist"
2) "counter:__rand_int__"
3) "myset:__rand_int__"
4) "key:__rand_int__"
127.0.0.1:6379>
清除当前数据库 flushdb
127.0.0.1:6379[3]> flushdb #清空数据库
OK
127.0.0.1:6379[3]> key *
(empty list or set)
清空所有数据库 flushall
为什么redis默认端口是6379?(了解)
6379在是手机按键上MERZ对应的号码,而MERZ取自意大利歌女Alessia Merz的名字。MERZ长期以来被antirez及其朋友当作愚蠢的代名词。Redis作者antirez同学在twitter上说将在下一篇博文中向大家解释为什么他选择6379作为默认端口号。而现在这篇博文出炉,在解释了Redis的LRU机制之后,向大家解释了采用6379作为默认端口的原因。
redis是单线程的
明白redis是很快的,官方表示,redis是基于内存操作的,CPU不是redis性能瓶颈,redis的瓶颈是根据机器的内存和网络带宽,既然可以使用单线程来实现,就是用了单线程!
redis使用C语言写的,官方提供的数据为100000+的QPS,不比Memcache差
redis为什么单线程这么快?
1.误区1:高性能的服务器一定是多线程的
2.误区2:多线程(CPU会上下文切换)一定比单线程效率高
先去CPU、内存、硬盘的速度要有所了解:CPU>内存>硬盘
核心:redis是将所有的数据全部放在内存中的,所以使用单线程操作,效率高。
多线程(CPU上下文切换,是耗时的操作),对于内存来说,没有切换效率就是最高的,多次读写都是在一个CPU上的,在内存的情况下,这是最佳的。
全段翻译:
Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets)与范围查询, bitmaps, hyperloglogs和 地理空间(geospatial)索引半径查询。 Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)
127.0.0.1:6379>keys * # 查看所有的key
127.0.0.1:6379>exists name # 判断这个key是否存在
127.0.0.1:6379>move name 1 # 移除当前key
127.0.0.1:6379>expire name 10 # 设置过期时间 s
127.0.0.1:6379>ttl name # 查看当前key的剩余时间
127.0.0.1:6379> type name # 查看当前key的类型
string
127.0.0.1:6379> set key1 kv1 #设置值
OK
127.0.0.1:6379> get key1 #获得值
"kv1"
127.0.0.1:6379> keys * #获得所有的key
1) "key1"
127.0.0.1:6379> exists key1 #判断一个key是否存在
(integer) 1
127.0.0.1:6379> append key1 hello #追加字符串,如果当前字符串不存在,就相当于set key
(integer) 8
127.0.0.1:6379> get key1
"kv1hello"
127.0.0.1:6379> strlen key1 #获取字符串的长度
(integer) 8
##########################################################
#自增1 i++
#步 长 i+=
127.0.0.1:6379> set views 0 #初始浏览量为 0
OK
127.0.0.1:6379> incr views #自增 1
(integer) 1
127.0.0.1:6379> get views
"1"
127.0.0.1:6379> incr views
(integer) 2
127.0.0.1:6379> get views
"2"
127.0.0.1:6379> decr views #自减 1
(integer) 1
127.0.0.1:6379> get views
"1"
127.0.0.1:6379> incrby views 10 #可以设置步长,指定增量
(integer) 11
127.0.0.1:6379> get views
"11"
127.0.0.1:6379> decrby views 5
(integer) 6
127.0.0.1:6379> get views
"6"
###############################################################
获取字符串范围:getrange
127.0.0.1:6379> set k1 hello,redis #设置k1的值
OK
127.0.0.1:6379> get k1
"hello,redis"
127.0.0.1:6379> getrange k1 0 3 #截取字符串,相当于substring();
"hell"
127.0.0.1:6379> getrange k1 0 -1 #获取全部的字符串
"hello,redis"
替换
127.0.0.1:6379> set k2 hello,world
OK
127.0.0.1:6379> get k2
"hello,world"
127.0.0.1:6379> setrange k2 6 redis #替换指定位置开始的字符串
(integer) 11
127.0.0.1:6379> get k2
"hello,redis"
################################################################
setex(set with expire) #设置过期时间
setnx(set if not exist) #不存在设置(在分布式锁中会常常使用)
127.0.0.1:6379> setex k3 30 hello #设置k3的值,30s后过期
OK
127.0.0.1:6379> get k3
"hello"
127.0.0.1:6379> ttl k3
(integer) 22
127.0.0.1:6379> setnx mykey redis #如果mykey不存在,创建mykey
(integer) 1
127.0.0.1:6379> keys *
1) "k2"
2) "mykey"
3) "k1"
127.0.0.1:6379> ttl k3 #查看过期时间,-2为已过期
(integer) -2
127.0.0.1:6379> setnx mykey hello,redis #如果mykey,创建失败
(integer) 0
127.0.0.1:6379> get mykey
"redis"
127.0.0.1:6379>
##############################################################
mset
mget
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3 #同时设置多个值
OK
127.0.0.1:6379> keys *
1) "k3"
2) "k2"
3) "k1"
127.0.0.1:6379> mget k1 k2 k3 #同时获取多个值
1) "v1"
2) "v2"
3) "v3"
127.0.0.1:6379> msetnx k1 v1 k4 v4 #msetnx,是一个原子性的操作,要么一起成功,要么一起失败
(integer) 0
127.0.0.1:6379> get k4
(nil)
#对象
set user:1{name:zhangsan,age:3} #设置一个user对象,值为json字符串,来保存一个对象
这里的key是一个巧妙的设计,
127.0.0.1:6379> mset user:1:name zhangsan user:1:age 2
OK
127.0.0.1:6379> mget user:1:name user:1:age
1) "zhangsan"
2) "2"
##############################################################
getset #先get后set
127.0.0.1:6379> getset db redis #如果不存在这个key,则返回nil,但是会设置这个key的值
(nil)
127.0.0.1:6379> get db
"redis"
127.0.0.1:6379> getset db hello #如果存在值,获取原来的值,并设置新的值
"redis"
127.0.0.1:6379> get db
"hello"
string类似的使用场景,value除了是我们的字符串,还可以是我们的数字
基本的数据类型,列表
在redis中,可以把list当成栈、队列、阻塞队列
127.0.0.1:6379> lpush list one #将一个值或者多个值,插入列表头部
(integer) 1
127.0.0.1:6379> lpush list two
(integer) 2
127.0.0.1:6379> lpush list three
(integer) 3
127.0.0.1:6379> lrange list 0 -1 #获取list中的值
1) "three"
2) "two"
3) "one"
127.0.0.1:6379> lrange list 0 2 #通过区间获取具体的值
1) "three"
2) "two"
3) "one"
127.0.0.1:6379> rpush list four #将一个值或者多个值,插入列表尾部
(integer) 4
127.0.0.1:6379> lrange list 0 3
1) "three"
2) "two"
3) "one"
4) "four"
###############################################################################
127.0.0.1:6379> clear
127.0.0.1:6379> lrange list 0 2
1) "three"
2) "two"
3) "one"
127.0.0.1:6379> lpop list #移除List第一个元素
"three"
127.0.0.1:6379> rpop list #移除list最后一个元素
"four"
127.0.0.1:6379> lrange list 0 -1
1) "two"
2) "one"
#################################################################################
lindex
127.0.0.1:6379> lindex list 1 #通过下标获取list中的某一个值
"one"
127.0.0.1:6379> lindex list 0
"two"
###################################################################################
Llen
127.0.0.1:6379> llen list #返回列表的长度
(integer) 3
###################################################################################
移除指定的值
lrem
127.0.0.1:6379> lpush list two
(integer) 4
127.0.0.1:6379> lrem list 1 one #移除list集合中指定个数的value,精确匹配
(integer) 1
127.0.0.1:6379> lrange list 0 -1
1) "two"
2) "two"
3) "one"
127.0.0.1:6379> lrem list 2 two
(integer) 2
127.0.0.1:6379> lrange list 0 -1
1) "one"
##################################################################################
ltrim 修剪
127.0.0.1:6379> rpush key hello redis hello zhangsan
(integer) 4
127.0.0.1:6379> lrange key 0 -1
1) "hello"
2) "redis"
3) "hello"
4) "zhangsan"
127.0.0.1:6379> ltrim key 0 1 #通过下标截取指定的长度
OK
127.0.0.1:6379> lrange key 0 -1
1) "hello"
2) "redis"
###################################################################################
rpoplpush #移除列表的最后一个元素
127.0.0.1:6379> rpush mylist hello1 hello2 hello3
(integer) 3
127.0.0.1:6379> rpoplpush mylist myotherlist #将最后一个元素移动到新的列表中
"hello3"
127.0.0.1:6379> lrange mylist 0 -1
1) "hello1"
2) "hello2"
127.0.0.1:6379> lrange myotherlist 0 -1
1) "hello3"
####################################################################################
exist
lset #将列表中指定下标的值替换为另一个值,更新操作
127.0.0.1:6379> exists list #判断这个列表是否存在
(integer) 0
127.0.0.1:6379> lpush list value
(integer) 1
127.0.0.1:6379> lrange list 0 0
1) "value"
127.0.0.1:6379> lset list 0 item #将列表中指定下标的值替换为另一个值,更新操作
OK
127.0.0.1:6379> lrange list 0 0
1) "item"
###################################################################################
linsert #将某个具体的value插入到列表中某个元素的前面和后面
127.0.0.1:6379> rpush list hello
(integer) 1
127.0.0.1:6379> rpush list redis
(integer) 2
127.0.0.1:6379> linsert list before redis world #将元素插入到元素的前面
(integer) 3
127.0.0.1:6379> lrange list 0 -1
1) "hello"
2) "world"
3) "redis"
127.0.0.1:6379> linsert list after redis haha #将元素插入到元素的后面
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "hello"
2) "world"
3) "redis"
4) "haha"
小结
消息队列(Lpush Rpop) 栈(Lpush Lpop)
set中的值是不能重复的
127.0.0.1:6379> sadd myset hello #set集合中添加元素
(integer) 1
127.0.0.1:6379> sadd myset redis
(integer) 1
127.0.0.1:6379> sadd myset "hello world"
(integer) 1
127.0.0.1:6379> smembers myset #查看集合中的元素
1) "hello world"
2) "redis"
3) "hello"
127.0.0.1:6379> sismember myset hello #判断元素是否存在
(integer) 1
127.0.0.1:6379> sismember myset world
(integer) 0
###################################################################################
127.0.0.1:6379> scard myset #获取set集合中的内容元素个数
(integer) 3
###################################################################################
srem
127.0.0.1:6379> srem myset hello #移除set集合中的指定元素
(integer) 1
127.0.0.1:6379> smembers myset
1) "hello world"
2) "redis"
##################################################################################
set 无需不重复集合
127.0.0.1:6379> SMEMBERS myset
1) "zhangsan"
2) "member"
3) "hello world"
4) "redis"
127.0.0.1:6379> srandmember myset #随机抽取一个元素
"redis"
127.0.0.1:6379> srandmember myset 2 #随机抽取指定元素的个数
1) "member"
2) "hello world"
127.0.0.1:6379> srandmember myset 2
1) "redis"
2) "hello world"
##################################################################################
删除key
127.0.0.1:6379> smembers myset
1) "zhangsan"
2) "member"
3) "hello world"
4) "redis"
127.0.0.1:6379> spop myset #随机删除一个key
"member"
127.0.0.1:6379> spop myset
"zhangsan"
127.0.0.1:6379> smembers myset
1) "hello world"
2) "redis"
##################################################################################
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 redis
(integer) 1
127.0.0.1:6379> smove myset myset2 world #将一个指定的值,移动到另一个set集合中
(integer) 1
127.0.0.1:6379> smembers myset
1) "redis"
2) "hello"
127.0.0.1:6379> smembers myset2
1) "world"
##################################################################################
127.0.0.1:6379> sadd key1 a b c
(integer) 3
127.0.0.1:6379> sadd key2 c d e
(integer) 3
127.0.0.1:6379> sdiff key1 key2 #差集,key1 - key2
1) "b"
2) "a"
127.0.0.1:6379> sinter key1 key2 #交集,查看共同之处(共同好友就是这样实现的)
1) "c"
127.0.0.1:6379> sunion key1 key2 #并集,
1) "a"
2) "c"
3) "b"
4) "e"
5) "d"
微博,A用户所有关注的人放在一个set集合中,将他的粉丝也放在一个集合中
共同关注,共同爱好
Map集合,key-Map集合。本质和string类型没有太大的区别
127.0.0.1:6379> hset myhash field zhangsan #set一个具体的key-value
(integer) 1
127.0.0.1:6379> hget myhash field #获取一个字段值
"zhangsan"
127.0.0.1:6379> hmset myhash field hello field2 redis #set多个key-value
OK
127.0.0.1:6379> hmget myhash field field2 #获取多个字段值
1) "hello"
2) "redis"
127.0.0.1:6379> hgetall myhash #获取全部的数据
1) "field" # k
2) "hello" # value
3) "redis"
4) "world"
5) "field2"
6) "redis"
##################################################################################
127.0.0.1:6379> hdel myhash redis #删除指定的key字段,对应的value也被删除
(integer) 1
127.0.0.1:6379> hgetall myhash
1) "field"
2) "hello"
3) "field2"
4) "redis"
##################################################################################
hlen #获取长度
127.0.0.1:6379> hlen myhash #获取hash的字段数据
(integer) 2
##################################################################################
hexists
127.0.0.1:6379> hexists myhash field #判断hash中指定字段是否存在
(integer) 1
127.0.0.1:6379> hexists myhash field3
(integer) 0
##################################################################################
#只获取所有的key
#只获取所有的value
127.0.0.1:6379> hkeys myhash #只获取所有的key
1) "field"
2) "field2"
127.0.0.1:6379> hvals myhash #只获取所有的value
1) "hello"
2) "redis"
##################################################################################
127.0.0.1:6379> hset myhash field 5
(integer) 0
127.0.0.1:6379> hincrby myhash field 1 #指定增量
(integer) 6
127.0.0.1:6379> hincrby myhash field -1
(integer) 5
127.0.0.1:6379> hsetnx myhash field3 hello #如果不存在则可以设置
(integer) 1
127.0.0.1:6379> hsetnx myhash field3 redis #如果存在则不能设置
(integer) 0
hash的应用:
在set的基础上,增加了一个值,set k1 v1 zset k2 v2
127.0.0.1:6379> zadd myset 1 one #添加值
(integer) 1
127.0.0.1:6379> zadd myset 2 two 3 three #添加多个值
(integer) 1
127.0.0.1:6379> zrange myset 0 -1 #获取值
1) "one"
2) "two"
3) "three"
####################################################################################
127.0.0.1:6379> zadd salary 500 zhangsan 2000 wangsi #添加三个用户
(integer) 2
127.0.0.1:6379> zadd salary 5000 xuexintai
(integer) 1
127.0.0.1:6379> zrangebyscore salary -inf +inf #按照从小到大排列 -inf 负无穷 +inf 正无穷
1) "zhangsan"
2) "wangsi"
3) "xuexintai"
127.0.0.1:6379> zrangebyscore salary -inf +inf withscores #按照从小到大排列,并且附带成绩
1) "zhangsan"
2) "500"
3) "wangsi"
4) "2000"
5) "xuexintai"
6) "5000"
127.0.0.1:6379> zrangebyscore salary -inf 2500 withscores #工资小于2500的升序排序
1) "zhangsan"
2) "500"
3) "wangsi"
4) "2000"
127.0.0.1:6379> zrevrange salary 0 -1 #降序排序,从大到小进行排序
1) "xuexintai"
2) "zhangsan"
127.0.0.1:6379> zrevrange salary 0 -1 withscores #
1) "xuexintai"
2) "5000"
3) "zhangsan"
4) "500"
####################################################################################
移除 zrem
127.0.0.1:6379> zrange salary 0 -1
1) "zhangsan"
2) "wangsi"
3) "xuexintai"
127.0.0.1:6379> zrem salary wangsi #移除有序集合中的指定元素
(integer) 1
127.0.0.1:6379> zcard salary #获取指定集合中的个数
(integer) 2
127.0.0.1:6379> zcount salary 0 5000 #获取指定区间的成员数量
(integer) 2
案例思路:
set:排序、存储班级成绩表、工资排序表
排行榜应用实现,取top 10
朋友的定位,附近的人,打车距离计算
redis的Geo在redis3.2版本就推出了,这个功能可以推算地理位置的信息,两地之间的距离,方圆几里的人
geoadd
#geoadd 添加地理位置
#规则:南极和北极无法直接添加,一般会下载城市数据,直接通过java程序一次性导入
#有效的经度从-180度到180度
#有效的纬度从-85.05112878到85.05112878度
#参数:key 值
127.0.0.1:6379> geoadd china:city 116.40 39.90 beijing
(integer) 1
127.0.0.1:6379> geoadd china:city 121.47 31.23 shanghai
(integer) 1
127.0.0.1:6379> geoadd china:city 106.50 29.53 chongqing 114.05 22.52 shenzhen
(integer) 2
127.0.0.1:6379> geoadd china:city 120.16 30.24 hangzhou 108.96 34.26 xian
(integer) 2
geopos
获取指定的城市的经度和纬度
127.0.0.1:6379> geopos china:city beijing #获取指定的城市的经度和纬度
1) 1) "116.39999896287918091"
2) "39.90000009167092543"
127.0.0.1:6379> geopos china:city beijing chongqing
1) 1) "116.39999896287918091"
2) "39.90000009167092543"
2) 1) "106.49999767541885376"
2) "29.52999957900659211"
geodist
两人之间的距离
单位:
127.0.0.1:6379> geodist china:city beijing shanghai #查看上海到北京的直线距离 单位 m
"1067378.7564"
127.0.0.1:6379> geodist china:city beijing shanghai km #查看上海到北京的直线距离 单位 km
"1067.3788"
georadius 以给定发经纬度为中心,找出某一个半径内的元素
附近的人(获得所有的附近的人的地址,定位)通过半径来查询
127.0.0.1:6379> georadius china:city 110 30 1000 km #以100,30这个经纬度为中心,寻找方圆1000km内的城市
1) "chongqing"
2) "xian"
3) "shenzhen"
4) "hangzhou"
127.0.0.1:6379> georadius china:city 110 30 500 km
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 withcoord count 1 #筛选出指定的结果
1) 1) "chongqing"
2) 1) "106.49999767541885376"
2) "29.52999957900659211"
127.0.0.1:6379> georadius china:city 110 30 500 km withcoord count 2
1) 1) "chongqing"
2) 1) "106.49999767541885376"
2) "29.52999957900659211"
2) 1) "xian"
2) 1) "108.96000176668167114"
2) "34.25999964418929977"
georadiusbymember
127.0.0.1:6379> georadiusbymember china:city beijing 1000 km #找出指定元素周围的其它元素
1) "beijing"
2) "xian"
127.0.0.1:6379> georadiusbymember china:city shanghai 400 km
1) "hangzhou"
2) "shanghai"
geohash 返回一个或多个位置元素的geohash表示
该命令返回11个字符的geohash字符串
#将二维的经纬度转换为一维的字符串,如果两个字符串越接近,那么则距离越近
127.0.0.1:6379> geohash china:city beijing chongqing #代表当前城市经纬度的hash码
1) "wx4fbxxfke0"
2) "wm5xzrybty0"
geo底层的实现原理,其实就是zset,可以使用zset操作geo
127.0.0.1:6379> zrange china:city 0 -1 #查看所有的元素
1) "chongqing"
2) "xian"
3) "shenzhen"
4) "hangzhou"
5) "shanghai"
6) "beijing"
127.0.0.1:6379> zrem china:city xian #移除指定元素
(integer) 1
127.0.0.1:6379> zrange china:city 0 -1
1) "chongqing"
2) "shenzhen"
3) "hangzhou"
4) "shanghai"
5) "beijing"
什么是基数
是指一个集合中不同元素的个数 ,例如:A{1,3,5,7,8,7}
基数(不重复的元素) = 5
简介
redis2.8.9版本就更新了HyperLogLog数据结构
redis HyperLogLog基数统计的算法
优点:占用的内存是固定的,z^64不同元素的技术,只需要费12KB的内存,如果要从内存的角度来比较的话,HyperLogLog首选!
网页的UV(一个人访问一个网页多次,还是算作一个人)
传统的方式,set保存用户的id,然后就可以统计set中的元素作为标准判断!,这个方式如果保存大量的用户id,就会比较麻烦,我们的目的是计数,不是保存用户id。
0.81%错误率,统计UV任务,可以忽略不计的
测试使用
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 a s f f s g j #创建第二组元素 mykey2
(integer) 1
127.0.0.1:6379> pfcount mykey2 #统计mykey2元素基数数量
(integer) 5
127.0.0.1:6379> pfmerge mykey3 mykey mykey2 #合并两组mykey mykey2 ==> mykey3 并集
OK
127.0.0.1:6379> pfcount mykey3 #看并集的基数个数
(integer) 11
如果允许容错,那么一定可以使用Hyperloglog
如果不允许容错,就使用set或者自己的数据结构
位存储
统计用户信息,活跃,不活跃!登录,未登录!
bitmap位图,数据结构!都是操作二进制来进行记录,就只有0和1两个状态。
测试
#使用bitmap记录周一到周日的打卡
周一:1 周二:0 周三:0
127.0.0.1:6379> setbit sign 0 1 #周一
(integer) 0
127.0.0.1:6379> setbit sign 1 0 #周二
(integer) 0
127.0.0.1:6379> setbit sign 2 0 #周三
(integer) 0
127.0.0.1:6379> setbit sign 3 0 #周四
(integer) 0
127.0.0.1:6379> setbit sign 4 1 #周五
(integer) 0
127.0.0.1:6379> setbit sign 5 1 #周六
(integer) 0
127.0.0.1:6379> setbit sign 6 0 #周日
(integer) 0
查看某一天是否有打卡
127.0.0.1:6379> getbit sign 3
(integer) 0
127.0.0.1:6379> getbit sign 5
(integer) 1
统计操作:统计打卡的天数
127.0.0.1:6379> bitcount sign #统计这周的打卡记录
(integer) 3
本质:一组命令的集合,一个事务的所有命令都会被序列化,在事务执行过程中,会按照顺序执行!
------队列set set set 执行------
redis事务没有隔离级别的概念
所有的命令在事务中,并没有直接被执行,只有发起执行命令的时候才会执行
redis单条命令是保证原子性的,但是事务不保证原子性的
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 k3 v3
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> getset k2 #错误的命令
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> exec #执行事务出错
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k2 #所有的命令都不会被执行
(nil)
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"
监控 watch 面试常问
悲观锁:
乐观锁:
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
测试多线程修改值之后,监视失败,相当于redis的乐观锁操作
#客户端一
127.0.0.1:6379> watch money #监视 money
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decrby money 10
QUEUED
127.0.0.1:6379> incrby out 10
QUEUED
127.0.0.1:6379> exec #执行之前另一个线程,修改了我们的值,这个时候,就会导致事务执行失败
(nil)
127.0.0.1:6379> unwatch #发现事务执行失败,先解锁
OK
127.0.0.1:6379> watch money #再次监视,获取最新的值
OK
127.0.0.1:6379> multi #开启事务
OK
127.0.0.1:6379> decrby money 10
QUEUED
127.0.0.1:6379> incrby out 10
QUEUED
127.0.0.1:6379> exec #执行事务
1) (integer) 990
2) (integer) 30
#客户端二
127.0.0.1:6379> get money
"80"
127.0.0.1:6379> set money 1000
OK
使用java操作redis
什么是jedis
是redis推荐的java连接工具,使用Java操作redis中间件。
测试
1.导入对应的依赖
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
<version>3.2.0version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.62version>
dependency>
2.启动redis服务器
3.编码测试
package cn.xue;
import redis.clients.jedis.Jedis;
public class TestPing {
public static void main(String[] args) {
//1.new Jedis对象
Jedis jedis = new Jedis("127.0.0.1",6379);
//jedis所有的命令就是之前我们学习的指令
//所以之前的指令学习很重要,在这里都是方法
String ping = jedis.ping();
System.out.println(ping);
}
}
输出
package cn.xue;
import redis.clients.jedis.Jedis;
import java.sql.Time;
import java.util.concurrent.TimeUnit;
public class TestString {
public static void main(String[] args) {
Jedis jedis = new Jedis("127.0.0.1", 6379);
System.out.println(jedis.set("key1","v1"));
System.out.println(jedis.set("key2","v2"));
System.out.println(jedis.set("key3","v3"));
System.out.println("删除一个key"+jedis.del("key2"));
System.out.println("获取key2"+jedis.get("key2"));
System.out.println("在key3后面加入值"+jedis.append("key3","End"));
System.out.println("增加多个键值对"+jedis.mset("key4","v4","key5","v5"));
System.out.println("获取多个键值对"+jedis.mget("key3","key4"));
System.out.println("删除多个key"+jedis.del("key4","key5"));
System.out.println("=========新增键值对防止覆盖原值==========");
System.out.println(jedis.setnx("key1","zhangsan")); //如果存在key1就返回0,不存在就创建key1
System.out.println(jedis.setnx("key2","value2"));
System.out.println(jedis.setnx("key2","value-new"));
System.out.println("==========设置键值对过期时间============");
System.out.println(jedis.setex("key4",3,"zhangsan")); //设置key4三秒后过期
System.out.println(jedis.get("key4"));
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(jedis.get("key4"));
System.out.println("获取一定长度的字符串"+jedis.getrange("key2",2,3));
}
}
package cn.xue;
import redis.clients.jedis.Jedis;
public class TestList {
public static void main(String[] args) {
Jedis jedis = new Jedis("127.0.0.1", 6379);
System.out.println("===========添加一个list===============");
jedis.lpush("collections","ArraryList","Vector","HashMap","WeakHashMap","LinkMap");
jedis.lpush("collections","HashSet");
jedis.lpush("collections","TreeSet");
jedis.lpush("collections","TreeMap");
System.out.println("获取collections中的全部内容"+jedis.lrange("collections",0,-1));
System.out.println("获取collections区间0-3的元素"+jedis.lrange("collections",0,3));
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("从右端添加元素"+jedis.rpush("collections","HashMap"));
System.out.println("获取collections中的全部内容"+jedis.lrange("collections",0,-1));
System.out.println("collections列表出栈(右端)"+jedis.rpop("collections"));
System.out.println("获取collections中的全部内容"+jedis.lrange("collections",0,-1));
System.out.println("修改collections指定下标的内容"+jedis.lset("collections",1,"ArrayList"));
System.out.println("获取collections中的全部内容"+jedis.lrange("collections",0,-1));
System.out.println("获取collections的长度"+jedis.llen("collections"));
System.out.println("获取 collections指定下标的内容"+jedis.lindex("collections",2));
System.out.println("-------------------------------------------");
jedis.lpush("sortList","3","2","8","4","5");
System.out.println("排序前:"+jedis.lrange("sortList",0,-1));
System.out.println(jedis.sort("sortList")); //排序
System.out.println("排序后:"+jedis.lrange("sortList",0,-1));
}
}
package cn.xue;
import redis.clients.jedis.Jedis;
public class TestSet {
public static void main(String[] args) {
Jedis jedis = new Jedis("127.0.0.1", 6379);
// jedis.flushDB();
System.out.println("向集合中添加元素(不重复)");
System.out.println(jedis.sadd("eleSet","e1","e2","e3","e4","e8","e5","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("删除一个元素"+jedis.srem("eleSet","e6"));
System.out.println("eleSet所有元素为:"+jedis.smembers("eleSet"));
System.out.println("删除两个元素"+jedis.srem("eleSet","e7","e8"));
System.out.println("eleSet所有元素为:"+jedis.smembers("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("e5是否是eleSet中的元素:"+jedis.sismember("eleSet","e5"));
System.out.println("-----------------------------");
System.out.println(jedis.sadd("eleSet1","e3","e2","e1","e4","e6","e5","e7","e8"));
System.out.println(jedis.sadd("eleSet2","e1","e0","e4","e5","e2"));
System.out.println("将eleSet1中的元素e1删除并存入eleSet3中"+jedis.smove("eleSet1","eleSet3","e1"));
System.out.println("将eleSet1中的元素e2删除并存入eleSet3中"+jedis.smove("eleSet1","eleSet3","e2"));
System.out.println("eleSet1中的元素:"+jedis.smembers("eleSet1"));
System.out.println("eleSet1中的元素:"+jedis.smembers("eleSet3"));
System.out.println("------------------集合运算-------------------");
System.out.println("eleSet1h和eleSet2的交集:"+jedis.sinter("eleSet1","eleSet2"));
System.out.println("eleSet1h和eleSet2的并集:"+jedis.sunion("eleSet1","eleSet2"));
System.out.println("eleSet1h和eleSet2的差集:"+jedis.sdiff("eleSet1","eleSet2"));
jedis.sinterstore("eleSet4","eleSet1","eleSet2"); //将eleSet1h和eleSet2的交集存到eleSet4中
System.out.println("eleSet4的元素"+jedis.smembers("eleSet4"));
}
}
package cn.xue;
import com.alibaba.fastjson.support.hsf.HSFJSONUtils;
import redis.clients.jedis.Jedis;
import java.util.HashMap;
import java.util.Map;
public class TestHash {
public static void main(String[] args) {
Jedis jedis = new Jedis("127.0.0.1", 6379);
jedis.flushDB();
Map<String,String> map = new HashMap();
map.put("key1","value1");
map.put("key2","value2");
map.put("key4","value3");
map.put("key4","value4");
jedis.hmset("hash",map); //添加名称为hash的hash元素
//向名称为hash的hash中添加key为key5,value为value5元素
jedis.hset("hash","key5","value5");
System.out.println("散列hash的所有键值对为:"+jedis.hgetAll("hash"));
System.out.println("散列hash的所有的key为:"+jedis.hkeys("hash"));
System.out.println("散列hash的所有的值为"+jedis.hvals("hash"));
System.out.println("将key6保存的值加上一个整数,如果key6不存在则创建key6"+jedis.hincrBy("hash","key6",1));
System.out.println("散列hash的所有键值对为:"+jedis.hgetAll("hash"));
System.out.println("将key6保存的值加上一个整数,如果key6不存在则创建key6"+jedis.hincrBy("hash","key6",1));
System.out.println("散列hash的所有键值对为:"+jedis.hgetAll("hash"));
System.out.println("删除一个或多个键值对"+jedis.hdel("hash","key3","key5"));
System.out.println("散列hash键值对的个数:"+jedis.hlen("hash"));
System.out.println("判断hash中是否存在key3:"+jedis.hexists("hash","key3"));
System.out.println("判断hash中是否存在key4:"+jedis.hexists("hash","key4"));
System.out.println("获取hash中的值"+jedis.hmget("hash","key4"));
System.out.println("获取hash中的值"+jedis.hmget("hash","key6"));
}
}
所有的API命令,就是我们上面学习的指令,一点也没变
事务
package cn.xue;
import com.alibaba.fastjson.JSONObject;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
public class TestTX {
public static void main(String[] args) {
Jedis jedis = new Jedis("127.0.0.1", 6379);
JSONObject json = new JSONObject();
json.put("hello","world");
json.put("name","zhangsan");
//开启事务
Transaction multi = jedis.multi();
String result = json.toJSONString();
try{
multi.set("user1",result);
multi.set("user2",result);
multi.exec(); //执行事务
}catch (Exception e){
multi.discard(); //放弃事务
e.printStackTrace();
}finally {
System.out.println(jedis.get("user1"));
System.out.println(jedis.get("user2"));
jedis.close(); //关闭连接
}
}
}
SpringBoot操作数据:spring-data jpa mongdb redis
SpringData也是和Spring Boot齐名的项目。
说明:在Spring Boot2.X之后,原来使用的jedis被替换为了lettuce
jedis: 采用的直连,多个线程操作的话,是不安全的,如果想要避免不安全,使用jedis pool连接池,BIO
lettuce: 采用netty,实例可以在多个线程中进行共享,不存在线程 不安全的情况,可以减少线程数据了,更像NIO模式
源码分析:
@Bean
@ConditionalOnMissingBean(name = {"redisTemplate"}) //我们可以定义一个redisTemplate来替换这个
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
//默认的RedisTemplate没有过多的设置,redis都是需要序列化的
//两个泛型都是object,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.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
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(); 操作字符串,类似string
//opsForList 操作list,类似list
//opsForSet
//opsForHash
//opsForZSet
//opsForGeo
//opsForHyperLogLog
//除了基本的操作,我们常用的方法都可以直接通过redisTemplate操作,比如事务和基本的CRUD
//获取连接对象
/* RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
connection.flushDb();
connection.flushAll();*/
redisTemplate.opsForValue().set("key1","zhangsan");
System.out.println(redisTemplate.opsForValue().get("key1"));
}
}
@Configuration
public class RedisConfig {
//固定模板,拿来即用
//自己定义一个redisTemplate
@Bean
@ConditionalOnMissingBean(name = {"redisTemplate"})
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
//我们为了自己开发方便,一般直接使用
RedisTemplate<String, Object> template = new RedisTemplate();
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;
}
}
package cn.xue.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.CollectionUtils;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
public class RedisUtil {
@Autowired
private RedisTemplate<String, Object> template;
// =============================common============================
/**
* 指定缓存失效时间
*
* @param key 键
* @param time 时间(秒)
* @return
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
template.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 template.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return template.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) {
template.delete(key[0]);
} else {
template.delete(CollectionUtils.arrayToList(key));
}
}
}
// ============================String=============================
/**
* 普通缓存获取
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : template.opsForValue().get(key);
}
/**
* 普通缓存放入
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
template.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) {
template.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)
* @return
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return template.opsForValue().increment(key, delta);
}
/**
* 递减
* @param key 键
* @param delta 要减少几(小于0)
* @return
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return template.opsForValue().increment(key, -delta);
}
// ================================Map=================================
/**
* HashGet
* @param key 键 不能为null
* @param item 项 不能为null
* @return 值
*/
public Object hget(String key, String item) {
return template.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
* @param key 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return template.opsForHash().entries(key);
}
/**
* HashSet
* @param key 键
* @param map 对应多个键值
* @return true 成功 false 失败
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
template.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 {
template.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 {
template.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 {
template.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) {
template.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
return template.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
* @return
*/
public double hincr(String key, String item, double by) {
return template.opsForHash().increment(key, item, by);
}
/**
* hash递减
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
* @return
*/
public double hdecr(String key, String item, double by) {
return template.opsForHash().increment(key, item, -by);
}
// ============================set=============================
/**
* 根据key获取Set中的所有值
* @param key 键
* @return
*/
public Set<Object> sGet(String key) {
try {
return template.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 template.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 template.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 = template.opsForSet().add(key, values);
if (time > 0)
expire(key, time);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取set缓存的长度
* @param key 键
* @return
*/
public long sGetSetSize(String key) {
try {
return template.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 = template.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
// ===============================list=================================
/**
* 获取list缓存的内容
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
* @return
*/
public List<Object> lGet(String key, long start, long end) {
try {
return template.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取list缓存的长度
* @param key 键
* @return
*/
public long lGetListSize(String key) {
try {
return template.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 通过索引 获取list中的值
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
* @return
*/
public Object lGetIndex(String key, long index) {
try {
return template.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, Object value) {
try {
template.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, Object value, long time) {
try {
template.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 {
template.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 {
template.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 {
template.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 = template.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
}
启动的时候,就通过配置文件来启动!
单位
1.配置文件,uints单位对大小写不敏感。
包含
就像spring配置文件中的import、include
网络
bind 127.0.0.1 #绑定的ip
protected-mode yes #保护模式
port 6379 #端口设置
通用 GENERAL
daemonize yes #以守护进程的方式运行,默认是no,我们需要自己开启为yes!
pidfile /var/run/redis_6379.pid #如果以后台的方式运行,我们就需要指定一个pid文件
#日志
#日志生成的文件名 logfile ""
databases 16 #默认的数据库数量,默认是16各个数据库
always-show-logo yes #是否总是显示LOGO
快照
持久化,在规定时间内,执行了多少次操作,则会之持久化到文件.rdb。
redis是内存数据库,如果没有持久化,那么数据断电即失
#如果900s内,如果至少有一个1 key进行了修改,我们就进行持久化操作。
save 900 1
#如果300s内,如果至少有一个10 key进行了修改,我们就进行持久化操作
save 300 10
#如果60s内,如果至少有一个10000 key进行了修改,我们就进行持久化操作
save 60 10000
#我们之后学习持久化,会自己定义这个测试!
stop-writes-on-bgsave-error yes #持久化如果出错,时候还需要继续工作
rdbcompression yes #是否压缩rdb文件,需要消耗一些cpu资源
rdbchecksum yes #保存rdb文件的时候,进行错误的检查校验
dir ./ #rdb文件保存的目录
REPLICATION 复制,主从复制的时候用的,后面再聊
SECURITY 安全,
可以在这里设置密码,默认是没有密码的
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> config set requirepass "123456" #设置redis的密码
OK
127.0.0.1:6379> ping
(error) NOAUTH Authentication required.
127.0.0.1:6379> auth 123456 #使用密码进行登录
OK
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> config get requirepass #获取redis的密码
1) "requirepass"
2) "123456"
显示CLIENTS
maxclients 10000 #设置能连上redis的最大客户端数量
MEMORY MANAGEMENT
maxmemory <bytes> #redis配置最大的内存容量,默认是字节
maxmemory-policy noeviction #内存到达上限之后的处理策略,
#volatile-lru:只对设置了过期时间的key进行LRU(默认值)
#allkeys-lru : 删除lru算法的key
#volatile-random:随机删除即将过期key
#allkeys-random:随机删除
#volatile-ttl : 删除即将过期的
#noeviction : 永不过期,返回错误
APPEND ONLY MODE 模式 aof配置
appendonly no #默认是不开启aof模式的,默认是使用rdb方式持久化的,再大部分的情况下,rdb完全够用!
appendfilename "appendonly.aof" #持久化文件的名字
# appendfsync always #每次修改都会写入 sync,消耗性能
appendfsync everysec #每秒执行一次 sync ,可能会丢失这1S的数据
# appendfsync no #不执行 sync,这个时候操作系统自己同步数据,速度最快
Redis是内存数据库,如果不将内存中的数据库状态保存到磁盘,那么一旦服务器进程退出,服务器中的数据状态也会消失,所以redis提供了持久化功能!
什么是RDB
在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里。
Redis会单独创建( fork )一个子进程来进行持久化 ,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何I0操作的。这就确保了极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。我们默认的就是RDB ,一般情况下不需要修改这个配置!
有时候在生产环境,会将这个文件备份。
rdb保存的文件是 dump.rdp,都是在配置文件的快照中进行配置的
触发机制
1.save的规则满足的情况下,会自动触发rdb规则
2.执行flushdb命令,也会触发rdb规则
3.退出redis,也会触发rdb文件
备份就会自动生成dump.rdb
恢复rdb文件
1.只需要将rdb文件放在redis启动目录就可以,redis启动的时候会自动检查dump.rdb并恢复其中的数据
2.查看需要存在的位置
127.0.0.1:6379> config get dir
1) "dir"
2) "/usr/local/bin" #如果这个目录下存在dump.rdb文件,启动就会自动恢复其中的数据
几乎就是redis自己默认的配置就够用了。
优点:
1.适合大规模的数据恢复!dump.rdb
2.如果对数据的完整性要求不高!
缺点:
1.需要一定的时间间隔进行操作!如果redis意外宕机了,这个最后一次修改数据就没了
2.fork进程的时候,会占用一定的内存空间
将我们的所有命令记录下来,history,恢复的时候就把这个文件全部执行一遍。
是什么
以日志的形式来记录每个写操作,将redis执行过程的所有指令记录下来(读操作不记录),只许追加文件的不可以改写文件,redis启动之处会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件将写指令从前到后执行一次完成数据的恢复工作。
AOF保存的是appendonly.aof文件
append
默认是不开启的,需要手动开启。我们只需要将appendonly改为yes就开启了AOF
重启redis就可以生效了
如果这个AOF文件有错位,这个时候redis是启动不起来的,我们需要修复这个AOF文件
redis给我们提供了一个工具==redis-check-aof --fix appendonly.aof
==
如果文件正常了,重启就可以生效了。
优点和缺点
优点:
1.每一次修改都同步,文件的完整性会更加好。
2.每秒同步一次,可能会丢失一秒的数据
3.从不同不,效率是最高的
缺点:
1.相对于数据文件来说,AOF远远大于RDB,修复的速度也比RDB慢。
2.AOF运行效率也比RDB慢,所以我们redis默认的配置就是RDB持久化。
1、RDB 持久化方式能够在指定的时间间隔内对你的数据进行快照存储
2、AOF持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据, AOF命令以Redis协议追加保存每次写的操作到文件末尾, Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大。
3、只做缓存,如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化
4、同时开启两种持久化方式
在这种情况下,当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整。
RDB的数据不实时,同时使用两者时服务器重启也只会找AOF文件,那要不要只使用AOF呢?作者建议不要,因为RDB更适合用于备份数据库( AOF在不断变化不好备份) , 快速重启,而且不会有AOF可能潜在的Bug ,留着作为一个万一的手段。
5、性能建议
因为RDB文件只用作后备用途,建议只在Slave_上持久化RDB文件,而且只要15分钟备份一 次就够了,只保留save 9001这条规则。
如果Enable AOF ,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单只load自己的AOF文件就可以了,代价一是带来 了持续的I0 ,二是AOF rewrite的最后将rewrite过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的。只要硬盘许可,应该尽量减少AOF rewrite的频率, AOF重写的基础大小默认值64M太小了,可以设到5G以上,默认超过原大小100%大小重写可以改到适当的数值。
如果不Enable AOF ,仅靠Master-Slave ReplIcation实现高可用性也可以,能省掉一大笔I0 ,也减少了rewrite时带来的系统波动。代价是如果Master/Slave同时倒掉,会丢失十几分钟的数据,启动脚本也要比较两个Master/Slave中的RDB文件,载入较新的那个,微博就是这种架构。
Redis 发布订阅 (pub/sub) 是一种消息通信模式:发送者 (pub) 发送消息,订阅者 (sub) 接收消息。
Redis 客户端可以订阅任意数量的频道。
订阅/发布消息图:
下图展示了频道 channel1 , 以及订阅这个频道的三个客户端 —— client2 、 client5 和 client1 之间的关系:
当有新消息通过 PUBLISH 命令发送给频道 channel1 时, 这个消息就会被发送给订阅它的三个客户端
命令
测试
订阅端
127.0.0.1:6379> subscribe xue
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "xue"
3) (integer) 1 #等待推送的信息
1) "message" #消息
2) "xue" #哪个频道的消息
3) "zhangsan" #具体内容
1) "message" #消息
2) "xue" #哪个频道的消息
3) "hello" #具体内容
发送端
127.0.0.1:6379> publish xue zhangsan #发布者发布消息到频道
(integer) 1
127.0.0.1:6379> publish xue hello #发布者发布消息到频道
(integer) 1
127.0.0.1:6379>
原理
Redis是使用C实现的,通过分析Redis源码里的pubsub.c文件,了解发布和订阅机制的底层实现,籍此加深对Redis 的理解。
Redis通过PUBLISH、SUBSCRIBE 和PSUBSCRIBE等命令实现发布和订阅功能。
通过SUBSCRIBE命令订阅某频道后, redis-server里维护了一个字典,字典的键就是一个个频道,而字典的值则是一个链表,链表中保存了所有订阅这个channel的客户端。SUBSCRIBE命令的关键,就是将客户端添加到给定channel的订阅链表中。
通过PUBLISH命令向订阅者发送消息, redis-server 会使用给定的频道作为键,在它所维护的channel字典中查找记录了订阅这个频道的所有客户端的链表,遍历这个链表,将消息发布给所有订阅者。
Pub/Sub从字面上理解就是发布( Publish )与订阅( Subscribe ) , 在Redis中,你可以设定对某一个key值进行消息发布及消息订阅,当一个key值上进行了消息发布后,所有订阅它的客户端都会收到相应的消息。这一功能最明显的用法就是用作实时消息系统,比如普通的即时聊天,群聊等功能。
使用场景:
稍微复杂的场景就会使用 消息中间件MQ。
主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master/leader),后者称为从节点(slave/follower) ;数据的复制是单向的,只能由主节点到从节点。Master以写为主, Slave以读为主。
默认情况下,每台Redis服务器都是主节点;且一个主节点可以有多个从节点(或没有从节点) ,但一个从节点只能有一个主节点。主从复制的作用主要包括:
1、数据冗余: 主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
2、故障恢复: 当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。
3、负载均衡: 在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点) , 分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
4、高可用基石: 除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。
一般来说,要将Redis运用于工程项目中,只使用一台Redis是万万不能的,原因如下:
1、从结构上,单个Redis服务器会发生单点故障,并且-台服务器需要处理所有的请求负载,压力较大;
2、从容量上,单个Redis服务器内存容量有限,就算一台Redis服务器内存 容量为256G ,也不能将所有内存用作Redis存储内存,一般来说 ,单台Redis最大使用内存不应该超过20G。
电商网站上的商品,一般都是一次上传,无数次浏览的,说专业点也就是"多读少写"。
对于这种场景,我们可以使如下这种架构:
主从复制,读写分离,80%的情况下都是在进行读操作,减缓服务器的压力,架构中经常使用,一主二从。
只要在公司中,主从复制就是必须要使用的,在真实的项目中不可能单机使用redis
只配置从库,不用配置主库。redis默认自己就是主库。
127.0.0.1:6379> info replication #查看当前库的信息
# Replication
role:master #角色master
connected_slaves:0 # 0 没有从机
master_replid:fafc634cc5c138b62b041048c496cb4cb07242bb
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
复制3个配置文件,然后修改对应的信息
修改完毕之后启动,三个redis服务,可以通过进程信息查看。
默认情况下,每台Redis服务器都是主节点,一般情况下只需要配置从机就可以。
一主(6379)二从(80、81)
127.0.0.1:6380> SLAVEOF 127.0.0.1 6379 #SLAVEOF host 6379 找主机的ip和地址
OK
127.0.0.1:6380> info replication #
# Replication
role:slave #当前角色是从机
master_host:127.0.0.1 #可以看到主机的信息
master_port:6379
master_link_status:up
master_last_io_seconds_ago:4
master_sync_in_progress:0
slave_repl_offset:0
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:5ccd75aa4194181c7a11e3c7bd4104383938f216
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:0
#在主机中查看
role:master
connected_slaves:1 #多了从机的配置
slave0:ip=127.0.0.1,port=6380,state=online,offset=28,lag=1 #从机的信息
master_replid:5ccd75aa4194181c7a11e3c7bd4104383938f216
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:28
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:28
如果两个都配置完了,会有两个从机的信息。
真实的从主配置应该在配置文件中,配置,这样的话是永久的,这里使用的是命令配置,是暂时的。
配置主机的ip和主机的端口号,这样这台redsi启动就自动是从机。
细节
主机可以写,从机不能写只能读!主机中的所有信息和数据,都会自动被从机所保存。
主机写:
从机只能读取内内容,不能写。
测试:主机断开了,从机依旧可以连接到主机,但是没有写操作,但是如果主机回来了,依旧可以读取到主机写的信息。
如果是使用命令行来配置的从机,这个时候如果重启了,就会变回主机。只要变回从机,立马就会从主机中获取值。
复制原理
Slave启动成功连接到master后会发送一个sync(同步)命令
Master接到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后, master将传送整个数据文件到slave ,并完成一次完全同步。
全量复制: 而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中。
增量复制: Master继续将新的所有收集到的修改命令依次传给slave ,完成同步
但是只要是重新连接master , 一次完全同步(全量复制)将被自动执行,我们的数据一定可以在从机中看到。
层层链路
上一个M连接下一个S,也可以完成主从复制。
如果第一个节点崩了,后面的也就不能使用了。
如果主机断开了连接,可以使用slaveof no one
让自己变成主机,其它的节点就可以手动连接到最新的这个主节点。
(自动选举老大的模式)
概述
主从切换技术的方法是: 当主服务器宕机后, 需要手动把一台从服务器切换为主服务器, 这就需要人工干预, 费事费力, 还会造成一段时间内服务不可用。这不是一种推荐的方式, 更多时候, 我们优先考虑哨兵模式。Redis从2.8开始正式提供 了Sentinel (哨兵)架构来解决这个问题。
谋朝篡位的自动版, 能够后台监控主机是否故障, 如果故障了根据投票数自动将从库转换为主库。
哨兵模式是一种特殊的模式 , 首先Redis提供了哨兵的命令, 哨兵是一个独立的进程 , 作为进程,它会独立运行。其原理是哨兵通过发送命令, 等待Redis服务器响应, 从而监控运行的多个Redis实例。
这里的哨兵有两个作用
然而,一个哨兵进程对Redis服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。
假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认为服务器不可用,这个现象称为主观下线,当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果有一个哨兵发起,进行failover[故障转移]操作,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。
测试
我们目前的状态时一主二从。
1.配置哨兵配置文件sentinel.conf
#sentinel monitor 被监控的名称 host port 1
sentinel monitor myredis 127.0.0.1 6379 1
后面的数字1,代表主机挂了,slave投票让谁接替主机,票数最多的就会成为主机
2.启动哨兵
[root@izuf6afv1lgs65fa0h4egvz bin]# redis-sentinel myredisconf/sentinel.conf
2106:X 11 Oct 2020 17:20:06.291 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
2106:X 11 Oct 2020 17:20:06.291 # Redis version=5.0.9, bits=64, commit=00000000, modified=0, pid=2106, just started
2106:X 11 Oct 2020 17:20:06.291 # Configuration loaded
_._
_.-``__ ''-._
_.-`` `. `_. ''-._ Redis 5.0.9 (00000000/0) 64 bit
.-`` .-```. ```\/ _.,_ ''-._
( ' , .-` | `, ) Running in sentinel mode
|`-._`-...-` __...-.``-._|'` _.-'| Port: 26379
| `-._ `._ / _.-' | PID: 2106
`-._ `-._ `-./ _.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' | http://redis.io
`-._ `-._`-.__.-'_.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' |
`-._ `-._`-.__.-'_.-' _.-'
`-._ `-.__.-' _.-'
`-._ _.-'
`-.__.-'
2106:X 11 Oct 2020 17:20:06.292 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
2106:X 11 Oct 2020 17:20:06.294 # Sentinel ID is 2f0d796534c47319a92d9f62b7e372be856fe3b2
2106:X 11 Oct 2020 17:20:06.294 # +monitor master myredis 127.0.0.1 6379 quorum 1
2106:X 11 Oct 2020 17:20:06.294 * +slave slave 127.0.0.1:6380 127.0.0.1 6380 @ myredis 127.0.0.1 6379
2106:X 11 Oct 2020 17:20:06.296 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ myredis 127.0.0.1 6379
如果Master节点断开了,这个时候就会从 从机中随机选择一个服务器(这里有一个投票算法)。
我这个就随机成6380成为主机。
如果主机此时重启了,只能归并到新的主机下, 当作从机,这就是哨兵模式的规则。
哨兵模式
优点:
缺点:
哨兵模式的全部配置
# Example sentinel.conf
# 哨兵sentinel实例运行的端口 默认26379
port 26379
# 哨兵sentinel的工作目录
dir /tmp
# 哨兵sentinel监控的redis主节点的 ip port
# master-name 可以自己命名的主节点名字 只能由字母A-z、数字0-9 、这三个字符".-_"组成。
# quorum 当这些quorum个数sentinel哨兵认为master主节点失联 那么这时 客观上认为主节点失联了
# sentinel monitor
sentinel monitor mymaster 127.0.0.1 6379 1
# 当在Redis实例中开启了requirepass foobared 授权密码 这样所有连接Redis实例的客户端都要提供密码
# 设置哨兵sentinel 连接主从的密码 注意必须为主从设置一样的验证密码
# sentinel auth-pass
sentinel auth-pass mymaster MySUPER--secret-0123passw0rd
# 指定多少毫秒之后 主节点没有应答哨兵sentinel 此时 哨兵主观上认为主节点下线 默认30秒
# sentinel down-after-milliseconds
sentinel down-after-milliseconds mymaster 30000
# 这个配置项指定了在发生failover主备切换时最多可以有多少个slave同时对新的master进行 同步,
这个数字越小,完成failover所需的时间就越长,
但是如果这个数字越大,就意味着越 多的slave因为replication而不可用。
可以通过将这个值设为 1 来保证每次只有一个slave 处于不能处理命令请求的状态。
# sentinel parallel-syncs
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
sentinel failover-timeout mymaster 180000
# SCRIPTS EXECUTION
#配置当某一事件发生时所需要执行的脚本,可以通过脚本来通知管理员,例如当系统运行不正常时发邮件通知相关人员。
#对于脚本的运行结果有以下规则:
#若脚本执行后返回1,那么该脚本稍后将会被再次执行,重复次数目前默认为10
#若脚本执行后返回2,或者比2更高的一个返回值,脚本将不会重复执行。
#如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为1时的行为相同。
#一个脚本的最大执行时间为60s,如果超过这个时间,脚本将会被一个SIGKILL信号终止,之后重新执行。
#通知型脚本:当sentinel有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等),将会去调用这个脚本,
#这时这个脚本应该通过邮件,SMS等方式去通知系统管理员关于系统不正常运行的信息。调用该脚本时,将传给脚本两个参数,
#一个是事件的类型,
#一个是事件的描述。
#如果sentinel.conf配置文件中配置了这个脚本路径,那么必须保证这个脚本存在于这个路径,并且是可执行的,否则sentinel无法正常启动成功。
#通知脚本
# sentinel notification-script
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
sentinel client-reconfig-script mymaster /var/redis/reconfig.sh #一般都是由运维来配置
Redis缓存的使用,极大的提升了应用程序的性能和效率,特别是数据查询方面。但同时,它也带来了一些问题。其中,最要害的问题,就是数据的一致性问题,从严格意义上讲,这个问题无解。如果对数据的一致性要求很高,那么就不能使用缓存。
另外的一些典型问题就是,缓存穿透、缓存雪崩和缓存击穿。目前,业界都有比较流行的解决方案。
概念
在默认情况下,用户请求数据时,会先在缓存(Redis)中查找,若没找到即缓存未命中,再在数据库中进行查找,数量少可能问题不大,可是一旦大量的请求数据(例如秒杀场景)缓存都没有命中的话,就会全部转移到数据库上,造成数据库极大的压力,就有可能导致数据库崩溃。网络安全中也有人恶意使用这种手段进行攻击被称为洪水攻击。
解决方案
布隆过滤器
对所有可能查询的参数以Hash的形式存储,以便快速确定是否存在这个值,在控制层先进行拦截校验,校验不通过直接打回,减轻了存储系统的压力。
缓存空对象
当存储层不命中后,即使返回的空对象也将其缓存起来,同时设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了后端数据源
但是这种方法会村存在两个问题:
概念
相较于缓存穿透,缓存击穿的目的性更强,一个存在的key,在缓存过期的一刻,同时有大量的请求,这些请求都会击穿到DB,造成瞬时DB请求量大、压力骤增。这就是缓存被击穿,只是针对其中某个key的缓存不可用而导致击穿,但是其他的key依然可以使用缓存响应。
比如热搜排行上,一个热点新闻被同时大量访问就可能导致缓存击穿。
解决方案
设置热点数据永不过期
这样就不会出现热点数据过期的情况,但是当Redis内存空间满的时候也会清理部分数据,而且此种方案会占用空间,一旦热点数据多了起来,就会占用部分空间。
加互斥锁(分布式锁)
在访问key之前,采用SETNX(set if not exists)来设置另一个短期key来锁住当前key的访问,访问结束再删除该短期key。保证同时刻只有一个线程访问。这样对锁的要求就十分高。
概念
大量的key设置了相同的过期时间,导致在缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩。
产生雪崩的原因之一,比如马上就要到双十一零点,会快就会又一波抢购,这波商品时间比较集中的放入了缓存,假设缓存时间为一个小时,那么到了凌晨一点钟的时候 ,这批商品的缓存就都过期了。而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。
解决方案
redis高可用
这个思想的含义是,既然redis有可能挂掉,那我多增设几台redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。
限流降级
这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
数据预热
数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。