文章目录
Redis
NoSQl简介
not only sql 不仅仅是SQL
Redis概述
官网概述:
Redis键的操作
- set keyName value 设置键和值
- keys * 列出当前库所有的键
- exists keyName 查看键是否存在(若存在,返回1,否则返回0)
- type keyName 查看当前的键对应的值类型
- del keyName 删除键
-
unlink KayName 非阻塞删除键(仅将keys中keyspace的数据删除,真正的删除会在异步操作中)
-
expire keyName seconds 设置键的过期时间
-
ttl keyName 查看当前key还有多长时间过期(-1表示永不过期 -2表示已过期)
- select index 切换数据库(默认16个数据库,index从0开始)
- dbsize 查看当前库中有多少个键
- move key index 移除当前key到index库
常用五大数据类型
Redis字符串
127.0.0.1:6379> set name liusiyuan # 设置键值
OK
127.0.0.1:6379> get name # 获取值
"liusiyuan"
127.0.0.1:6379> exists name # 判断键是否存在
(integer) 1
127.0.0.1:6379> append name hello# 向字符串追加,如果没有键相当于key
(integer) 14
127.0.0.1:6379> get name
"liusiyuanhello"
127.0.0.1:6379> strlen name# 获取键的长度
(integer) 14
127.0.0.1:6379> append name lsy
(integer) 17
127.0.0.1:6379> get name
"liusiyuanhellolsy"
127.0.0.1:6379> set views 0 #初始浏览量为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> decr views#自减1
(integer) 0
127.0.0.1:6379> get views
"0"
127.0.0.1:6379> incrby views 10# 设置指定增量
(integer) 9
127.0.0.1:6379> set key lslsllslslsl
OK
127.0.0.1:6379> get key1
(nil)
127.0.0.1:6379> get key
"lslsllslslsl"
127.0.0.1:6379> getrange key 0 1 #获取指定长度的字符串
"ls"
127.0.0.1:6379> getrange key 0 -1#获取全部字符串
"lslsllslslsl"
127.0.0.1:6379> set key2 12345678
OK
127.0.0.1:6379> get key2
"12345678"
127.0.0.1:6379> setrange key2 2 2
(integer) 8
127.0.0.1:6379> get keys
(nil)
127.0.0.1:6379> get key2
"12245678"
127.0.0.1:6379> setrange key2 3 *******
(integer) 10
127.0.0.1:6379> get key2
"122*******"
#setex(set with expire) 设置过期时间
#setnx(set if not exist) 不存在则设置(在分布式锁中会常常使用)
#msetnx (原子性操作)
127.0.0.1:6379> mset k1 1 k2 2 k3 3
OK
127.0.0.1:6379> keys
(error) ERR wrong number of arguments for 'keys' command
127.0.0.1:6379> keys *
1) "k3"
2) "k2"
3) "k1
127.0.0.1:6379> mget k1 k2 k3
1) "1"
2) "2"
3) "3"
127.0.0.1:6379> getset db redis
(nil)
127.0.0.1:6379> get db
"redis"
使用场景
value除了是我们的字符串还可以是我们的数字
- 计数器(浏览量)
- 统计多单位的数量
- 粉丝数
- 对象缓存
List(集合)
在redis里面,可以把list当成栈,队列,阻塞队列 所有的list命令都是由l开头
lpush lrange
127.0.0.1:6379> lpush list 1 # 将一个值或多个值插入到list
(integer) 1
127.0.0.1:6379> lpush list 2
(integer) 2
127.0.0.1:6379> lpush list 2
(integer) 3
127.0.0.1:6379> lpush list 3
(integer) 4
127.0.0.1:6379> lrange lisy 0 -1
(empty array)
127.0.0.1:6379> lrange lisy 0 -1
(empty array)
127.0.0.1:6379> lrange list 0 -1
1) "3"
2) "2"
3) "2"
4) "1"
127.0.0.1:6379> lrange list 0 1
1) "3"
2) "2"
127.0.0.1:6379> rpush list 4
(integer) 5
127.0.0.1:6379> lrange lisy 0 -1
(empty array)
127.0.0.1:6379> lrange list 0 -1
1) "3"
2) "2"
3) "2"
4) "1"
5) "4"
127.0.0.1:6379> lrange list 0 -1
1) "3"
2) "2"
3) "2"
4) "1"
5) "4"
127.0.0.1:6379> lpop list
"3"
127.0.0.1:6379> lrange list 0 -1
1) "2"
2) "2"
3) "1"
4) "4"
127.0.0.1:6379> rpop list
"4"
127.0.0.1:6379> lrange list 0 -1
1) "2"
2) "2"
3) "1"
127.0.0.1:6379> lindex list 1 #获取下标的元素
"2"
127.0.0.1:6379> lindex list 0
"2"
127.0.0.1:6379> llen list# 获取列表的长度
(integer) 3
127.0.0.1:6379> ltrim key1 1 2 #截断操作
OK
127.0.0.1:6379> lrange key1 0 -1
1) "5"
2) "4"
127.0.0.1:6379>
127.0.0.1:6379> lrange key1 0 -1
1) "1"
2) "1"
3) "1"
4) "5"
5) "4"
6) "3"
7) "2"
127.0.0.1:6379> lrem key1 2 1#去除两个value为1的
(integer) 2
127.0.0.1:6379> lrange key1 0 -1
1) "1"
2) "5"
3) "4"
4) "3"
5) "2"
127.0.0.1:6379> lpush ksy1 1
(integer) 1
127.0.0.1:6379> lpush ksy1 2
(integer) 2
127.0.0.1:6379> lpush ksy1 3
(integer) 3
127.0.0.1:6379> lpoplpush ksy1 ksy2
(error) ERR unknown command `lpoplpush`, with args beginning with: `ksy1`, `ksy2`,
127.0.0.1:6379> rpoplpush ksy1 ksy2
"1"
127.0.0.1:6379> keys *
1) "ksy1"
2) "ksy2"
127.0.0.1:6379> lrange ksy1 0 -1
1) "3"
2) "2"
127.0.0.1:6379> lrange ksy2 0 -1
1) "1"
127.0.0.1:6379>
lset 如果不存在列表去更新就会报错
如果存在,更新当前下标的值
linsert将具体的value插入到某个value的前面或后面
127.0.0.1:6379> linsert key before 3 2.5
(integer) 4
实际上是一个双链表
如果不存在,创建新的链表
如果存在,新增内容
如果移除key ,空链表,也代表不存在
在两边插入或改动值效率最高
使用场景
消息排队,消息队列
Set集合
无序不重复集合
127.0.0.1:6379> sadd set hello#set中添加元素
(integer) 1
127.0.0.1:6379> sadd set lsy
(integer) 1
127.0.0.1:6379> sadd set css
(integer) 1
127.0.0.1:6379> smembers set#查看指令set的所有值
1) "lsy"
2) "css"
3) "hello"
127.0.0.1:6379> sismember set css#判断set中是否存在值
(integer) 1
127.0.0.1:6379> sismember set csss
(integer) 0
127.0.0.1:6379> scard set#获取set中的个数
(integer) 3
127.0.0.1:6379>
127.0.0.1:6379> srem set css #移除set中的元素
(integer) 1
127.0.0.1:6379> scard set
(integer) 2
127.0.0.1:6379> smembers set
1) "lsy"
2) "hello"
#抽奖随机
127.0.0.1:6379> srandmember set #随机获取其中的一个值
"hello"
127.0.0.1:6379> srandmember set
"lsy"
127.0.0.1:6379> srandmember set
"lsy"
127.0.0.1:6379> spop set#随机移除元素
"css"
#将一个指定的值移到另一个集合中
127.0.0.1:6379> smove set set2 hello
(integer) 1
127.0.0.1:6379> smembers set2
1) "world"
2) "hello"
#共同关注(交集)
127.0.0.1:6379> sdiff set set2 #差集
1) "1"
2) "5"
127.0.0.1:6379> sinter set set2 #交集
1) "2"
2) "3"
3) "4"
127.0.0.1:6379> sunion set set2#并集
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
6) "6"
微博,A用户将所有关注的人放在一个set集合中,将它的粉丝也放在一个集合中 共同关注求交集
hash(Map集合)
127.0.0.1:6379> hset myhash name css#向hash中添加值
(integer) 1
[hmset 命令] [key 表名] [field 名称] [value值] [field 名称] [value值] ...
127.0.0.1:6379> hset myhash age 21
(integer) 1
127.0.0.1:6379> hget myhash age#获取hash中的值
"21"
#获取全部值
127.0.0.1:6379> hgetall myhash
1) "name"
2) "lsy"
3) "age"
4) "21"
#删除元素
127.0.0.1:6379> hdel myhash name
(integer) 1
#查看是否存在
127.0.0.1:6379> hexists myhash name
(integer) 0
127.0.0.1:6379> hexists myhash age
(integer) 1
#列出所有的值
127.0.0.1:6379> hvals myhash
1) "21"
#自增值
127.0.0.1:6379> hincrby myhash age 1
(integer) 22
#如果不存在则添加
127.0.0.1:6379> hsetnx myhash name 1
(integer) 1
127.0.0.1:6379> hsetnx myhash age 1
(integer) 0
数据结构
hash类型对应的数据结构是两种:ziplist压缩列表,hashtable哈希表,当field-value长度较短且个数较少时使用ziplist 否则hashtable
zset有序集合
数据结构
hash+跳跃表
跳跃表:和二分查找思想有点类似,建立多级索引结构,实现跳跃式查找,时间复杂度为logn
127.0.0.1:6379> zadd topn 200 java 300 c++ 400 mysql 500 redis
(integer) 4
127.0.0.1:6379> zcard topn
(integer) 4
127.0.0.1:6379> zrange topn 0 -1
1) "java"
2) "c++"
3) "mysql"
4) "redis"
127.0.0.1:6379> zrange topn 0 -1 withscores
1) "java"
2) "200"
3) "c++"
4) "300"
5) "mysql"
6) "400"
7) "redis"
8) "500"
新数据类型
bitmaps
位图,bitmaps想象成一个以位为单位的数组,数组中每个单元只能存0和1
应用场景:商品是否存在,用户是否存在 0和1
127.0.0.1:6379> publish channel1 hello
(integer) 1
127.0.0.1:6379> setbit users:20220101 1 1
(integer) 0
127.0.0.1:6379> setbit users:20220101 6 1
(integer) 0
127.0.0.1:6379> setbit users:20220101 11 1
(integer) 0
127.0.0.1:6379> setbit users:20220101 15 1
(integer) 0
127.0.0.1:6379> setbit users:20220101 19 1
(integer) 0
127.0.0.1:6379> getbit users:20220101 1
(integer) 1
127.0.0.1:6379> getbit users:20220101 8
(integer) 0
127.0.0.1:6379> getbit users:20220101 19
(integer) 1
#统计
127.0.0.1:6379> bitcount users:20220101
(integer) 5
Hyperloglog
是一种用于统计基数的数据集合类型,主要用于网页的UV(不重复访客,一个人多次访问网站,只记录一次)
GeoSpatial
主要用于地理位置的存储,适合朋友的定位,附近的人,打车距离计算
发布与订阅
消息通信方式
127.0.0.1:6379> subscribe channel1
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "channel1"
3) (integer) 1
1) "message"
2) "channel1"
3) "hello"
127.0.0.1:6379> publish channel1 hello
(integer) 1
各数据类型使用场景总结
String
最常规的操作,set和get,一般做计数缓存功能,
- 计数器(浏览量)
- 统计多单位的数量
- 粉丝数
- 对象缓存
hash
- 购物车 用户id作为key 商品id作为field 商品数量作为value
- 存储对象
list
- 简单的消息队列 lpush和rpop
set(无序不重复)
- 共同关注 A用户将所有关注的人放在一个set集合中,将它的粉丝也放在一个集合中 共同关注求交集 sinter set set2 #交集
- 全局去重
- 抽奖随机 srandmember set
zset(有序不重复)
比set多了一个权重参数scope
- 排行榜应用(比如所熟悉的语言排行榜,销售排行榜,点赞量排序)
Redis为什么这么快?
- redis底层为C语言编写,有一个全局hash表,查询时间为O(1)
- 在内存中操作,读取效率高
- 单线程数据操作,避免了多线程下的上下文切换
- 采用IO多路复用技术和非阻塞式IO
- 丰富的数据结构,string,hash,set,zset,list
Redis删除策略
在redis中,如果键设置了过期时间,可能不会立即删除,redis将该键带上过期时间开辟了一块内存用作于expires字典,采用惰性删除和定期删除
-
立即删除:创建一个定时器,当key设置了过期时间,时间过期后,有定时器任务立即执行对数据的删除. 用处理器性能换取内存空间
- 优点:节省内存,到时就立即删除,快速释放内存空间
- 缺点:不考虑CPU当前性能,无论CPU过载多高,都会执行删除任务
-
惰性删除:数据到期后不会立即删除,当下一次get数据时,会将数据删除
- 优点:节省了cpu当前性能
- 缺点:内存压力大,出现长期不使用的数据不会删除
-
定期删除:周期性的对redis数据进行检查,采用随机抽取策略,利用过期数据占比的方式控制删除频度,redis启动时会读取server.hz值,默认为10
- 每秒会对服务器进行定时轮询,在轮询中,依次对16个数据库进行轮询,每次轮询一个数据库时,会进行依次数据库检测.检测时间为250ms/server.hz,在expires空间内挑选w个数据检测是否超时,超时在删除 如果检测数据>W*25%时,则删除后再次检测次数据库
- Cpu占用设置峰值,检测频度可以自定义
- 内存压力不是很大,长期占用内存的冷数据会持续清理
Redis是多线程还是单线程
在Redis6.x前,redis是真正意义上的单线程操作,包括网络请求和数据命令操作,都是以单线程进行操作
在Redis6.x之后,处理客户端连接请求由专门线程完成,执行命令还是说单线程,保证了多线程环境下的数据安全
Redis持久化机制
RDB:在redis中默认采用RDB持久化,一段时间间隔,将数据快照写入dump.rdb文件中,写入机制在配置文件中有save m n 在m时间里数据改变n次写入文件
优点:
- 只有一个dump.rdb文件,方便持久化
- 适合大规模的数据恢复,比AOF启动效率高
- 性能最大化,fork子进程来完成写操作,让主进程继续处理命令
缺点
- 数据安全性地,最后一次数据持久化容易丢失
AOF:类似于日志文件,将操作命令写入文件,保存到AOF文件
优点
- 数据安全 不会存在数据丢失问题
- 如果AOF文件存在问题,也可以使用redis - check -aof检测文件
缺点
- AOF文件比RDB文件大,恢复速度慢
- 启动效率低
怎么选择持久化方式
同时开启两种机制,用AOF保证数据安全,作为恢复数据的第一选择,RDB作为不同程度的冷备
Redis事务
redis事务不支持事务回滚(原子性操作),但是保证事务隔离性问题
在redis事务中,一组命令是最小的单位,首先先将这些命令入队操作,exec执行命令,保证一次性多个命令,服务端在执行事务的过程中,不会被其他客户端发来的请求打扰
操作:开启事务–>multi
执行事务—>exec
Redis和MySQL数据一致性问题
总有四种方案:
- 先更新数据库,后更新redis
- 先更新redis,后更新数据库
- 先删除缓存,后更新数据库
- 先更新数据库,后删缓存
前两种方式没人使用
第一种存在问题:并发情况下先更新数据库,会将脏数据刷到缓存中
同时有请求A和请求B进行更新操作,那么会出现
(1)线程A更新了数据库
(2)线程B更新了数据库
(3)线程B更新了缓存
(4)线程A更新了缓存
这就出现请求A更新缓存应该比请求B更新缓存早才对,但是因为网络等原因,B却比A更早更新了缓存。这就导致了脏数据,因此不考虑。
第二种存在问题:如果先更新缓存成功,但是更新数据库失败,会造成数据不一致,和第一种情况类似
第三种存在问题:
- 线程A删除缓存
- 线程B访问缓存不存在
- 线程B读取数据库
- 线程B将旧值写入缓存
- 线程A新值写入数据库
解决方式:
①延迟双删
- 先淘汰数据缓存
- 写入数据库
- 休眠一秒,再次淘汰缓存,这么做,可以将一秒内产生的缓存脏数据再次删除
②更新和读取操作进行异步串行化