- Rides笔记
Rides笔记
Redis入门
1. 概述
Redis 能干嘛?
- 内存存储、持久化,内存中是断电即失,所以说持久化很重要(RDB、AOF)
- 效率高,可以用于高效缓存
- 发布订阅系统(消息队列)
- 地图信息分析
- 计时器、计数器(浏览量)
- …
特性
- 多样的数据类型
- 持久化
- 集群
- 事务
学习中需要用到
- www.redis.io
- www.redis.cn
2. 测试性能
-
使用redis-benchmark命令进行测试
redis-benchmark -p 6379 -c 100 -n 100000
3. 基础知识
-
默认有16个数据库
-
切换数据库操作
select 3
-
查看当前数据库大小
dbsize
-
清除当前数据库
flushdb
-
清除所有数据库
flushall
-
-
Redis是单线程的!
Redis是很快的,官方表示,Redis是基于内存表示的,CPU不是redis的性能瓶颈,Redis的瓶颈是根据机器的内存和网络带宽的Redis为何单线程还这么快?
- 误区1: 高性能的服务器一定是多线程的?
- 误区2: 多线程(CPU上下文切换)一定比单线程效率高?
核心:redis是将所有的数据全部放到内存当中的,而对于内存系统来说没有上下文切换就是效率最高的
-
- 基于内存
redis的数据都是存储在内存中的,所有读写速度很快
-
- 单线程,不用维护锁机制
redis采用单线程机制,指令串行,不用维护额外的锁机制,资源竞争等
- 单线程,不用维护锁机制
-
- 数据结构简单,操作简单
自己内部实现了各种数据结构,根据情况进行了优化
(1)动态字符串结构
(2)hash的字典结构的容量大小取2^N,使取模运算可以转换为按位运算,更快。同时扩容缩容采用采用渐进式rehash
(3)zset采用跳表结构,范围查询更快
(4)list采用压缩链表结构,内存空间连续
- 数据结构简单,操作简单
五大数据类型
Redis-Key
# 获取所有的key
keys *
# 判断当前的key是否存在
exists name
# 移除当前的key
move name 1
# 设置当前key的过期时间(s)
expire name 10
# 查看当前key的剩余时间
ttl name
# 查看当前key的类型
type name
String
set key1 v1 #设置值
get key1 #获得值
APPEND key1 "hello" #追加字符串,如果不存在则新建
STRLEN key1 #获得key1的长度
127.0.0.1:6379[1]> set views 0
OK
127.0.0.1:6379[1]> incr views #对变量执行加一操作
(integer) 1
127.0.0.1:6379[1]> incr views
(integer) 2
127.0.0.1:6379[1]> decr views # 对变量执行减一操作
(integer) 1
127.0.0.1:6379[1]> decr views
(integer) 0
127.0.0.1:6379[1]> decr views
(integer) -1
127.0.0.1:6379[1]> decr views
(integer) -2
127.0.0.1:6379[1]> get views
"-2"
#####################################
设置自增量
incrby key increment
decrby key increment
######################## ##################################################
获取字符串范围内的字符
getrange key 0 3
127.0.0.1:6379[1]> getrange name 0 5
"hello,"
127.0.0.1:6379[1]> getrange name 0 -1
"hello,wodeshen"
替换字符串范围内的字符
setrange key 0 3
127.0.0.1:6379[1]> set key2 "abcd"
OK
127.0.0.1:6379[1]> setrange key2 0 "xx" #替换指定位置开始的字符串
(integer) 4
127.0.0.1:6379[1]> get key2
"xxcd"
##########################################################################
设置过期时间
setex (set with expire)
语法:setex key seconds value
当不存在时设置(在分布式锁中常常使用)
setnx(set if not exist)
127.0.0.1:6379> setex key 10 ll #设置变量key-value :key-ll 并且10s过期
OK
127.0.0.1:6379> ttl key
(integer) 6
127.0.0.1:6379> ttl key
(integer) 4
127.0.0.1:6379> setnx key ll
(integer) 1
127.0.0.1:6379> get key
"ll"
127.0.0.1:6379> setnx key la
(integer) 0
127.0.0.1:6379> get key
"ll"
##########################################################################
批量设置和批量读取
mset
mget
127.0.0.1:6379> mset k1 v1 k2 v2
OK
127.0.0.1:6379> keys *
1) "k2"
2) "k1"
127.0.0.1:6379> mget k1 k2
1) "v1"
2) "v2"
127.0.0.1:6379> msetnx k1 v2 k4 v4
(integer) 0
#原子操作,前面的不成功后面的也不执行
#对象
set user:1{name:zhangsan, age:20} #设置对象user:1,对象值为json字符来保存一个对象
127.0.0.1:6379> set user:1 {name:zhangsan,age:20}
OK
或者:
127.0.0.1:6379> mset user:1:name zhangsan user:1:age 20
OK
127.0.0.1:6379> keys *
1) "k1"
2) "user:1"
3) "user:1:name"
4) "user:1:age"
127.0.0.1:6379> get user:1
"{name:zhangsan,age:20}"
##########################################################################
先get再set
getset
127.0.0.1:6379> getset db ke
(nil)
127.0.0.1:6379> getset db no
"ke"
127.0.0.1:6379> get db
"no"
String 类型的使用场景:value除了是字符串还可以是数字
- 计数器
- 统计多单位的数量
- 粉丝数
- 对象缓存储存
List
基本的数据类型
可以将其是现成栈、队列、阻塞队列
- 所有的list命令都是l开头的
- 插入:
lpush
rpush
linsert
127.0.0.1:6379> lpush list one # 向列表中添加元素one,插入到列表头部(左侧)
(integer) 1
127.0.0.1:6379> lpush list 2
(integer) 2
127.0.0.1:6379> lpush list 3
(integer) 3
127.0.0.1:6379> lrange list 0 -1 # 获取列表中的所有元素 start0 end-1
1) "3"
2) "2"
3) "one"
127.0.0.1:6379> lrange list 1 3 # 新加入的在最上面,标号最小
1) "2"
2) "one"
127.0.0.1:6379> lrange list 1 4
1) "2"
2) "one"
127.0.0.1:6379> lrange list 0 4
1) "3"
2) "2"
3) "one"
127.0.0.1:6379> rpush list right1 right2 #将元素插入到列表尾部
(integer) 5
127.0.0.1:6379> lrange list 0 -1
1) "3"
2) "2"
3) "one"
4) "right1"
5) "right2"
#################
linsert #在指定值前面或后面插入指定的值
127.0.0.1:6379> lpush letter you
(integer) 1
127.0.0.1:6379> lpush letter I
(integer) 2
127.0.0.1:6379> linsert letter before you love #指定值前面
(integer) 3
127.0.0.1:6379> lrange letter 0 -1
1) "I"
2) "love"
3) "you"
127.0.0.1:6379> linsert letter after you too
(integer) 4
127.0.0.1:6379> lrange letter 0 -1
1) "I"
2) "love"
3) "you"
4) "too"
-
移除元素:
lpop
rpop
ltrim
lrem
rpoplpush
LPOP RPOP 127.0.0.1:6379> LPOP list "3" 127.0.0.1:6379> rpop list "right2" 127.0.0.1:6379> lrange list 0 -1 1) "2" 2) "one" 3) "right1" ################ 移除指定的值 # 取关 uid """ lrem """ 127.0.0.1:6379> lrange list 0 -1 1) "3" 2) "3" 3) "2" 4) "1" 127.0.0.1:6379> lrem list 1 3 (integer) 1 127.0.0.1:6379> lrange list 0 -1 1) "3" 2) "2" 3) "1" ################# ltrim 截断操作 127.0.0.1:6379> lrange list1 0 -1 1) "1" 2) "2" 3) "3" 4) "4" 5) "5" 6) "1" 127.0.0.1:6379> ltrim list1 2 6 # [左闭右闭] OK 127.0.0.1:6379> lrange list1 0 -1 1) "3" 2) "4" 3) "5" 4) "1" ################################## rpoplpush # 移除列表最后一个元素并将他添加到新的列表 127.0.0.1:6379> lpush list 1 2 3 45D (integer) 4 127.0.0.1:6379> rpoplpush list listCopy "1"
-
获取值:
lindex
lrange
Llen
lindex 127.0.0.1:6379> lrange list 0 -1 1) "2" 2) "one" 3) "right1" 127.0.0.1:6379> lindex list 1 "one" 127.0.0.1:6379> lindex list 2 "right1" lrange list start end Llen #获得list长度 127.0.0.1:6379> llen list (integer) 3
-
修改:
lset
lset #必须是已存在的值 127.0.0.1:6379> lrange list 0 -1 1) "45D" 2) "3" 3) "2" 127.0.0.1:6379> lset list 0 4 OK 127.0.0.1:6379> lrange list 0 -1 1) "4" 2) "3" 3) "2"
总结
1. 实际上是一个链表,before Node after,left,right 都可以插入值
1. 如果key不存在,则创建新的链表
2. 如果存在,则新增内容
3. 如果移除了所有值,空链表也代表不存在
4. 在两边插入或者改动值效率最高
应用
- 消息排队
- 消息队列(Lpush Rpop)
- 栈(Lpush Lpop)
Set
不能重复,操作以s开头
1. 添加
-
sadd
127.0.0.1:6379> sadd s1 1 2 3 4 (integer) 4 127.0.0.1:6379> smembers
2. 查看
-
smembers
(error) ERR wrong number of arguments for 'smembers' command 127.0.0.1:6379> smembers s1 #查询某个元素是否在集合中 1) "1" 2) "2" 3) "3" 4) "4"
-
sIsMember
127.0.0.1:6379> sismember s1 1 #----查询某个元素是否存在 (integer) 1
-
scard
127.0.0.1:6379> scard s1 #----获取集合中元素个数 (integer) 4
-
srandMember
127.0.0.1:6379> srandmember s1 # ---- 获取集合中随机的值 "1" 127.0.0.1:6379> srandmember s1 "4" 127.0.0.1:6379> srandmember s1 "2" 127.0.0.1:6379> srandmember s1 2 #----指定个数返回 1) "1" 2) "4"
3. 删除
-
srem
127.0.0.1:6379> srem s1 1 2 #---- 删除元素 (integer) 2 127.0.0.1:6379> smembers s1 1) "3" 2) "4"
-
spop
127.0.0.1:6379> spop s1 #---- 随机删除集合中的元素 "1" 127.0.0.1:6379> spop s1 "3"
-
smove
#---- 把一个集合中的元素移动到另一个集合中 27.0.0.1:6379> sadd s2 sqsq (integer) 1 127.0.0.1:6379> smove s1 s2 4 (integer) 1 127.0.0.1:6379> smembers s1 1) "2" 127.0.0.1:6379> smembers s2 1) "4" 2) "sqsq"
4.集合运算
127.0.0.1:6379> sadd s1 a b c
(integer) 3
127.0.0.1:6379> sadd s2 c d e
(integer) 3
-
sdiff
127.0.0.1:6379> sdiff s1 s2 1) "b" 2) "a" #----返回s1 - s2 ,即差集
-
sinter
127.0.0.1:6379> sinter s1 s2 1) "c" #---- 返回交集 # 共同关注
-
sunion
127.0.0.1:6379> sunion s1 s2 1) "e" 2) "a" 3) "c" 4) "b" 5) "d" #---- 返回并集
Hash
Map集合 key --> key-value
命令以h开头
1. 新增
-
hset
127.0.0.1:6379> hset myhash name lizzy (integer) 1
-
hmset
一次设置多个field值
127.0.0.1:6379> hmset myhash habbit tennis sex female OK
-
hsetnx
当不存在时设置值
127.0.0.1:6379> hsetnx myhash major ai (integer) 1 127.0.0.1:6379> hsetnx myhash major cs (integer) 0
2. 读取
-
hget
127.0.0.1:6379> hget myhash name "lizzy"
-
hmget
一次获得多个field值
127.0.0.1:6379> hmget myhash habbit sex 1) "tennis" 2) "female"
-
hgetall
获得所有的键值对
127.0.0.1:6379> hgetall myhash 1) "name" 2) "lizzy" 3) "habbit" 4) "tennis" 5) "sex" 6) "female"
-
hlen
获取hash中有多少个键值对
127.0.0.1:6379> hlen myhash (integer) 2
-
hexists
判断某个field是否存在
127.0.0.1:6379> hexists myhash sex (integer) 1
-
hkeys
只获得所有的fields
-
hvals
只获得所有的values
3. 删除
-
hdel
删除某指定field
127.0.0.1:6379> hdel myhash habbit (integer) 1 127.0.0.1:6379> hgetall myhash 1) "name" 2) "lizzy" 3) "sex" 4) "female"
4.修改
-
Hincrby
指定字段value加一
127.0.0.1:6379> hset myhash age 20 (integer) 1 127.0.0.1:6379> hincrby myhash age 4 (integer) 24
-
Hdecrby
指定字段value减一
应用
-
hash可以存储对象,如user: name , age等,适合经常变动的信息
String更适合存字符串
Zset
有序集合,增加了一个值,set k1 v1 zset k1 score v1
1.新增
-
zadd
127.0.0.1:6379> zadd myset incr 1 one "1" 127.0.0.1:6379> zadd myset 2 two 3 three (integer) 2 # 可以一次加多个值
2. 读取
-
zrange
127.0.0.1:6379> zrange myset 0 -1 1) "one" 2) "two" 3) "three"
-
zrangebyscore
按照分数进行排序
127.0.0.1:6379> zadd salary 5000 xiaoming (integer) 1 127.0.0.1:6379> zadd salary 8000 xiaohong (integer) 1 127.0.0.1:6379> zadd salary 50000 xiaoli (integer) 1 127.0.0.1:6379> zadd salary 20000 xiaoshen (integer) 1 127.0.0.1:6379> ZRANGEBYSCORE salary -inf inf 1) "xiaoming" 2) "xiaohong" 3) "xiaoshen" 4) "xiaoli" ## inf代表无穷大
-
zrevrangebyscore
按照降序排列
127.0.0.1:6379> ZREVRANGEBYSCORE salary +inf -inf 1) "xiaoli" 2) "xiaoshen" 3) "xiaohong" 4) "xiaoming"
-
zcard
查看有多少个元素
127.0.0.1:6379> zcard salary (integer) 3
-
zcount
zcount 统计集合中分数区间内的元素个数
127.0.0.1:6379> zcount salary 8000 +inf (integer) 2
3. 删除
-
rem
删除该集合中指定值的元素
127.0.0.1:6379> zrem salary xiaohong (integer) 1 127.0.0.1:6379> zrange salary 0 -1 1) "xiaoming" 2) "xiaoshen" 3) "xiaoli"
应用
案例思路 : 排行榜应用、存储工资表排序等
普通消息1 、 重要消息2 带权重进行排序
排行榜应用
zset 数据结构
redis中是通过两种底层数据结构实现的。一种是ziplist压缩列表,另一种就是redis中最经典的数据结构skipList跳跃表。
- 第一次插入数据结构的选择
- 在使用ZADD
命令添加第一个元素到空key时,程序通过检查输入的第一个元素来决定该创建什么编码的有序集。 - 符合下面的条件就会创建ziplist
- 服务器属性server.zset_max_ziplist_entries 的值大于 0
- 元素的member长度小于服务器属性server.zset_max_ziplist_value的值(默认64)
- 后期编码转换
- 当刚开始选择了ziplist,会在下面两种情况下转为skipList。
- ziplist所保存的元素超过服务器属性server.zset_max_ziplist_entries
的值(默认值为 128 ) - 新添加元素的 member 的长度大于服务器属性
server.zset_max_ziplist_value 的值(默认值为 64)
- 为什么需要转换呢?
ziplist是一种紧挨着的存储空间,并且没有预留空间。ziplist的优势在于节省空间,但是当容量大到一定程度扩容就是最影响性能的因素。
-
SkipList
简介
- redis的skipList
因为是有序的,所以需要一个hash结构来存储value和score的对应关系,另一方面需要提供按照score来排序的功能,还能够指定score的范围来获取value列表的功能,上述也就是跳跃表要实现的功能 - 跳跃表(skiplist)是一种随机化的数据,
skiplist以有序的方式在层次化的链表中保存元素, 效率和平衡树媲美
------ 查找、删除、添加等操作都可以在对数期望时间下完成,
并且比起平衡树来说, 跳跃表的实现要简单直观得多
基本数据结构
- Redis 的跳跃表共有 64 层,意味着最多可以容纳 2^64
次方个元素。每一个 kv 块对应的结构如下面的代码中的 zslnode
结构,kv header 也是这个结构,只不过 value 字段是 null
值------无效的,score 是Double.MIN_VALUE,用来垫底的。kv
之间使用指针串起来形成了双向链表结构,它们是有序
排列的,从小到大。不同的 kv 层高可能不一样,层数越高的 kv
越少。同一层的 kv 会使用指针串起来。每一个层元素的遍历都是从 kv
header 出发。 - 更加清晰的的跳跃表实现文章:https://lotabout.me/2018/skip-l
为什么要使用跳表而不是用红黑树
-
从内存占用上来比较,跳表比平衡树更灵活一些。平衡树每个节点包含
2
个指针(分别指向左右子树),而跳表每个节点包含的指针数目平均为
1/(1-p),具体取决于参数 p 的大小。如果像 Redis里的实现一样,取
p=1/4,那么平均每个节点包含 1.33 个指针,比平衡树更有优势。 -
在做范围查找的时候,跳表比平衡树操作要简单。在平衡树上,我们找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。如果不对平衡树进行一定的改造,这里的中序遍历并不容易实现。而在跳表上进行范围查找就非常简单,只需要在找到小值之后,对第
1
层链表进行若干步的遍历就可以实现。 -
从算法实现难度上来比较,跳表比平衡树要简单得多。平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而跳表的插入和删除只需要修改相邻节点的指针,操作简单又快速。
- redis的skipList
三种特殊数据类型
geospatial
朋友的定位、打车的位置、附近的人
geo数据可以推算两地之间的距离,方圆几里的人
1.geoadd
添加地理位置
- 将指定的地理空间位置(经度、纬度、名称)添加到指定的key中,这些数据将会储存到sorted
set中 - 两极无法直接添加,一般会下载城市数据,通过Java程序一次性导入
127.0.0.1:6379> geoadd china:city 116.40 39.90 beijing 121.47 31.23 shanghai
(integer) 2
127.0.0.1:6379> geoadd china:city 91.23 29.66 lhasa 103.83 36.05 lanzhou
(integer) 2
2.geodist
返回两个给定位置之间的距离。
如果两个位置之间的其中一个不存在, 那么命令返回空值。
指定单位的参数 unit 必须是以下单位的其中一个:
- m 表示单位为米。
- km 表示单位为千米。
- mi 表示单位为英里。
- ft 表示单位为英尺。
如果用户没有显式地指定单位参数, 那么 GEODIST
默认使用米作为单位。
GEODIST
命令在计算距离时会假设地球为完美的球形, 在极限情况下,
这一假设最大会造成 0.5% 的误差。
127.0.0.1:6379> geodist china:city lhasa lanzhou
"1373410.1899"
3. geohash
返回一个或多个位置元素的
Geohash 表示。
- 连续不断的二分
- 经度放在偶数位,纬度放在奇数位
- 再做base32位编码
127.0.0.1:6379> geohash china:city lanzhou beijing
1) "wq3v4dwwqq0"
2) "wx4fbxxfke0"
4.geopos
查询指定位置的经度、纬度
127.0.0.1:6379> geopos china:city beijing
1) 1) "116.39999896287918091"
2) "39.90000009167092543"
5.georadius
以给定的经纬度为中心,给出指定半径内的元素
127.0.0.1:6379> georadius china:city 110 30 1000 km
1) "lanzhou"
127.0.0.1:6379> georadius china:city 110 30 10000 km
1) "lhasa"
2) "lanzhou"
3) "shanghai"
4) "beijing"
127.0.0.1:6379> georadius china:city 110 30 10000 km count 2
1) "lanzhou"
2) "shanghai"
count 参数返回指定数量的值
6.给radiusbyMember
指定集合内的某个元素为中心点查询半径范围内的地点
127.0.0.1:6379> georadiusbymember china:city lanzhou 1500 km
1) "lhasa"
2) "lanzhou"
3) "beijing"
底层原理
底层使用Zset,可以使用zset命令操作geo
127.0.0.1:6379> zrem china:city shanghai
(integer) 1
127.0.0.1:6379> zrange china:city 0 -1
1) "lhasa"
2) "chengdu"
3) "lanzhou"
4) "beijing"
hyperloglog
什么是基数
A {1,3,5,7,8,5}
B {1,3,5,4,7}
基数------不重复的元素 = 6 , 可以接受误差!
简介
redis hyperloglog 基数统计的算法
网页的UV(一个人访问网站多次,但是还是算作一次访问量)
传统的方式:使用set保存用户的uid,使用uid作为唯一量的衡量
缺点:使用该方法会储存大量的uid,属于无效数据会占用大量内存
Hyperloglog优点:占用固定内存,2^64个不同的元素只需要12kb内存
命令以PF开头:
-
pfadd
添加数据
127.0.0.1:6379> pfadd mykey 1 2 3 4 5
(integer) 1
127.0.0.1:6379> pfcount mykey
(integer) 12
-
pfcount
统计不重复的数据
-
pfmerge
合并集合
127.0.0.1:6379> pfadd mykey2 9 10 11 (integer) 1 127.0.0.1:6379> pfmerge mykey mykey2 OK 127.0.0.1:6379> pfcount mykey (integer) 15
bitmaps
统计两个状态的需求,都可以用bitmaps,如:是否打卡、是否登录…
-
setbit
设置每一位的状态 0、1
127.0.0.1:6379> setbit sign 0 0 (integer) 0 127.0.0.1:6379> setbit sign 1 0 (integer) 0 127.0.0.1:6379> setbit sign 2 1 (integer) 0
-
getbit
设置某一位的状态 0、1
127.0.0.1:6379> getbit sign 2 (integer) 1 127.0.0.1:6379> getbit sign 1 (integer) 0
-
bitcount
统计1的个数
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> bitcount sign (integer) 3
事务
1. 基本操作
Redis事务本质:一组命令的集合!一个事务中的所有命令都会被序列化,在事务执行过程中会按照顺序执行。
---队列---
------ 命令1 命令2 命令3 ... 命令n -----> 执行
1. Redis事务没有隔离级别的概念!
所有的命令在事务中,并没有被直接执行!只有发起执行命令的时候才会执行!
2. Redis单条命令保证原子性,但是redis的事务不保证原子性!
3. Redis事务的执行步骤:
- 开启事务(multi)
- 命令入队(…)
- 执行事务(exec)
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> get k2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) OK
3) "v2"
4) OK
放弃事务
- discard
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k4 v4
QUEUED
127.0.0.1:6379(TX)> discard
OK
127.0.0.1:6379> get k4
(nil)
编译型异常(代码有问题,编译不通过)
事务中所有的命令都不会执行!
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> getset k3 v3
QUEUED
127.0.0.1:6379(TX)> getset k4
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379(TX)> set k5 v5
QUEUED
127.0.0.1:6379(TX)> exec
(error) EXECABORT Transaction discarded because of previous errors.
运行时异常
如果队列中存在语法性异常,那么执行命令其他命令可以正常执行
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> incr k1 #执行时出错
QUEUED
127.0.0.1:6379(TX)> set k2 v3
QUEUED
127.0.0.1:6379(TX)> getset k2 v2
QUEUED
127.0.0.1:6379(TX)> get k2
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) (error) ERR value is not an integer or out of range
3) OK
4) "v3"
5) "v2"
2. 实现乐观锁
使用Watch
悲观锁:什么时候都会出问题,无论做什么都会加锁
乐观锁:
- 认为什么时候都不会出问题,所以不回加锁!在更新数据时判断,在此期间是否有人修改过这个数据
- 获取Version
- 更新的时候比较Version
测试Watch
正常执行成功!
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
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> decrby money 20
QUEUED
127.0.0.1:6379(TX)> incrby out 20
QUEUED
127.0.0.1:6379(TX)> exec
1) (integer) 80
2) (integer) 20
-
多线程操作
线程1:
127.0.0.1:6379> watch money OK 127.0.0.1:6379> multi OK 127.0.0.1:6379(TX)> decrby money 20 QUEUED 127.0.0.1:6379(TX)> INCRBY out 20 QUEUED 127.0.0.1:6379(TX)> exec #模拟线程2改动了money数值,所以执行不成功 (nil)
线程2:
127.0.0.1:6379> get money "80" 127.0.0.1:6379> set money 1000 OK
-
执行失败后续操作
- 如果发现事务执行失败,先使用unwatch解锁
- 获取最新的值再次监视
Jedis
使用java来操作redis
jedis是Redis官方推荐的java连接开发工具!
编码测试
- 连接数据库
- 操作命令
- 结束测试
事务
-
jedis.multi()
使用该命令开启事务
-
multi.exec()
使用该命令执行事务队列
SpringBoot整合
说明:在SpringBoot2.x之后,原来使用的jedis替换成为了lettuce
jedis:采用直连,多个线程操作是不安全的。如果想避免不安全的,则采用jedis pool连接池!更像BIO模式
lettuce:采用netty,实例可以在多个现称中共享,不存在线程不安全的情况!减少线程数量,更像NIO模式
源码分析
//默认的RedisTemplate没有过多的设置,Redis对象都是需要序列化的
//可以自己定义一个redisTemplate来替换这个默认的
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnKnownHostException{
//两个泛型都是Object类型,后续使用需要强制转换<String,Object>
RedisTemplate<Object, Object> template = new RedisTemplate<>(); template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean
public StringRedisTemplate<Object, Object> stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnKnownHostException{
StringRedisTemplate template = new StringRedisTemplate(); template.setConnectionFactory(redisConnectionFactory);
return template;
}
整合测试
-
导入依赖
-
配置连接
使用yml文件或者properties配置
spring: redis: # Redis本地服务器地址,注意要开启redis服务,即那个redis-server.exe host: 127.0.0.1 # Redis服务器端口,默认为6379.若有改动按改动后的来 port: 6379 #Redis服务器连接密码,默认为空,若有设置按设置的来 password: jedis: pool: # 连接池最大连接数,若为负数则表示没有任何限制 max-active: 8 # 连接池最大阻塞等待时间,若为负数则表示没有任何限制 max-wait: -1 # 连接池中的最大空闲连接 max-idle: 8
#Redis本地服务器地址,注意要开启redis服务,即那个redis-server.exe spring.redis.host=127.0.0.1 #Redis服务器端口,默认为6379.若有改动按改动后的来 spring.redis.port=6379 #Redis服务器连接密码,默认为空,若有设置按设置的来 spring.redis.password= #连接池最大连接数,若为负责则表示没有任何限制 spring.redis.jedis.pool.max-active=8 #连接池最大阻塞等待时间,若为负责则表示没有任何限制 spring.redis.jedis.pool.max-wait=-1 #连接池中的最大空闲连接 spring.redis.jedis.pool.max-idle=8
-
测试
Redis.conf详解
启动时就需要配置文件
1. 包含
可以包含多个配置文件
2. 网络
bind 绑定的ip
protected-mode yes #保护模式
port 6379 # 端口模式
3.通用
daemonize yes #以守护进行的方式运行,默认是no,打开为yes(即后台运行)
pidfile /var/run/redis_6379.pid #如果后台方式运行,需要指定一个pid文件
# 日志
loglevel notice # debug 、 verbose、notice、warning
logfile '' #生成的日志文件地址
4. 快照
持久化,在规定的时间内执行了多少次操作就会持久化到 .rdb .aof 文件
redis是内存数据库如果不持久化断电即失
Redis持久化
持久化是重点
RDB(Redis DataBase)
- 触发规则:
- save规则满足的条件下
- flushall的命令下
- 退出redis
- 如何恢复
- 只需要将rdb文件放在redis启动目录下,就会在启动时自动检查dump.rdb并恢复其中数据
- 优点:
- 适合大规模的数据恢复!
- 对数据的完整性要求不高则效率很高!
- 缺点:
- 需要一定的时间间隔进行操作,如果意外宕机,则最后一次数据会丢失
- fork进程时,会占用一定的内存空间
AOF(Append Only File)
将所有命令都记录下来,history,恢复时执行所有历史命令
-
如果aof文件出错,redis启动不起来,需要使用
redis-check-aof
进行修复 -
优点:
-
每一次修改都同步,文件的完整性会更好
-
默认每秒同步一次,可能会丢失一秒的数据
- 缺点:
- 相对于数据文件大小来说,aof文件远大于rdb,修复速度也比rdb慢
- aof运行效率也低于rdb
Redis发布订阅
redis发布订阅(pub/sub)
是一种消息通信模式:发送者发送消息,订阅者接收消息
redis客户端可以订阅任意数量的频道
-
测试:
- 接收端
SUBSCRIBE lizzy
-
发送端
127.0.0.1:6379> publish lizzy "youarehere" (integer) 1
-
结果
1) "message" 2) "lizzy" 3) "youarehere"
-
原理
Redis通过PUBLISH、SUBSCRIBE、PSUBSCRIBE等命令实现发布和订阅功能
-
通过Subscribe命令订阅某频道后,redis-server里维护了一个字典
- 字典的键就是一个个频道,
- 字典的值是一个链表,链表中保存了订阅了这个频道的所有客户端
Subscribe命令的本质就是将客户端添加到指定的channel的订阅链表中
-
通过publish向订阅者发送消息,redis-server会使用给定的频道作为键,在它所维护的channel字典中查找记录了订阅这个频道的所有客户端的链表,遍历链表发送消息
-
-
使用场景:
-
实时消息系统
-
实时聊天(频道当作聊天室,将消息回显给所有人即可)
-
订阅发布,关注系统
TIPS:稍微复杂些的场景用消息中间件做
-
Redis 主从复制
适用于大部分情况,因为大部分都是读多写少
需要修改每个从库的config文件
- 修改项
-
端口号
-
daemonize 改为yes,使其后台运行
-
pid文件
-
logfile------否则会重复
-
rdb文件名称
直接include redis.conf
然后添加这些信息即可
-
配置
- 在从机中执行命令
slaveof localhost 6379 127.0.0.1:6380> SLAVEOF localhost 6379 OK info replication # Replication role:slave master_host:localhost master_port:6379 master_link_status:down master_last_io_seconds_ago:-1 master_sync_in_progress:0 slave_read_repl_offset:0 slave_repl_offset:0 master_link_down_since_seconds:-1 slave_priority:100 slave_read_only:1 replica_announced:1 connected_slaves:0 master_failover_state:no-failover master_replid:c2996c05bc9498b14d92da6500dc57df86b89688 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
-
或者直接在从机配置文件中配置
replicaof localhost 6379
在主机中查看
info replication 127.0.0.1:6379> info replication # Replication role:master connected_slaves:2 slave0:ip=::1,port=6380,state=online,offset=350,lag=1 slave1:ip=::1,port=6381,state=online,offset=350,lag=1 master_failover_state:no-failover master_replid:ecff98bc0008e740607f7999d5fa1a527054412a master_replid2:0000000000000000000000000000000000000000 master_repl_offset:350 second_repl_offset:-1 repl_backlog_active:1 repl_backlog_size:1048576 repl_backlog_first_byte_offset:1 repl_backlog_histlen:350
-
复制原理
- Slave启动成功后连接到master后会发送一个sync同步命令
- Master接收到命令,启动后台的存盘程序,同时收集所有接收到的用于修改数据集的命令,在后太执行完毕之后,master将传送整个数据文件到slave,并完成一次完全同步
- 全量复制:slave接受到数据库文件数据后,将其存盘并加载到内存中
- 增量复制:master将新收集到的命令依次传给slave完成同步
只要重新连接,执行全量复制
哨兵模式(自动选取老大)
配置哨兵文件sentinel.conf
sentinel monitor myredis ::1 6380 1
在主机宕机后,随机选择一个从机(投票)选举为主机
当主机连接回来之后,只能作为从机
- 优点:
- 哨兵集群,基于主从复制模式,所有主从配置的优点全由
- 主从可以切换,故障可以转移,系统可用性更好
- 哨兵模式是主从模式的升级,手动变成自动,更加健壮
- 缺点:
- redis不好在线扩容,集群容量到达上限后,在线扩容很麻烦
- 实现哨兵模式的配置很麻烦,有很多选择
全部配置
port 26379 #如果哨兵集群,需要配置每个哨兵的端口
sentinel monitor mymaster 127.0.0.1 6379 2 #默认主机
Redis 缓存穿透和雪崩
缓存穿透
1. 概念
查询数据时,redis内存数据库中没有,也就是缓存没有命中,于是向数据库中查询,当请求量很大的时候会给数据库造成很大压力,这时候就相当于出现了缓存穿透
2. 解决方案
布隆过滤器
是一种数据结构,对所有可能查询的参数以hash形式储存,在控制层先进行校验,不符合则丢弃,从而避免了对底层存储系统的查询压力
缓存空对象
当存储层不命中后,即使返回空对象也将其缓存起来,同时会设置一个过期时间,之后再次访问这个数据将会从缓存中获取,保护了后端数据源
缓存击穿
1. 概述:
1. 缓存击穿:是指一个key非常热点,在不停的扛着大并发,大并发集中对着一个点进行访问,当这个key失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上穿了一个洞
2. 当某个key在过期的瞬间,有大量的请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访问数据库来查询最新数据,并写回缓存,会对数据库瞬间造成很大压力
2. 解决方案
1. 设置热点数据永不过期
2. 加互斥锁
setnx
分布式锁:使用分布式锁,确保对于每个key同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只需要等待即可
缓存雪崩
1.概述:
再某一个时间段,所有的缓存集中过期失效
2.解决方案:
1. redis高可用
搭建redis集群,一台崩掉还能使用其他的
2. 限流降级
缓存失效后,通过加锁或者队列来控制都数据库的线程数量
3. 数据预热
在正式部署之前,把可能的数据先预先访问一遍,将可能大量访问的数据加到缓存当中。在即将发生大并发访问之前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均衡。
缓存读写策略
1. Cache Aside Pattern
服务器同时维护db和cache,并且以db的结果为准
写:
- 先更新db
- 直接删除cache
读:
- 首先从cache中读取数据,命中则直接返回
- 否则从数据库中读取,并写入cache
- 问题:
-
写数据时,能先删除cache,后更新db吗? 会造成数据不一致
先写后读会造成不一致:1先写A时,先把cache中A删了,2直接从db中读到A之前的数据并写入cache,db再更新,造成不一致。 -
先更新db,再删除cache 同样也会造成不一致
先读后写,如果cache不存在A,1从db中读取A,此时2更新db中的A(因为cache中不存在,所以不用删),1返回A并写入cache,造成不一致。
缺陷:
-
首次请求数据一定不在cache中: 可以将热点数据提前存到cache中
-
写操作频繁会导致cache中的数据频繁被删除,影响命中率
- 强一致:更新db时同时更新cache,加分布式锁来保证不存在线程安全问题
- 短暂允许不一致,给cache数据设置一个短的过期时间,保证不一致影响较小
2. Read/Write Through Pattern(读写穿透)
Read/Write Through Pattern中服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中。cache
服务负责将此数据读取和写入 db,从而减轻了应用程序的职责。
写:
- 先更新cache
- 再由cache更新db
读:
- 从cache读,有则返回
- 没有的话从db中读再写入cache
Read-Through Pattern 实际只是在 Cache-Aside Pattern 之上进行了封装。在 Cache-Aside Pattern
下,发生读请求的时候,如果 cache中不存在对应的数据,是由客户端自己负责把数据写入 cache,而 Read Through Pattern 则是 cache 服务自己来写入缓存的,这对客户端是透明的。
缺陷:
也有首次请求数据一定不再 cache 的问题,对于热点数据可以提前放入缓存中。
3. Write Behind Pattern
由cache直接操作db,只更新cache,再由cache服务异步批量的更新db,一致性有更大的挑战,cache还没更新数据库,cache就挂了
Write Behind Pattern 下 db的写性能非常高,非常适合一些数据经常变化又对数据一致性要求没那么高的场景,比如浏览量、点赞量。
同时访问数据库来查询最新数据,并写回缓存,会对数据库瞬间造成很大压力
2. 解决方案
1. 设置热点数据永不过期
2. 加互斥锁
setnx
分布式锁:使用分布式锁,确保对于每个key同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只需要等待即可
缓存雪崩
1.概述:
再某一个时间段,所有的缓存集中过期失效
2.解决方案:
1. redis高可用
搭建redis集群,一台崩掉还能使用其他的
2. 限流降级
缓存失效后,通过加锁或者队列来控制都数据库的线程数量
3. 数据预热
在正式部署之前,把可能的数据先预先访问一遍,将可能大量访问的数据加到缓存当中。在即将发生大并发访问之前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均衡。
缓存读写策略
1. Cache Aside Pattern
服务器同时维护db和cache,并且以db的结果为准
写:
- 先更新db
- 直接删除cache
读:
- 首先从cache中读取数据,命中则直接返回
- 否则从数据库中读取,并写入cache
- 问题:
-
写数据时,能先删除cache,后更新db吗? 会造成数据不一致
先写后读会造成不一致:1先写A时,先把cache中A删了,2直接从db中读到A之前的数据并写入cache,db再更新,造成不一致。 -
先更新db,再删除cache 同样也会造成不一致
先读后写,如果cache不存在A,1从db中读取A,此时2更新db中的A(因为cache中不存在,所以不用删),1返回A并写入cache,造成不一致。
缺陷:
-
首次请求数据一定不在cache中: 可以将热点数据提前存到cache中
-
写操作频繁会导致cache中的数据频繁被删除,影响命中率
- 强一致:更新db时同时更新cache,加分布式锁来保证不存在线程安全问题
- 短暂允许不一致,给cache数据设置一个短的过期时间,保证不一致影响较小
2. Read/Write Through Pattern(读写穿透)
Read/Write Through Pattern中服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中。cache
服务负责将此数据读取和写入 db,从而减轻了应用程序的职责。
写:
- 先更新cache
- 再由cache更新db
读:
- 从cache读,有则返回
- 没有的话从db中读再写入cache
Read-Through Pattern 实际只是在 Cache-Aside Pattern 之上进行了封装。在 Cache-Aside Pattern
下,发生读请求的时候,如果 cache中不存在对应的数据,是由客户端自己负责把数据写入 cache,而 Read Through Pattern 则是 cache 服务自己来写入缓存的,这对客户端是透明的。
缺陷:
也有首次请求数据一定不再 cache 的问题,对于热点数据可以提前放入缓存中。
3. Write Behind Pattern
由cache直接操作db,只更新cache,再由cache服务异步批量的更新db,一致性有更大的挑战,cache还没更新数据库,cache就挂了
Write Behind Pattern 下 db的写性能非常高,非常适合一些数据经常变化又对数据一致性要求没那么高的场景,比如浏览量、点赞量。