1、单机MySQL的年代
2、Memcached(缓存)+MySQL+垂直拆分(读写分离)
网站80%的情况都是在读,每次都要去查询数据库的话十分麻烦!所以说我们希望减轻数据的压力,我们可以用缓存来保证效率!
发展过程:优化数据结构和索引–>文件索引(IO)–>Memcached(当时最热门的技术)
3、分库分表+水平拆分+MySQL集群
本质:数据库(读,写)
早些年MyISAM:表锁,十分影响效率!高并发下就会出现严重的锁问题
转战Innodb:行锁
慢慢的就开始使用分库分表来解决写的压力
4、当今年代
MySQL等关系型数据库就不够用了!数据量大!
目前一个基本的互联网项目
为什么要用NoSQL!
用户的个人信息,社交网络,地理位置,用户自己产生的数据,用户日志等等爆发时增长!
这时候我们就需要使用NoSQL数据库,NoSQL可以很好的处理以上的情况!
NoSQL
泛指非关系型数据库,随着web2.0互联网的诞生!传统的关系型数据库很难对付web2.0时代!尤其是超大规模的高并发的社区!NoSQL在当今大数据环境下发展十分迅速,Redis是发展最快的,而且是我们当下必须要掌握的一个技术!
很多的数据类型用户的个人信息,社交网络,地理位置,这些数据类型的存储不需要一个固定的格式!
NoSQL特点
1、方便扩展(数据之间没有关系,很好扩展!)
2、大数据量高性能(Redis 一秒写8万次,读取11万)
3、数据类型是多样型的(不需要事先设计数据库!随取随用!)
4、传统的RDBMS和NoSQL
传统的RDBMS
-结构化组织
-SQL
-数据和关系都存在单独的表中
-操作,数据定义语言
-严格的一致性
-基础的事务
-...
NoSQL
-不仅仅是数据
-没有固定的查询语言
-键值对存储,列存储,文档存储,图形数据库(社交关系)
-最终一致性
-CAP定理和BASE
-高性能,高可用,高可扩展
-...
KV键值对:
新浪:Redis
美团:Redis + Tair
阿里、百度:Redis + memecache
文档型数据库(bson格式和json一样):
MongoDB(一般必须要掌握)
MongoDB是一个基于分布式文件存储的数据库,C++编写,主要用来处理大量的文档!MongeDB是一个介于关系型数据库和非关系型数据库中中间的产品!MongeDB是非关系型数据库中功能最丰富,最像关系型数据库的!
ConthDB
列存储数据库
图形关系型数据库
四大分类比较
Redis是什么?
Redis(Remote Dictionary Server ),即远程字典服务!
是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API
免费和开源!是当下最热门的NoSQL技术之一!也被人们称为结构化数据库
Redis能干嘛
1、内存存储、持久化,内存中是断电即失,所以说持久化很重要
2、效率高,可以用于高速缓存
3、发布订阅系统
4、地图信息分析
5、计时器
6、…
特性
1、多样的数据类型
2、持久化
3、集群
4、事务
…
1、下载安装包:https://github.com/microsoftarchive/redis/releases/tag/win-3.2.100
2、解压到自己电脑上的环境目录
3、开启Redis,双击运行服务即可
4、使用redis客户端来来连接redis
1、下载安装包
2、解压Redis的安装包
3、进入解压后的文件,可以看到我们redis的配置文件
4、基本的环境安装
yum install gcc-c++
make
make install
5、redis的默认安装路径 usr/local/bin
6、将redis配置文件。复制到我们当前目录下
7、redis默认不是后台启动的,修改配置文件
8、启动redis服务
9、使用redis-cli进行连接测试
11、如何关闭redis服务
12、再次查看进程是否存在
redis-benchmark 是一个压力测试工具
官方自带的性能测试工具
redis-benchmark命令参数
我们来简单测试一下
#测试:100个并发连接 100000万个请求
redis-benchmark -h localhost -p 6379 -c 100 -n 100000
默认使用的是第0个
可以使用select进行切换
127.0.0.1:6379> select 3 #切换数据库
OK
127.0.0.1:6379[3]> dbsize #查看db大小
(integer) 0
127.0.0.1:6379[3]> set name codesheep
OK
127.0.0.1:6379[3]> DBSIZE
(integer) 1
127.0.0.1:6379[3]> keys * #查看数据库所有的key
1) "name"
清空当前数据库
127.0.0.1:6379[3]> flushdb
OK
127.0.0.1:6379[3]> DBSIZE
(integer) 0
清空所有数据库
127.0.0.1:6379[3]> flushall
OK
Redis是单线程的!
Redis是很快的,官方表示,Redis是基于内存操作,CPU不是Redis的性能瓶颈,Redis的瓶颈是根据机器的内存和网络带宽,既然可以使用单线程来实现,就使用单线程。
redis为什么单线程还这么快
redis是将所有的数据全部放在内存中的,所以说使用单线程去操作效率就是最高的,多次读写都是在一个CPU上,在内存情况下,这个就是最佳的方案。
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> clear
127.0.0.1:6379> set name codesheep
OK
127.0.0.1:6379> set age 2
OK
127.0.0.1:6379> EXISTS name
(integer) 1
127.0.0.1:6379> EXPIRE name 10 #设置key的过期时间
(integer) 1
127.0.0.1:6379> ttl name #查看当前key的剩余时间
(integer) -2
127.0.0.1:6379> get name
(nil)
127.0.0.1:6379> type name #查看当前key的类型
string
127.0.0.1:6379> type age
string
127.0.0.1:6379> set key1 v1 #设置值
OK
127.0.0.1:6379> get key1 #获得值
"v1"
127.0.0.1:6379> keys *
1) "key1"
127.0.0.1:6379> EXISTS key1
(integer) 1
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> set views 0
OK
127.0.0.1:6379> get views
"0"
127.0.0.1:6379> incr views #自增1
(integer) 1
127.0.0.1:6379> incr views
(integer) 2
127.0.0.1:6379> get views
"2"
127.0.0.1:6379> decr views #自减1
(integer) 1
127.0.0.1:6379> get views
"1"
127.0.0.1:6379> INCRBY views 10 #可以设置步长,指定增量
(integer) 11
127.0.0.1:6379> DECRBY viers 5
(integer) -5
127.0.0.1:6379> DECRBY views 5
(integer) 6
127.0.0.1:6379>
#######################################################
#字符串范围
127.0.0.1:6379> set key1 "hello"
OK
127.0.0.1:6379> GETRANGE key1 0 3 #截取字符串[0,3]
"hell"
127.0.0.1:6379> GETRANGE key1 0 -1 #获取全部的字符串
"hello"
#####################################################
#替换
127.0.0.1:6379> set key2 abcdefg
OK
127.0.0.1:6379> get key
(nil)
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(set with expire) #设置过期时间
# setnx(set if not exist) #不存在再设置
127.0.0.1:6379> setex key3 30 "hello"
OK
127.0.0.1:6379> ttl key3
(integer) 24
127.0.0.1:6379> get key3
"hello"
127.0.0.1:6379> setnx mykey "redis"
(integer) 1
127.0.0.1:6379> ttl key3
(integer) -2
########################################################
#mset
#mget
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3 #批量设置值
OK
127.0.0.1:6379> keys *
1) "k2"
2) "k3"
3) "k1"
127.0.0.1:6379> mget k1 k2 #批量获取值
1) "v1"
2) "v2"
#对象
set user:1{name:zhangsan,age:2} #设置一个user:1对象 值为json字符来保存一个对象!
#这里的key是一个巧妙的设计 user:{id}:{filed}
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 #如果不存在值,则返回null
(nil)
127.0.0.1:6379> getset db redis
"redis"
127.0.0.1:6379> getset db mongodb #如果存在值,先获取原来的值,并设置新的值
"redis"
127.0.0.1:6379> get db
"mongodb"
基本的数据类型,列表
在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 1 #通过区间获取具体的值
1) "three"
2) "two"
127.0.0.1:6379> RPUSH list four #将一个值或者多个值,插入到列表尾部
(integer) 4
127.0.0.1:6379> LRANGE list 0 -1
1) "three"
2) "two"
3) "one"
4) "four"
########################################################
LPOP
RPOP
127.0.0.1:6379> LRANGE list 0 -1
1) "three"
2) "two"
3) "one"
4) "four"
127.0.0.1:6379> LPOP list #移除列表的第一个元素
"three"
127.0.0.1:6379> RPOP list #移除列表的最后一个元素
"four"
127.0.0.1:6379> LRANGE list 0 -1
1) "two"
2) "one"
########################################################
Lindex
127.0.0.1:6379> LRANGE list 0 -1
1) "two"
2) "one"
127.0.0.1:6379> LINDEX list 1 #通过下标获取值
"one"
127.0.0.1:6379> LINDEX list 0
"two"
########################################################
Llen
127.0.0.1:6379> Llen list #返回列表的长度
(integer) 2
#######################################################
Lrem #移除指定的值
127.0.0.1:6379> Lrem list 1 one
(integer) 1
#######################################################
127.0.0.1:6379> Lpush mylist "hello"
(integer) 1
127.0.0.1:6379> Lpush mylist "hello1"
(integer) 2
127.0.0.1:6379> Lpush mylist "hello2"
(integer) 3
127.0.0.1:6379> Lpush mylist "hello3"
(integer) 4
127.0.0.1:6379> ltrim mylist 1 2 #通过下标截取指定的长度
OK
127.0.0.1:6379> LRANGE mylist 0 -1
1) "hello2"
2) "hello1"
#######################################################
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 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 #将一个具体的值插入某个元素的前面或者后面
127.0.0.1:6379> rpush list "v1"
(integer) 1
127.0.0.1:6379> rpush list "v2"
(integer) 2
127.0.0.1:6379> linsert list before "v2" "v3"
(integer) 3
127.0.0.1:6379> lrange list 0 -1
1) "v1"
2) "v3"
3) "v2"
小结
应用:
消息排队!消息队列(Lpush Rpop),栈(Lpush Lpop)
Redis的Set是string类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。
Redis 中 集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。
集合中最大的成员数为 232 - 1 (4294967295, 每个集合可存储40多亿个成员)
#######################################################
127.0.0.1:6379> sadd myset "v1" #set集合中添加元素
(integer) 1
127.0.0.1:6379> sadd myset "v2"
(integer) 1
127.0.0.1:6379> sadd myset "v3"
(integer) 1
127.0.0.1:6379> SMEMBERS myset #查看指定set的所有值
1) "v3"
2) "v2"
3) "v1"
127.0.0.1:6379> SISMEMBER myset v1 #判断某一个值是不是在set集合中
(integer) 1
127.0.0.1:6379> SISMEMBER myset v4
(integer) 0
#######################################################
127.0.0.1:6379> SCARD myset #获取元素个数
(integer) 3
#######################################################
127.0.0.1:6379> SMEMBERS myset
1) "v3"
2) "v2"
3) "v1"
127.0.0.1:6379> srem myset v3 #移除指定元素
(integer) 1
127.0.0.1:6379> SMEMBERS myset
1) "v2"
2) "v1"
#######################################################
127.0.0.1:6379> SRANDMEMBER myset #随机抽选出一个元素
"v1"
127.0.0.1:6379> SRANDMEMBER myset
"v1"
127.0.0.1:6379> SRANDMEMBER myset
"v2"
#######################################################
127.0.0.1:6379> SMEMBERS myset
1) "v2"
2) "v1"
127.0.0.1:6379> spop myset #随机删除set集合中的元素
"v1"
127.0.0.1:6379> SMEMBERS myset
1) "v2"
#######################################################
127.0.0.1:6379> sadd set1 "v1"
(integer) 1
127.0.0.1:6379> sadd set1 "v2"
(integer) 1
127.0.0.1:6379> sadd set1 "v3"
(integer) 1
127.0.0.1:6379> sadd set2 "v4"
(integer) 1
127.0.0.1:6379> smove set1 set2 v1 #将一个指定的值,移动到另一个set集合
(integer) 1
127.0.0.1:6379> SMEMBERS set1
1) "v3"
2) "v2"
127.0.0.1:6379> SMEMBERS set2
1) "v4"
2) "v1"
#######################################################
127.0.0.1:6379> SDIFF set1 set2 #差集
1) "v2"
2) "v3"
127.0.0.1:6379> SINTER set1 set2 #交集 共同好友就可以这样实现
1) "v4"
127.0.0.1:6379> SUNION set1 set2 #并集
1) "v4"
2) "v2"
3) "v3"
4) "v1"
Redis hash 是一个string类型的field和value的映射表,hash特别适合用于存储对象。
Set就是一种简化的Hash,只变动key,而value使用默认值填充。可以将一个Hash表作为一个对象进行存储,表中存放对象的信息。
#######################################################
127.0.0.1:6379> hset hash filed1 codesheep #set一个具体的key-value
(integer) 1
127.0.0.1:6379> hget hash filed1 #获取一个字段值
"codesheep"
127.0.0.1:6379> hset hash filed1 hello filed2 world
(integer) 1
127.0.0.1:6379> hmset hash filed1 hello filed2 world #set多个key-value
OK
127.0.0.1:6379> hmget hash filed1 filed2 #获取多个字段值
1) "hello"
2) "world"
127.0.0.1:6379> hgetall hash #获取全部的数据
1) "filed1"
2) "hello"
3) "filed2"
4) "world"
#######################################################
127.0.0.1:6379> hdel hash filed1 #删除hash指定的key
(integer) 1
127.0.0.1:6379> hgetall hash
1) "filed2"
2) "world"
#######################################################
127.0.0.1:6379> hlen hash #获取字段数量
(integer) 1
#######################################################
127.0.0.1:6379> HEXISTS hash filed1 #判断指定字段是否存在
(integer) 0
127.0.0.1:6379> HEXISTS hash filed2
(integer) 1
#######################################################
127.0.0.1:6379> hkeys hash #只获得所有的key
1) "filed2"
2) "filed1"
127.0.0.1:6379> hvals hash #只获得所有的value
1) "world"
2) "hello"
#######################################################
Hash变更的数据user name age,尤其是用户信息之类的,经常变动的信息!Hash更适合于对象的存储,Sring更加适合字符串存储!
不同的是每个元素都会关联一个double类型的分数(score)。redis正是通过分数来为集合中的成员进行从小到大的排序。
score相同:按字典顺序排序
有序集合的成员是唯一的,但分数(score)却可以重复。
127.0.0.1:6379> zadd set 1 one #添加一个值
(integer) 1
127.0.0.1:6379> zadd set 2 two 3 three #添加多个值
(integer) 2
127.0.0.1:6379> ZRANGE set 0 -1
1) "one"
2) "two"
3) "three"
#######################################################
127.0.0.1:6379> zadd salary 2500 xiaohong
(integer) 1
127.0.0.1:6379> zadd salary 5000 zhangsan
(integer) 1
127.0.0.1:6379> zadd salary 4000 xiaoming
(integer) 1
127.0.0.1:6379> ZRANGEBYSCORE salary -inf +inf #排序 从小到大
1) "xiaohong"
2) "xiaoming"
3) "zhangsan"
127.0.0.1:6379> ZREVRANGE salary 0 -1 #排序 从大到小
1) "zhangsan"
2) "xiaoming"
127.0.0.1:6379> ZRANGEBYSCORE salary -inf +inf withscores #
1) "xiaohong"
2) "2500"
3) "xiaoming"
4) "4000"
5) "zhangsan"
6) "5000"
127.0.0.1:6379> ZRANGEBYSCORE salary -inf 4000 withscores #
1) "xiaohong"
2) "2500"
3) "xiaoming"
4) "4000"
#######################################################
127.0.0.1:6379> zrem salary xiaohong #移除指定元素
(integer) 1
127.0.0.1:6379> zrange salary 0 -1
1) "xiaoming"
2) "zhangsan"
127.0.0.1:6379> zcard salary #获取集合个数
(integer) 2
#######################################################
127.0.0.1:6379> zadd set 1 v1 2 v2 3 v3
(integer) 3
127.0.0.1:6379> zcount set 1 3 #获取指定区间的成员数量
(integer) 3
#######################################################
应用案例:
geoadd
#geoadd 添加地理位置
127.0.0.1:6379> geoadd china:city 116.40 39.90 beijing
(integer) 1
127.0.0.1:6379> geoadd china:city 121.47 31.23 shanghai
(integer) 1
127.0.0.1:6379> geoadd china:city 106.50 29.53 chongqing
(integer) 1
127.0.0.1:6379> geoadd china:city 114.05 22.52 shenzhen
(integer) 1
127.0.0.1:6379> geoadd china:city 120.16 30.24 hangzhou
(integer) 1
127.0.0.1:6379> geoadd china:city 108.96 34.26 xian
(integer) 1
geopos
#geopos 获取指定的城市的经度和纬度
127.0.0.1:6379> geopos china:city beijing
1) 1) "116.39999896287918091"
2) "39.90000009167092543"
geodist
#geodist 获取两个城市的直线距离
127.0.0.1:6379> GEODIST china:city beijing shanghai
"1067378.7564"
127.0.0.1:6379> GEODIST china:city beijing shanghai km
"1067.3788"
georadius
#georadius 以给定的经纬度为中心,找出某一半径内的元素
127.0.0.1:6379> GEORADIUS china:city 110 30 1000 km
1) "chongqing"
2) "xian"
3) "shenzhen"
4) "hangzhou"
有效经纬度
指定单位的参数 unit 必须是以下单位的其中一个:
关于GEORADIUS的参数
通过georadius
就可以完成 附近的人功能
withcoord:带上坐标
withdist:带上距离,单位与半径单位相同
COUNT n : 只显示前n个(按距离递增排序)
什么是基数?
数据集中不重复的元素的个数。
简介
Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。
花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。
因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。
其底层使用string数据类型
应用场景
网页的访问量(UV):一个用户多次访问,也只能算作一个人。
127.0.0.1:6379> pfadd set a b c d e f #创建第一组元素
(integer) 1
127.0.0.1:6379> PFCOUNT set #统计元素的基数数量
(integer) 6
127.0.0.1:6379> pfadd set1 a h d f r
(integer) 1
127.0.0.1:6379> PFCOUNT set1
(integer) 5
127.0.0.1:6379> PFMERGE set2 set set1 #合并两组元素 并集
OK
127.0.0.1:6379> PFCOUNT set2
(integer) 8
如果允许容错,那么一定可以使用Hyperloglog !
如果不允许容错,就使用set或者自己的数据类型即可 !
位存储
信息状态只有 0 和 1
BitMap
Bitmap是一串连续的2进制数字(0或1),每一位所在的位置为偏移(offset),在bitmap上可执行AND,OR,XOR,NOT以及其它位操作。
应用场景
签到统计、状态统计
使用bitmap记录周一到周日的打卡
周一:1 周二:0 周三:0......
127.0.0.1:6379> clear
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> bitcount sign #统计打卡天数
(integer) 4
Redis事务本质:一组命令的集合。
----------------- 队列 set set set 执行 -------------------
事务中每条命令都会被序列化,执行过程中按顺序执行,不允许其他命令进行干扰。
- 一次性
- 顺序性
- 排他性
Redis事务没有隔离级别的概念
Redis单条命令是保证原子性的,但是事务不保证原子性
multi
)exec
)所以事务中的命令在加入时都没有被执行,直到提交时才会开始执行(Exec)一次性完成。
正常执行事务
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 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> error k4 #错误的命令
(error) ERR unknown command `error`, with args beginning with: `k4`,
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 k4 #所有的命令都不会执行
(nil)
代码逻辑错误 (运行时异常) **其他命令可以正常执行 ** >>> 所以不保证事务原子性
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"
悲观锁:
乐观锁:
使用watch key
监控指定数据,相当于乐观锁加锁。
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的乐观锁操作(相当于getversion)
我们启动另外一个客户端模拟插队线程。
线程1:
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> # 此时事务并没有执行
模拟线程插队,线程2:
127.0.0.1:6379> get money
"80"
127.0.0.1:6379> set money 1000 # 修改了线程一中监视的money
OK
回到线程1,执行事务:
127.0.0.1:6379> EXEC # 执行之前,另一个线程修改了我们的值,这个时候就会导致事务执行失败
(nil) # 没有结果,说明事务执行失败
解锁获取最新值,然后再加锁进行事务。
unwatch
进行解锁。
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) 980
2) (integer) 40
注意:每次提交执行exec后都会自动释放锁,不管是否成功
使用Java来操作Redis,Jedis是Redis官方推荐使用的Java连接redis的客户端。
测试
1.导入对应的依赖
redis.clients
jedis
3.2.0
com.alibaba
fastjson
1.2.70
2.编码测试
public class TestPing {
public static void main(String[] args) {
Jedis jedis = new Jedis("127.0.0.1",6379);
String response = jedis.ping();
System.out.println(response); // PONG
}
}
3.事务
public class TestTX {
public static void main(String[] args) {
Jedis jedis = new Jedis("127.0.0.1",6379);
JSONObject jsonObject = new JSONObject();
jsonObject.put("k1","v1");
//开启事务
Transaction multi = jedis.multi();
String result = jsonObject.toJSONString();
try {
multi.set("user",result);
multi.exec();//执行事务
} catch (Exception e) {
multi.discard();//放弃事务
e.printStackTrace();
} finally {
System.out.println(jedis.get("user"));
jedis.close();//关闭连接
}
}
}
springboot 2.x后 ,原来使用的 Jedis 被 lettuce 替换。
jedis:采用的直连,多个线程操作的话,是不安全的。如果要避免不安全,使用jedis pool连接池!更像BIO模式
lettuce:采用netty,实例可以在多个线程中共享,不存在线程不安全的情况!可以减少线程数据了,更像NIO模式
我们在学习SpringBoot自动配置的原理时,整合一个组件并进行配置一定会有一个自动配置类xxxAutoConfiguration,并且在spring.factories中也一定能找到这个类的完全限定名。Redis也不例外。
那么就一定还存在一个RedisProperties类
之前我们说SpringBoot2.x后默认使用Lettuce来替换Jedis,现在我们就能来验证了。
先看Jedis:
@ConditionalOnClass注解中有两个类是默认不存在的,所以Jedis是无法生效的
然后再看Lettuce:
完美生效。
现在我们回到RedisAutoConfiguratio
只有两个简单的Bean
当看到xxTemplate时可以对比RestTemplat、SqlSessionTemplate,通过使用这些Template来间接操作组件。那么这俩也不会例外。分别用于操作Redis和Redis中的String数据类型。
在RedisTemplate上也有一个条件注解,说明我们是可以对其进行定制化的
说完这些,我们需要知道如何编写配置文件然后连接Redis,就需要阅读RedisProperties
这是一些基本的配置属性。
还有一些连接池相关的配置。注意使用时一定使用Lettuce的连接池。
整合测试
org.springframework.boot
spring-boot-starter-data-redis
#配置redis
spring.redis.host=127.0.0.1
spring.redis.port=6379
@SpringBootTest
class Redis02SpringbootApplicationTests {
@Autowired
private RedisTemplate redisTemplate;
@Test
void contextLoads() {
// redisTemplate 操作不同的数据类型,api和我们的指令是一样的
// opsForValue 操作字符串 类似String
// opsForList 操作List 类似List
// opsForHah
// 除了基本的操作,我们常用的方法都可以直接通过redisTemplate操作,比如事务和基本的CRUD
// 获取连接对象
//RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
//connection.flushDb();
//connection.flushAll();
redisTemplate.opsForValue().set("key","codesheep");
System.out.println(redisTemplate.opsForValue().get("key"));
}
}
此时我们回到Redis查看数据时候,惊奇发现全是乱码,可是程序中可以正常输出:
这时候就关系到存储对象的序列化问题,在网络中传输的对象也是一样需要序列化,否者就全是乱码。
我们转到看那个默认的RedisTemplate内部什么样子:
在最开始就能看到几个关于序列化的参数。
默认的序列化器是采用JDK序列化器
而默认的RedisTemplate中的所有序列化器都是使用这个序列化器:
后续我们定制RedisTemplate就可以对其进行修改。
RedisSerializer提供了多种序列化方案:
直接调用RedisSerializer的静态方法来返回序列化器,然后set
自己new 相应的实现类,然后set
我们创建一个Bean加入容器,就会触发RedisTemplate上的条件注解使默认的RedisTemplate失效。
@Configuration
public class RedisConfig {
@Bean
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
// 将template 泛型设置为
RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
// 连接工厂,不必修改
template.setConnectionFactory(factory);
//序列化配置
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、hash的key 采用 String序列化方式
template.setKeySerializer(RedisSerializer.string());
template.setHashKeySerializer(RedisSerializer.string());
// value、hash的value 采用 Jackson 序列化方式
template.setValueSerializer(RedisSerializer.json());
template.setHashValueSerializer(RedisSerializer.json());
template.afterPropertiesSet();
return template;
}
}
这样一来,只要实体类进行了序列化,我们存什么都不会有乱码的担忧了。
使用RedisTemplate需要频繁调用.opForxxx然后才能进行对应的操作,这样使用起来代码效率低下,工作中一般不会这样使用,而是将这些常用的公共API抽取出来封装成为一个工具类,然后直接使用工具类来间接操作Redis,不但效率高并且易用。
工具类参考博客:
https://www.cnblogs.com/zeng1994/p/03303c805731afc9aa9c60dbbd32a323.html
https://www.cnblogs.com/zhzhlong/p/11434284.html
单位 大小写不敏感
可以使用 include 组合多个配置问题
网络配置
日志输出级别
日志输出文件
快照
由于Redis是基于内存的数据库,需要将数据由内存持久化到文件中
持久化方式:
RDB文件相关
主从复制
Security模块中进行密码设置
客户端连接相关
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 :永不过期,返回错误
AOF相关部分
redis是内存数据库,如果不将内存中的数据库保存到磁盘,那么一旦服务器进程退出,服务器中的数据库状态也会消失。
什么是RDB
在指定时间间隔后,将内存中的数据集快照写入数据库 ;在恢复时候,直接读取快照文件,进行数据的恢复 ;
默认情况下, Redis 将数据库快照保存在名字为 dump.rdb的二进制文件中。文件名可以在配置文件中进行自定义。
工作原理
在进行 RDB 的时候,redis 的主线程是不会做 io 操作的,主线程会 fork 一个子线程来完成该操作;
这种工作方式使得 Redis 可以从写时复制(copy-on-write)机制中获益(因为是使用子进程进行写操作,而父进程依然可以接收来自客户端的请求。)
触发机制
save
使用 save
命令,会立刻对当前内存中的数据进行持久化 ,但是会阻塞,也就是不接受其他操作了;
由于 save
命令是同步命令,会占用Redis的主进程。若Redis数据非常多时,save
命令执行速度会非常慢,阻塞所有客户端的请求。
bgsave
bgsave
是异步进行,进行持久化的时候,redis
还可以将继续响应客户端请求 ;
bgsave和save对比
命令 | save | bgsave |
---|---|---|
IO类型 | 同步 | 异步 |
阻塞 | 是 | 是 |
复杂度 | O(n) | O(n) |
优点 | 不会消耗额外的内存 | 不阻塞客户端命令 |
缺点 | 阻塞客户端命令 | 需要fock子进程,消耗内存 |
优缺点
优点:
缺点:
什么是AOF
将我们所有的命令都记录下来,history,恢复的时候就把这个文件全部再执行一遍
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WlhukFCi-1618411869456)(C:\Users\Jerry\AppData\Roaming\Typora\typora-user-images\image-20210303112241028.png)]
以日志的形式来记录每个写的操作,将Redis执行过的所有指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。
为什么使用AOF
快照功能(RDB)并不是非常耐久(durable): 如果 Redis 因为某些原因而造成故障停机, 那么服务器将丢失最近写入、以及未保存到快照中的那些数据。 从 1.1 版本开始, Redis 增加了一种完全耐久的持久化方式: AOF 持久化。
如果要使用AOF,需要修改配置文件:
appendonly no yes则表示启用AOF
默认是不开启的,我们需要手动配置,然后重启redis,就可以生效了!
如果这个aof文件有错误,这时候redis是启动不起来的,我需要修改这个aof文件
redis给我们提供了一个工具redis-check-aof --fix
优点和缺点
优点
缺点
一般来说, 如果想达到足以媲美 PostgreSQL 的数据安全性, 你应该同时使用两种持久化功能。
如果你非常关心你的数据, 但仍然可以承受数分钟以内的数据丢失, 那么你可以只使用 RDB 持久化。
有很多用户都只使用 AOF 持久化, 但并不推荐这种方式: 因为定时生成 RDB 快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快。
Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。
Redis客户端可以订阅任意数量的频道
订阅/发布消息图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lyn7fkob-1618411869457)(C:\Users\Jerry\AppData\Roaming\Typora\typora-user-images\image-20210303132924446.png)]
下图展示了频道 channel1 , 以及订阅这个频道的三个客户端 —— client2 、 client5 和 client1 之间的关系:
当有新消息通过 PUBLISH 命令发送给频道 channel1 时, 这个消息就会被发送给订阅它的三个客户端:
命令 | 描述 |
---|---|
PSUBSCRIBE pattern [pattern…] | 订阅一个或多个符合给定模式的频道。 |
PUNSUBSCRIBE pattern [pattern…] | 退订一个或多个符合给定模式的频道。 |
PUBSUB subcommand [argument[argument]] | 查看订阅与发布系统状态。 |
PUBLISH channel message | 向指定频道发布消息 |
SUBSCRIBE channel [channel…] | 订阅给定的一个或多个频道。 |
SUBSCRIBE channel [channel…] | 退订一个或多个频道 |
订阅端
127.0.0.1:6379> SUBSCRIBE codesheep # 订阅一个频道codesheep
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "codesheep"
3) (integer) 1
# 等待读取推送的信息
1) "message" # 消息
2) "codesheep" # 哪个频道的消息
3) "hello" #消息的具体内容
发送端
127.0.0.1:6379> PUBLISH codesheep hello # 发布者发布消息到频道
(integer) 1
127.0.0.1:6379>
每个 Redis 服务器进程都维持着一个表示服务器状态的 redis.h/redisServer 结构, 结构的 pubsub_channels 属性是一个字典, 这个字典就用于保存订阅频道的信息,其中,字典的键为正在被订阅的频道, 而字典的值则是一个链表, 链表中保存了所有订阅这个频道的客户端。
客户端订阅,就被链接到对应频道的链表的尾部,退订则就是将客户端节点从链表中移除。
稍微复杂的场景,我们就会使用消息中间件MQ处理。
主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(Master/Leader),后者称为从节点(Slave/Follower), 数据的复制是单向的!只能由主节点复制到从节点(主节点以写为主、从节点以读为主)。
默认情况下,每台Redis服务器都是主节点,一个主节点可以有0个或者多个从节点,但每个从节点只能由一个主节点。
只配置从库,不用配置主库
查看当前库的信息:info replication
127.0.0.1:6379> info replication
# Replication
role:master # 角色
connected_slaves:0 # 从机数量
master_replid:3b54deef5b7b7b7f7dd8acefa23be48879b4fcff
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
既然需要启动多个服务,就需要多个配置文件。每个配置文件对应修改以下信息:
启动单机多服务集群:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y0MS4UNe-1618411869457)(C:\Users\Jerry\AppData\Roaming\Typora\typora-user-images\image-20210303154432086.png)]
==默认情况下,每台Redis服务器都是主节点;==我们一般情况下只用配置从机就好了!
一主(79)二从(80,81)
使用SLAVEOF host port
就可以为从机配置主机了。
然后主机上也能看到从机的状态:
我们这里是使用命令搭建,是暂时的,==真实开发中应该在从机的配置文件中进行配置,==这样的话是永久的。
从机只能读,不能写,主机可读可写但是多用于写。
127.0.0.1:6381> set name sakura # 从机6381写入失败
(error) READONLY You can't write against a read only replica.
127.0.0.1:6380> set name sakura # 从机6380写入失败
(error) READONLY You can't write against a read only replica.
127.0.0.1:6379> set name sakura
OK
127.0.0.1:6379> get name
"sakura"
当主机断电宕机后,默认情况下从机的角色不会发生变化 ,集群中只是失去了写操作,当主机恢复以后,又会连接上从机恢复原状。
当从机断电宕机后,若不是使用配置文件配置的从机,再次启动后作为主机是无法获取之前主机的数据的,若此时重新配置称为从机,又可以获取到主机的所有数据。这里就要提到一个同步原理。
第二条中提到,默认情况下,主机故障后,不会出现新的主机,有两种方式可以产生新的主机:
如果主机断开了连接,我们可以使用SLAVEOF no one
让自己变成主机!其他的节点就可以手动连接到最新的主节点(手动)!如果这个时候老大修复了,那么就重新连接!
更多信息参考博客:https://www.jianshu.com/p/06ab9daf921d
主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。
哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行多个Redis实例。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-orrfkIFR-1618411869458)(C:\Users\Jerry\AppData\Roaming\Typora\typora-user-images\image-20210303182228666.png)]
哨兵的作用:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cuVYrymP-1618411869458)(C:\Users\Jerry\AppData\Roaming\Typora\typora-user-images\image-20210303182502801.png)]
假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象成为主观下线,当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。
我们目前的状态是一主二从!
1、配置哨兵配置文件 sentinen.conf
# sentinen monitor 被监控的名称 host port 1
sentinen monitor myredis 127.0.0.1 6379 1
2、启动哨兵
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4FQJzNUr-1618411869458)(C:\Users\Jerry\AppData\Roaming\Typora\typora-user-images\image-20210303184850721.png)]
此时哨兵监视着我们的主机6379,当我们断开主机后:
优点:
缺点:
完整的哨兵模式配置文件 sentinel.conf
# 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)中查找,若没找到即缓存未命中,再在数据库中进行查找,数量少可能问题不大,可是一旦大量的请求数据(例如秒杀场景)缓存都没有命中的话,就会全部转移到数据库上,造成数据库极大的压力,就有可能导致数据库崩溃。网络安全中也有人恶意使用这种手段进行攻击被称为洪水攻击。
解决方案
布隆过滤器
对所有可能查询的参数以Hash的形式存储,以便快速确定是否存在这个值,在控制层先进行拦截校验,校验不通过直接打回,减轻了存储系统的压力。
缓存空对象
一次请求若在缓存和数据库中都没找到,就在缓存中方一个空对象用于处理后续这个请求。
这样做有一个缺陷:存储空对象也需要空间,大量的空对象会耗费一定的空间,存储效率并不高。解决这个缺陷的方式就是设置较短过期时间
即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。
概念
相较于缓存穿透,缓存击穿的目的性更强,一个存在的key,在缓存过期的一刻,同时有大量的请求,这些请求都会击穿到DB,造成瞬时DB请求量大、压力骤增。这就是缓存被击穿,只是针对其中某个key的缓存不可用而导致击穿,但是其他的key依然可以使用缓存响应。
比如热搜排行上,一个热点新闻被同时大量访问就可能导致缓存击穿。
解决方案
1.设置热点数据永不过期**
这样就不会出现热点数据过期的情况,但是当Redis内存空间满的时候也会清理部分数据,而且此种方案会占用空间,一旦热点数据多了起来,就会占用部分空间。
2.加互斥锁(分布式锁)**
在访问key之前,采用SETNX(set if not exists)来设置另一个短期key来锁住当前key的访问,访问结束再删除该短期key。保证同时刻只有一个线程访问。这样对锁的要求就十分高。
概念
大量的key设置了相同的过期时间,导致在缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩。
解决方案
这个思想的含义是,既然redis有可能挂掉,那我多增设几台redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群
这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。