学习目标:
实验室项目后端 Redis 缓存技术
- 复习 Redis 基础知识
- 学习 Redis 高级知识,
学习内容:
基础篇 - Redis
Nosql
为什么需要Nosql
- 单机 Mysql 的年代!
思考一下:
- 数据量如果太大,一个机器放不下了
- 300万访问量,数据的索引(B+Tree)会太大了,机器内存也放不下
- 大量访问量(读写混合),一个服务器承受不了!
- Memcached(缓存) + MySql + 垂直拆分(读写分离)
缓存主要为解决“读"的问题
为什么进行读写分离?:一个网站 80% 的情况都在读数据,所以我们可以一个数据库专做写操作,一个数据库只做读操作,每次写操作后,保持数据库之间的数据一致性即可。
为什么需要Cache?:大量用户都在重复去数据库查询同一部分数据就会造成数据库性能的压力增大。所以我们可以使用缓存技术来减轻数据库的压力!
- 分库分表 + 水平拆分 + MySQL集群
早些年MyISAM :表锁,十分影响效率,高并发下就会出现严重的锁问题!
之后的Innodb :行锁,效率上比MyISAM提升了许多
慢慢就开始使用分库分表来解决写的压力,MySQL的集群很好的解决了那个时代的网站需求
- 如今大数据年代
大数据的IO压力下,存储了一亿条数据,表结构几乎无法再更改了,增加一列相当于更新一亿条数据… …
MySQL 等关系型数据库就不够用了,数据量太大了!![相当于说:MySQL 骨子里性能落后了,玩的再花哨也满足不了需求了… …]
回顾以前的 Mysql,都是用来存储整个网站内的所有杂七杂八的数据,数据库表很大,内容也很杂,效率也就差强人意了!就需要一些数据库专门帮助 Mysql 处理这些比如图片,文档,声音等数据了.这样 Mysql 的压力就得到了减轻。
用户的个人信息,社交网络,用户日志,地理信息等用户自己产生的数据,仅一个用户的数据就呈现爆发式增长! 这时候我们就需要使用NoSQL非关系型数据库了! NoSQL数据库可以很好的处理以上需求
Nosql + RDBMS(关系型数据库) 构建的项目才是最好的!
什么是Nosql
Nosql = Not Only SQL
比如说个人信息,社交网络,用户日志,地理信息等用户自己产生的数据**不需要一个固定的格式,**不需要多余的操作就可以横向扩展的!
Nosql 特点
-
方便扩展! 数据之间没有关系,高扩展性!
-
大数据量!高性能 !!! (1秒可读11万,写8万)
-
Nosql的缓存记录级是细粒度的缓存,性能会很高,
-
不需要事先设计数据库,数据类型是多样的,随取随用!
因为如果数据库量很大,我们根本最初就能无法设计好一个全面的RDBMS关系型数据库
-
传统的 RDBMS[关系数据库]
- 结构化组织
- SQL
- 数据和关系都存储在单独表中 row & Colum
- 严格的一致性
- 基础的事务处理
-
NoSQL[非关系型数据库]
- 没有固定的查询语言
- 有很多的的存储方式
- 键值对存储
- 列存储 : 大数据
- 文档存储 : Mongodb
- 图形存储 : 社交关系拓扑图
- 保持最终一致性,过程允许有误差
- CAP 定理,和 BASE(异地多活)
项目中用的数据库
-
商品基本信息
名称,价格,商家信息,
关系型数据库就可以解决了
-
商品的描述.评论(特点:文字比较多)
文档新数据库: Mongodb
-
图片
分布式文件系统: FastDFS
Taobao : TFS
GOOGLE:GFS
Hadoop: HDFS
Aliyun: OSS
-
商品检索
- 搜索引擎: solr ElasticSearch
- Aliyun : ISearch 多隆
-
商品热门的波段信息
- 内存数据库: Redis Tair Memache
-
商品的交易,支付的外部接口
第三方应用
NoSql 的四大分类
KV 键值对数据库
新浪: Redis
美团: Redis+Tair
阿里,百度 :Redis+memache
文档型数据库 bason格式和json一样
-
mongoDB (需要掌握)
基于分布式文件存储的数据库,C++编写,用于存储大量的文档
mongoDB 介于关系型数据库和非关系数据库中间的产品,归属于非关系型数据中功能最丰富**,最像关系型数据库**
-
ConthDB
列存储数据库 [大数据]
- 大数据中的 HBase
- 分布式文件系统
图形关系数据库
他不是存图片的,放的是关系,比如:朋友圈社交网络
Redis
Redis 是什么
Redis(Remote Dictionary Server) 远程字典服务. 开源的使用 C语言 编写。
支持网络,可基于内存,可持久化的日志型
是key-value 数据库
提供了多种语言的API
Redis 是当下最热门的 Nosql 技术之一
Redis 能干嘛
误区: 千万不要误以为Redis只做数据库
Redis 可以做 数据库,缓存,消息中间件.
- 可做内存存储 [断电即失] 效率高,用于高速缓存
- 可以做数据持久化 [RDB, AOF]
- 可做消息订阅
- 地图信息分析
- 计时器,计数器,(浏览量)
Redis 特性
- 多样的数据类型
- 支持持久化
- 支持集群
- 支持事务
Redis 单线程
Redis 是基于内存操作,Redis的瓶颈是根据机器的内存大小和带宽决定。
Redis 为什么单线程还这么快?
- 误区1: 高性能服务器一定是多线程的
- 误区2: 多线程(CPU上下文会切换!)一定比单线程效率高
Redis 是将所有的数据都放在了内存,所以Redis使用单线程避免了CPU上下文切换,多次读写都在一个CPU上.
windows 安装
Redis 推荐在 Linux 平台搭建使用
-
将windows压缩包解压缩到本地目录
-
双击 redis-server.exe 即可启动Redis服务
Linux 默认泡在 6379 端口
-
双击 redis-cli.exe 客户端连接Redis
redis 启动
redis 默认不是后台启动的,需要修改配置文件 redis.conf
-
修改 redis 配置文件
vim redis.conf
daemonize no
: Redis 默认不是以守护进程的方式运行,可以通过该配置项修改,使用 yes 启用守护进程(Windows 不支持守护线程的配置为 no )
-
启动 redis 后台服务
命令:
redis-server
注意这种方式启动 redis 使用的是默认配置。也可以通过启动参数告诉 redis 使用指定配置文件使用下面命令启动, 如下:
命令:
redis-server zzlconfig/redis.conf
启动 redis 服务进程后,就可以使用测试客户端程序
redis-cli
和 redis 服务交互了 -
使用 redis-cli 连接 redis 服务
命令:
$ redis-cli -h host -p port -a password
如果是本机,则 -h host 可以省略。
redis 默认端口号为 6379
Redis Ping 命令使用客户端向 Redis 服务器发送一个 PING ,如果服务器运作正常的话,会返回一个 PONG .ping 通常用于测试与服务器的连接是否仍然生效,或者用于测量延迟值。
如果连接正常就返回一个 PONG ,否则返回一个could not connect to Redis at 127.0.0.1:6379:Connectioni refused。
-
查看 redis 进程是否开启
redis 关闭
命令:shutdown
-> exit
Redis 性能测试
redis 性能测试是通过同时执行多个命令实现的
语法
redis-benchmark [option] [option value]
注意:该命令是在 redis 的目录下执行的,而不是在 redis-cli 客户端的内容命令
option
实例
Redis 配置文件
databases 16
设置数据库的数量,默认使用数据库为第0个,可以使用 select
命令切换
# 切换到第3个数据库
127.0.0.1:6379> select 3
0k
# 查看当前 db 大小
127.0.0.1:6379[3]> dbsize
(integer) 0
# set 一个值
127.0.0.1:6379[3]> set name zzl
# 再次查看 db 大小
127.0.0.1:6379[3]> dbsize
(integer) 1
# 查看 db 所有的 key
127.0.0.1:637[3]> keys *
1) "name"
# 清空 db
127.0.0.1:6379[3]> flushdb
ok
# 查看 db 所有的 key
127.0.0.1:6379[3]> keys *
(empty list or set)
Redis 基本命令
现在控制台使用的命令也一定要记住,只不过我们之后使用SpringBoot,Jedis 调用方法的本质也是这些命令
dbsize
: 查看当前 db 的大小
flushdb
: 清空当前 db
flushall
: 清空 redis 全部 db
keys *
: 查询当前 db 所有的 key
exists xxx
: 判断某个 key 是否存在
move xxx 1
: 移除当前 db 中的某个key, 1代表当前db
expire xxx n
: 设置 n 秒后移除某个key [应用:比如说一些热点数据一定时间内自动过期]
ttl xxx
: 查看某个 key 生命还剩几秒,默认-2表示以移过期,即移除
type xxx
: 查看某个 key 属于那种类型
五大数据类型
string( 字符串)
使用场景: 计数器,统计观看数量,粉丝数,对象缓存存储
批量创建和获取key
127.0.0.1:6379> mset k1 a k2 b k3 c # 批量创建
ok
127.0.0.1:6379> keys *
1) "k1"
2) "k2"
3) "k3"
127.0.0.1:6379> mget k1 k2 k3 # 批量获取value
1) "a"
2) "b"
3) "c"
插入,追加,长度
127.0.0.1:6379> keys * # 查看所有的key
1) "name"
127.0.0.1:6379> exists name # 判断某个key是否存在
(integer) 1
127.0.0.1:6379> type name # 查看当前key的类型
string
127.0.0.1:6379> append name "city" # 在某个key追加字符串
(integer) 7 # 返回修改后字符串长度
127.0.0.1:6379> get name
"zzlcity"
127.0.0.1:6379> strlen name # 获取某个key的长度
(integer) 7
自增,自减 incr xxx; decr xxx;
127.0.0.1:6379> set views 0 # 插入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> decr views
(integer) 0
127.0.0.1:6379> decr views
(integer) -1
127.0.0.1:6379> get views
"-1"
127.0.0.1:6379> incrby views 10 # 自增,并自定义步长
(integer) 9
127.0.0.1:6379> decrby views 10 # 自减,并自定义步长
(integer) -1
截取字符串 == subString 不会改变原字符串!
127.0.0.1:6379> set name "abcdefg"
ok
127.0.0.1:6379> get name
"abcdefg"
127.0.0.1:6379> getrange name 0 3 # 截取 [0,3] 闭区间
"abcd"
127.0.0.1:6379> getrange name 0 -1 # 截取整个字符串 == get key
"abcdefg"
覆盖字符串 == replace
127.0.0.1:6379> set name "abcdefg"
ok
127.0.0.1:6379> setrange name 0 xxx # 截取前三个字符
(integer) 7
127.0.0.1:6379> get name
"xxxdefg"
setex : set with expire 设置过期时间 | 对应的有 msetex [原子性操作]
setnx : set if not exist 不存在则设置,否则直接使用set存在覆盖的风险 | 对应的有 msetnx [原子性操作]
127.0.0.1:6379> setex name 30 "hell" # 设置name的值为 hell ,并30秒后过期
ok
127.0.0.1:6379> setnx mye "red" # 不存在才创建 mye
(integer) 1
127.0.0.1:6379> setnx mye "esf"
(integer) 0 # 表示 mye 已存在,创建失败
定义对象
127.0.0.1:6379> mset user:1:name zzl age 12
ok
127.0.0.1:6379> mget user:1:name age
1) "zzl"
2) "12"
getset : 先get再set
127.0.0.1:6379> getset db redis # 如果不存在值,则返回 nil
(nil)
127.0.0.1:6379> get db
"redis"
127.0.0.1:6379> getset db mysql # 如果存在值,则返回当前值
"redis"
127.0.0.1:6379> get db
"mysql"
List(列表)
redis 中的list 是一个链表结构
- 限制 Lpush Rpop 就是 "队列"结构
- 限制 Lpush Lpop 就是 "栈"结构
关于 list 的命令大部分以 ‘l’ 开头
#-----------------入!---------------------
127.0.0.1:6379> lpush list a # 将一个或多个值,插入列表头部(左)
(integer) 1
127.0.0.1:6379> lpush list b
(integer) 2
127.0.0.1:6379> lpush list c
(integer) 3
127.0.0.1:6379> lrange list 0 -1 # 获取list全部内容
1) "c"
2) "b"
3) "a"
127.0.0.1:6379> lrange list 0 1
1) "c"
2) "b"
127.0.0.1:6379> rpush list x # 将一个或多个值,插入到表尾部(右)
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "c"
2) "b"
3) "a"
4) "x"
#-----------------出!---------------------
127.0.0.1:6379> lpop list # 从左边开始移除一个元素
"c"
127.0.1:6379> Rpop list # 从右边开始移除一个元素
"x"
127.0.0.1:6379> lrange list 0 -1
1) "b"
2) "a"
#-----------------下标---------------------
127.0.1:6379> lindex list 1 # 通过下标获取 list 中的某一个值
"a"
127.0.1:6379> lindex list 0
"b"
#-----------------长度---------------------
127.0.1:6379> llen list # 返回列表的长度
(integer)2
#-----------------根据 count,value 移除指定元素---------------------
127.0.1:6379> lrem list 1 b # 移除list集合中的一个b
(integer)1
#-----------------修剪list(==trim)---------------------
# 假设 list 有 [a,b,c,d]
127.0.1:6379> ltrim list 1 2 # start stop 其实都填位置
ok
127.0.1:6379> lrange list 0 -1 # list 已经被改变了
1) a
2) d
#-----------------组合命令:移除列表的最后一个元素,并放入另一个list---------------------
# 假设 list1 有 [a,b,c,d]
127.0.1:6379> rpoplpush list1 list2
"d"
127.0.1:6379> lrange list1 0 -1
1) "a"
2) "b"
3) "c"
127.0.1:6379> lrange list2 0 -1
1) "d"
#-----------------替换list中指定下标的值---------------------
127.0.1:6379> lset list 0 d # 前提list指定位置需要有旧元素!否则报错
#-----------------在list某个位置左右插入值---------------------
# list = ["hello","world"]
# linsert key before|after pivot value
127.0.1:6379> linsert list before "world" "other"
(integer)3
127.0.1:6379> lrange list 0 -1
1) "hello"
2) "other"
3) "world"
127.0.1:6379> linsert list after "other" "new"
(integer)4
127.0.1:6379> lrange list 0 -1
1) "hello"
2) "other"
3) "new"
4) "world"
set(集合)
特点:set 集合中的值不可重复! , 无序的!
关于 set 集合的命令都是以 ‘s’ 开头
#---------------------set集合添加元素------------------------------------
127.0.1:6379> sadd myset 'a'
(integer) 1
127.0.1:6379> sadd myset 'b'
(integer) 1
127.0.1:6379> sadd myset 'c'
(integer) 1
#----------------------查看set集合所有值-----------------------------------
127.0.1:6379> smembers myset # set 集合是无序的
1) "a"
2) "c"
3) "b"
#-----------------------判断某值是否存于set集合----------------------------------
127.0.1:6379> sismemebr myset a # 判断 set 集合中是否存在某一个元素
(integer) 1
#-----------------------获取set集合中内容元素个数----------------------------------
127.0.1:6379> scart myset
(integer) 3
#-----------------------移除set集合指定值---------------------------------
127.0.1:6379> srem myset a
(integer) 1
#-----------------------随机筛选出一个或多个值---------------------------------
127.0.1:6379> srandmember myset 2 # 不写count,默认抽一个
1) "b"
2) "a"
#-----------------------随机移除一个或多个元素---------------------------------
127.0.1:6379> spop myset
"a"
#-----------------------将一个指定的值移动到另外一个set集合中--------------------------------
127.0.1:6379> smove myset myset2 "kuangs"
(integer)1
127.0.1:6379> smembers myset2
1) "kuangs"
#-----------两个集合-差集 sdiff------------------
127.0.1:6379> sdiff myset myset2
#-----------两个集合-交集 sinter------------------
127.0.1:6379> sinter myset myset2
#-----------两个集合-并集 sunion------------------
127.0.1:6379> sunion myset myset2
Hash(哈希)
结构: <key ,map>
应用:
hash易于存储经常变更的数据比如 user信息. hash比String类型更适合对象的存储.String更适合字符串存储
与hash有关的命令以h打头
#-------------单个赋值,取值-----------------
127.0.1:6379> hset myhash name zzl
127.0.1:6379> hset myhash age 18
127.0.1:6379> hget myhash name
"zzl"
#-------------多个赋值,取值------------------
127.0.1:6379> hmset myhash name zzl age 18 city yuncheng
ok
127.0.1:6379> hmget myhash name age city
1)"zzl"
2)"18"
3)"yuncheng"
#------------拿到某个hash所有的key------------
127.0.1:6379> hgetall myhash # 拿到某个Hash的所有key
1)"name"
2)"age"
3)"city"
#------------删除某个hash指定的key-------------
127.0.1:6379> hdel myhash name
#------------查看hash的字段数量-------------
127.0.0.1:6379> hlen myhash
(integer) 3
#------------判断hash指定字段是否存在-------------
127.0.0.1:6379> hexists myhash name
(integer) 1
#------------拿到hash所有的字段,值-------------
127.0.0.1:6379> hkeys myhash
1) "name"
2) "age"
3) "city"
127.0.0.1:6379> HVALS myhash
1) "zzl"
2) "18"
3) "yuncheng"
#------------拿到hash字段值自增,自减-------------
127.0.0.1:6379> HINCRBY myhash age 1 # 增1
(integer) 19
127.0.0.1:6379> HINCRBY myhash age -1 # 减1
(integer) 18
#----------不存在则创建赋值,存在则不行----------
127.0.0.1:6379> HSETNX myhash name lisi # name字段存在
(integer) 0
127.0.0.1:6379> HSETNX myhash parent dad # parent字段不存在
(integer) 1
#----------使用hash定义对象----------
127.0.0.1:6379> hset myhash:1 name www age 18
(integer) 0
127.0.0.1:6379> hget myhash:1 name
"www"
127.0.0.1:6379> hget myhash:1 age
"18"
Zset(有序集合)
特点: 在set基础上,增加了一个值,达到排序功能
#----------------------加值 zadd--------------------
127.0.0.1:6379> zadd myset 1 zzl # 添加一个值
(integer) 1
127.0.0.1:6379> zadd myset 2 lisi 3 zs # 添加多个值
(integer) 2
127.0.0.1:6379> ZRANGE myset 0 -1 # 显示myset所有元素
1) "zzl"
2) "lisi"
3) "zs"
#----------------------排序------------------
127.0.0.1:6379> ZRANGEBYSCORE myset -inf +inf # -inf:无穷小 +inf:无穷大
1) "zzl"
2) "lisi"
3) "zs"
127.0.0.1:6379> ZRANGEBYSCORE myset -inf +inf withscores # 增加显示值大小
1) "zzl"
2) "1"
3) "lisi"
4) "2"
5) "zs"
6) "3"
127.0.0.1:6379> ZRANGE myset 0 -1 withscores # 从低到高
1) "zzl"
2) "1"
3) "zs"
4) "3"
127.0.0.1:6379> ZREVRANGE myset 0 -1 withscores # 从高到低
1) "zs"
2) "3"
3) "zzl"
4) "1"
#---------------------获取区间元素 zrange------------
127.0.0.1:6379> ZRANGE myset 1 2
1) "lisi"
2) "zs"
#---------------------移除指定元素 zrem------------
127.0.0.1:6379> ZREM myset lisi
(integer) 1
127.0.0.1:6379> ZRANGE myset 0 -1
1) "zzl"
2) "zs"
#---------------------获取集合元素个数 zcard------------
127.0.0.1:6379> ZCARD myset
(integer) 2
#---------------------两个闭区间内值有几个
127.0.0.1:6379> ZRANGE myset 0 -1 withscores
1) "zzl"
2) "1"
3) "zs"
4) "3"
127.0.0.1:6379> ZCOUNT myset 1 2
(integer) 1
三种特殊类型
geospatial (地理空间)
附近的人,朋友的定位,打车距离计算.
Redis的geospatial 可以推算地理位置信息,两地之间的举例,方圆几里的人
GEO 底层是由 zset 实现的,所以可以使用 zset 操作 GEO
getadd key longitude latitude member [longitude latitude member ...]
有效经度 longitude : -180 ~ 180
有效维度 latitude : -85.05 ~ 85.05
# 添加地理位置
127.0.0.1:6379> geoadd china:city 166.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 160.50 29.53 chongqing
(integer) 1
127.0.0.1:6379> geoadd china:city 144.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 key member [member ...]
127.0.0.1:6379> geopos china:city beijing shanghai # 查看北京,上海的经纬度
1) 1) "166.40000134706497192"
2) "39.90000009167092543"
2) 1) "121.47000163793563843"
2) "31.22999903975783553"
geodist key member1 member2 [unit]
单位 unit:
- m 表示单位为米
- km 表示单位为千米
- mi 表示单位为盈利
- ft 表示单位为英尺
127.0.0.1:6379> geodist china:city beijing shanghai km # 获取北京-上海之间的距离[km]
"4132.6387"
georadius key longitude latitude radius unit [withcoord] [withdist][count count] [asc|desc]
功能: 以给定的经纬度为中心,找出某一半径内的元素
我附近的人? 通过半径查询附近人的定位
127.0.0.1:6379> georadius china:city 110 30 1000 km # [100,20] 为中心,1000km为半径
1) "xian"
2) "hangzhou"
127.0.0.1:6379> georadius china:city 110 30 500 km # [100,20] 为中心,500km为半径
1) "xian"
127.0.0.1:6379> georadius china:city 110 30 500 km withcoord # 显示出经纬度
1) 1) "xian"
2) 1) "108.96000176668167114"
2) "34.25999964418929977"
127.0.0.1:6379> georadius china:city 110 30 500 km withdist # 显示出距离
1) 1) "xian"
2) "483.8340"
127.0.0.1:6379> georadius china:city 110 30 1000 km withcoord asc # 短到长
1) 1) "xian"
2) 1) "108.96000176668167114"
2) "34.25999964418929977"
2) 1) "hangzhou"
2) 1) "120.1600000262260437"
2) "30.2400003229490224"
127.0.0.1:6379> georadius china:city 110 30 1000 km withcoord count 1 # 获取指定数量的人[实用]
1) 1) "xian"
2) 1) "108.96000176668167114"
2) "34.25999964418929977"
georadiusbymember key member radius unit[withcoord][withdist][count count] [asc|desc]
功能: 找出位于指定范围内的元素**,中心点是由给定的位置元素决定**
127.0.0.1:6379> georadiusbymember china:city shanghai 400 km withdist
1) 1) "hangzhou"
2) "166.7613"
2) 1) "shanghai"
2) "0.0000"
GEO 底层是由 zset 实现的,所以可以使用 zset 操作 GEO
127.0.0.1:6379> zrange china:city 0 -1 # 查看地图中所有元素
1) "xian"
2) "hangzhou"
3) "shanghai"
4) "shenzhen"
5) "chongqing"
6) "beijing"
127.0.0.1:6379> zrem china:city beijing # 移除北京位置信息
(integer) 1
127.0.0.1:6379> zrange china:city 0 -1
1) "xian"
2) "hangzhou"
3) "shanghai"
4) "shenzhen"
5) "chongqing"
Hyperloglog
什么是基数?
A{1,3,4,3,4,5} B{1,2,3,4,5}
基数(不重复的元素个数) = 4 当元素达到几万时,可以接收误差
简介
Hyperloglog 是一个基数统计的算法 !
网页的UV(UV是指一个页面的用户访问量。规则是一个人访问一个网站多次,还是计一个人!)
- 传统方式: set 保存用户的id ,然后可以统计 set 中的元素数量作为标准判断
- 以上方式保存了大量的用户id,如果是分布式id,每个id会很长,但是我们的业务不是为了保存用户id,而是简单的计数,所以传统方式不推荐
- 优点:Hyperloglog 占用固定的 12kb 内存,可统计 2^64 不同的用户,算法仅有 0.81% 的错误率
127.0.0.1:6379> pfadd mykey1 1 2 3 4 5 # 创建第一组数据 mykey1
(integer) 1
127.0.0.1:6379> pfadd mykey2 6 7 4 3 0 # 创建第二组数据 mykey2
(integer) 1
127.0.0.1:6379> pfmerge mykey3 mykey1 mykey2 # 合并两组数据,相当于取并集
OK
127.0.0.1:6379> pfcount mykey3 # 查看合并后的
(integer) 8
Bitmaps
位存储
统计用户感染人数: 0 0 0 0 1 0 0 1 0 1 1 1
统计活跃,不活跃,登录,未登录,打卡,
只要像上述两个状态的,都可以使用 Bitmap 来解决,不需要使用mysql表结构来解决
介绍
Bitmap: 位图,都是操作二进制位来进行记录,都只有 0 1 两个状态
统计打卡天数
周一 : 1 周二: 1 周三: 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 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 4 # 查看星期5的打卡情况
(integer) 1
127.0.0.1:6379> BITCOUNT sign # 查看整周打卡次数,业务:全勤
(integer) 4
Redis 事务
Redis 单条命令保持了原子性,但是Redis事务不保证原子性
Redis 事务本质: 一组命令的集合,一个事务中的所有命令都会被序列化,在事务执行过程中,会按照顺序执行.遵循一次性,顺序性,排他性,执行一系列命令.所有命令在事务中,并没有直接被执行,只有发起执行命令的时候才会执行
Redis 事务没有隔离级别的概念,即不会出现幻读,脏读,不可重复读
Redis 的事务:
- 开启事务(
multi
) - 命令入队
- 执行事务(
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> set k3 v3
QUEUED # 命令入队
127.0.0.1:6379> exec # 执行事务
1) OK
2) OK
3) 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 k2 # 事务中的命令都不会得到执行
(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 ke # 错误命令
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379> set k3 k3
QUEUED
127.0.0.1:6379> exec # 执行事务报错,事务队列中所有的命令都不是执行
(error) EXECABORT Transaction discarded because of previous errors.
运行时异常: 如果事务队列中存在语法性错误,那么执行命令的时候,其他命令都可正常执行,错误命令抛出异常
127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379> set k1 "v1"
QUEUED
127.0.0.1:6379> incr k1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> exec # 执行事务
1) OK
2) (error) ERR value is not an integer or out of range
3) OK
127.0.0.1:6379> get k1 # 获取成功
"v1"
127.0.0.1:6379> get k2 # 获取成功
"v2"
Redis 锁
悲观锁 : 无论做什么都加锁
乐观锁 : 认为什么时候都不会出现问题,不会上锁. 更新数据时会判断一下,在此期间是否有人修改过这个数据
- 事先获取 version
- 更新数据时比较 version
开发中一般都是乐观锁,不使用悲观锁
127.0.0.1:6379> watch money # 监视 money,相当于给了一个 money 版本号
127.0.0.1:6379> multi # 开启事务
127.0.0.1:6379> decrby money 10 # 减10
queued
127.0.0.1:6379> incrby out 10 # 多 10
queued
127.0.0.1:6379> exec # 执行之前,另一个线程修改了money,直接导致事务执行失败!
(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 1
queued
127.0.0.1:6379> incrby money 1
queued
127.0.0.1:6379> exec # 执行事务.Redis会比对监视的值是否发生变化,如果未变化执行失败,变化则执行失败,再次unwatch -> watch
1) (integer) 999
2) (integer) 1000
Jedis
介绍
Jedis 是 Redis 官方推荐的 java语言 连接开发工具,是 java 操作 Redis 的中间件.如果要使用 java 操作 redis, 需要对 Jedis 十分数据
-
导入依赖坐标
<dependencies> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>3.2.0</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.62</version> </dependency> </dependencies>
-
连接 redis 数据库
如果jedis连接 linux 上 redis出错,可尝试修改以下配置
-
关闭防火墙
-
修改linux配置文件
- bind 注释掉
- protection-mode : no
- redis 加个密码
public class TestPing { public static void main(String[] args) { // 1. 创建 jedis 对象[填写serverIP、端口号、密码] Jedis jedis = new Jedis("192.168.1.109",6379); jedis.auth("12345"); // 2. jedis 所有命令就是redis的命令操作 // 3. ping 命令 System.out.println(jedis.ping()); } }
-
-
常用方法
SpringBoot 整合 Redis
在 springboot2.x 之后,原来使用的 jedis 换为 lettrce
jedis: 底层采用的是直连,多个线程操作的话,是不安全的,如果想要避免不安全,使用 jedis pool 连接池可解决
lettuce: 底层采用的是 netty,实例可以在多个线程中共享,不存在线程不安全的情况
SpringBoot 所有的配置类,都以一个自动配置类 RedisAutoConfiguration
自动配置类都会绑定一个 properties 配置文件 RedisProperties
-
导入 redis 依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
-
配置连接
# 配置 redis spring.redis.host=192.168.1.109 spring.redis.port=6379 spring.redis.password=12345 spring.redis.database=0
-
**测试 **[使用默认的 RedisTemplate]
@SpringBootTest class Redis02SpringbootApplicationTests { @Autowired RedisTemplate redisTemplate; // 导入 redisTemplate @Test void contextLoads() { // 获取redis连接对象,操作数据库 RedisConnection connection = redisTemplate.getConnectionFactory().getConnection(); connection.flushDb(); // 操作 String 字符串 redisTemplate.opsForValue().set("name","zzl"); System.out.println(redisTemplate.opsForValue().get("name")); // 操作 Hash redisTemplate.opsForHash(); // 操作 List 集合 redisTemplate.opsForList(); // 操作 set 集合 redisTemplate.opsForSet(); // 操作 zset 集合 redisTemplate.opsForZSet(); } }
写一个Redis配置类
-
聊聊RedisTemplate的自动配置
其实现在就可以在代码中注入RedisTemplate,为啥可以直接注入呢?先看下源码吧
@Configuration 2 @ConditionalOnClass(RedisOperations.class) 3 @EnableConfigurationProperties(RedisProperties.class) 4 @Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class }) 5 public class RedisAutoConfiguration { 6 7 @Bean 8 @ConditionalOnMissingBean(name = "redisTemplate") 9 public RedisTemplate<Object, Object> redisTemplate( 10 RedisConnectionFactory redisConnectionFactory) throws UnknownHostException { 11 RedisTemplate<Object, Object> template = new RedisTemplate<>(); 12 template.setConnectionFactory(redisConnectionFactory); 13 return template; 14 } 15 16 @Bean 17 @ConditionalOnMissingBean 18 public StringRedisTemplate stringRedisTemplate( 19 RedisConnectionFactory redisConnectionFactory) throws UnknownHostException { 20 StringRedisTemplate template = new StringRedisTemplate(); 21 template.setConnectionFactory(redisConnectionFactory); 22 return template; 23 } 24 25 }
从以上源码可以看出: SpringBoot 自动帮我们在容器中生成了一个 RedisTemplate 和 StringRedisTemplate .但是,这个RedisTemplate的泛型是**<Object,Object>,写代码很不方便,学要写很多类型转换的代码,我们需要一个泛型为<String,Object>形式的RedisTemplate**.并且这个RedisTemplate没有设置数据存在Redis时,key及value的序列化方式。
看到这个**@ConditionalOnMissingBean注解后,就知道如果Spring容器中有了RedisTemplate对象了,这个自动配置的RedisTemplate不会实例化**。因此我们可以直接自己写个配置类,配置RedisTemplate。
-
自动配置不好用,就重新设置一个RedisTemplate
注意设置下 key 和 value 为其他序列化方式,不然存到Redis的中数据看起来像乱码一下 [ 默认自带的RediseTemplate采用JDK序列化方式 ]
可选的序列化方式:
jackson
.String
,JDKSerial
// 自定义 RedisTemplate @Bean public RedisTemplate<String , Object> redisTemplate(RedisConnectionFactory factory) throws UnknownHostException { // 一般直接使用 <String,Object> RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); // 配置 jackson 序列化方式 Jackson2JsonRedisSerializer<Object> objectJackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class); ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); objectJackson2JsonRedisSerializer.setObjectMapper(om); // 配置 String 序列化方式 StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); /* 所有的key采用String序列化,value采用jackson序列化 */ // key 采用 String 的序列化方式 template.setKeySerializer(stringRedisSerializer); // value 采用 jackson 的序列化方式 template.setValueSerializer(objectJackson2JsonRedisSerializer); // hash 的 key 采用 String 的序列化方式 template.setHashKeySerializer(stringRedisSerializer); // hash 的 value 采用 jackson 序列化方式 template.setHashValueSerializer(objectJackson2JsonRedisSerializer); template.afterPropertiesSet(); return template; }
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u7D8H81s-1655629331040)(E:\note\holiday\Redis\img\img42.jpg)]
写一个Redis工具类
直接用RedisTemplate操作Redis,需要很多行代码,因此直接封装好一个RedisUtils,这样写代码更方便点。这个RedisUtils交给Spring容器实例化,使用时直接注解注入。
使用时需要用 @Autowired 注入工具类
/**
* 在公司中,大部分都会封装自己的redis工具类
*/
@Component
public class RedisUtils {
@Autowired
private RedisTemplate redisTemplate;
// =============================common============================
/**
* 指定缓存失效时间
* @param key 键
* @param time 时间(秒)
* @return
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.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 redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
}
// ============================String=============================
/**
* 普通缓存获取
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
0
* 普通缓存放入并设置时间
0
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
0
* 递增
* @param key 键
* @param delta 要增加几(大于0)
* @return
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
* @param key 键
* @param delta 要减少几(小于0)
* @return
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
// ================================Map=================================
/**
* HashGet
* @param key 键 不能为null
* @param item 项 不能为null
0
* @return 值
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
* @param key 键
* @return 对应的多个键值
0
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
* @param key 键
* @param map 对应多个键值
* @return true 成功 false 失败
0
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashSet 并设置时间
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.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 {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
0
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
0
* 删除hash表中的值
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
0
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
0
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
* @return
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
0
* hash递减
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
* @return
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
// ============================set=============================
/**
* 根据key获取Set中的所有值
* @param key 键
* @return
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
0
* 根据value从一个set中查询,是否存在
0
* @param key 键
0
* @param value 值
0
* @return true 存在 false不存在
0
*/
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将数据放入set缓存
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 将set数据放入缓存
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
0
* @return 成功个数
*/
public long sSetAndTime(String key, long time, Object... values) {
try {
Long count = redisTemplate.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 redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 移除值为value的
0
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object... values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
// ===============================list=================================
/**
* 获取list缓存的内容
* @param key 键
* @param start 开始
* @param end 结束 0 到 -代表所有值
0
* @return
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
0
* 获取list缓存的长度
0
* @param key 键
0
* @return
0
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 通过索引 获取list中的值
* @param key 键
* @param index 索引 index>=0时, 0 表头, 第二个元素,依次类推;index<0时,-,表尾,-倒数第二个元素,依次类推
* @return
0
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
* @param key 键
0
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.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 {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
0
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0)
expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
0
* 根据索引修改list中的某条数据
0
* @param key 键
0
* @param index 索引
0
* @param value 值
0
* @return
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 移除N个值为value
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
try {
Long remove = redisTemplate.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
}
Redis.conf 配置文件详解
- 大小写不敏感
- 可以包含多个配置文件,进行整合
高级篇-redis
Redis 持久化
RDB持久化
RDB优缺点
AOF持久化
将我们所有命令都记录下来, 有一个history文件,恢复的时候就把这个文件里的命令全部执行一遍 !
总结
1 RDB持久化方式 能够在指定时间间隔内对你的数据进行快照存储
2 AOF 持久化方式记录每次对服务器的写操作,当服务器重启的时候会重新执行这些命令来恢复原始数据,AOF命令以Redis协议追加保存每次写的操作到文件末尾
3 若只做缓存,如果你希望你的数据在服务器运行的时候存在,你可以就不需要任何持久化
4 同时开启两种持久化方式
- 在两种情况下,当redis重启会优先加载AOF文件来恢复原始数据,通常比RDB保存的数据要完整
- 作者不建议使用AOF,因为RDB更适合备份数据库(AOF不断变化不好备份)
因为RDB文件只做后备用途,建议只在从机上持久化RDB文件,而且只要15分钟备份一次就够了,只保留save 900 1 这条规则
Redis 发布订阅
主从复制
概念
主从复制,读写分离! 80%情况下都是进行读操作!减缓服务器的压力!架构中常常使用! 最少需要 >=3 台 redis 服务器
复制原理
假集群环境搭建
只用配置从库,主库不用额外配置
查看当前redis的角色:
由于只有一台server, 只能复制并修改三个配置文件, 来模拟假的集群
修改对应信息:
- 端口名
- pid名
- log文件名
- dump.rdb名
修改完毕后,启动三个redis服务:
主从复制配置
从机 配置:
主机 配置:
注意:
上述是用命令行配置的从机, 如果"从机"shutdown, 重启后配置会重置.
只有直接在配置文件中配置, 才是永久配置
注意
层层链路集群
哨兵模式[重点]
概念
哨兵模式: 自动选取老大
哨兵配置
测试
我们目前的状态是:一主二从
-
配置哨兵配置文件 sentinel.conf
# sentinel monitor 被监控的名称 Host post 1 sentinel monitor myredis 127.0.0.1 6379 1
后面的数字1,代表: 若主机挂了, slave投票看谁接替成为主机
服务高可用问题
缓存穿透(查不到)
解决方案
1、布隆过滤器
2、缓存空对象
当缓存不命中,即使返回的空对象也将其缓存起来,同时设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了后端数据源
缓存击穿(量太大,缓存过期)
缓存过期》?是因为redis缓存可能会满,所以必须定期进行清理出一部分key,来存别的