Redis
1.NoSQL
NoSQL = Not Only SQL
泛指非关系型数据库
2.NoSQL的四大分类
-
KV键值对
- 新浪:Redis
- 美团:Redis + Tair
-
文档型数据库(bson格式和json格式一样)
- MongoDB(一般必须要掌握),基于分布式文件存储的数据库,c++编写,主要用来处理大量的文档
- MongoDB世界语关系型数据库和非关系型数据库中间的产品,MongoDB是非关系型数据库中功能最丰富,最像关系型数据库的
- ConthDB
-
列存储数据库
- HBase(需要掌握)
- 分布式文件系统
-
图关系数据库
- 不是存储图形,存放的是关系,比如朋友圈社交网络,广告推荐
- Neo4j(需要掌握),InfoGrid
3.Redis概述
-
Redis:(Remote Dictionary Server),即远程字典服务
-
Redis做什么:
- 内存存储,持久化,内存是断电即失,所以说持久化很重要(rdb,aof)
- 效率高,可用于高速缓存
- 发布订阅系统(可以进行一些简单的消息队列)
- 地图信息分析
- 计时器,计数器(浏览量)
-
Redis的基本命令使用
set name wang //设置键值对
get name //获取key对应的值
keys * //查看所有的key
select 3 //选择3号数据库,redis默认有16个数据库
dbsize //查看数据库的大小
flushdb //清空当前数据库
flushall //清空所有数据库
4.五大数据类型
- Redis-key
set name wang
type name //查看当前类的一个类型
keys *
get name
exists name
move name
expire name 10 //设置过期时间为10s
ttl name //可以查看剩余多长时间过期
- 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"
2) "name"
127.0.0.1:6379> exists key1 #判断某一个key是否存在
(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> getranger key1 0 3 #截取字符串
127.0.0.1:6379> getranger key1 0 -1 #获取全部字符串
127.0.0.1:6379> setranger key1 1 xx #从字符串下标为1的位置开始,替换下表2和3的字符串
127.0.0.1:6379> set views 0
OK
127.0.0.1:6379> get views #增加1
"0"
127.0.0.1:6379> incr views
(integer) 1
127.0.0.1:6379> get views
"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 #增加10
(integer) 10
127.0.0.1:6379> get views
"10"
127.0.0.1:6379> incrby views 10
(integer) 20
127.0.0.1:6379> decrby views 5 #减少5
(integer) 15
setex (set with expire) #设置过期时间
setnx (set if not exist) #不存在设置(在分布式锁会常常使用)
#批量设置值
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3 #批量设置值
OK
127.0.0.1:6379> keys *
1) "views"
2) "k3"
3) "k1"
4) "k2"
127.0.0.1:6379> mget k1 k2 k3 #批量获取值
1) "v1"
2) "v2"
3) "v3"
#对象
set user:1 {name:zhangsan,age:3} #设置一个user:1对象,值为json字符来保存一个对象
127.0.0.1:6379> set user:1 {name:zhangsan,age:3}
OK
127.0.0.1:6379> get user:1
"{name:zhangsan,age:3}"
#这里的key是一个巧妙的设计: user:{id}:{filed}.如此设计在Redis中是完全OK了
127.0.0.1:6379> mset user:2:name zhangsan user:2:age 18
OK
127.0.0.1:6379> mget user:2:name user:2:age
1) "zhangsan"
2) "18"
#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 mongodb #如果存在值,获取原来的值,并设置新的值
"redis"
127.0.0.1:6379> get db
"mongodb"
- List
- 我们可以把list玩成栈、队列、阻塞队列
- 所有的list命令都是L开头的
# 将一个或者多个值插入到列表头部(左)
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
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 right
(integer) 4
127.0.0.1:6379> LRANGE list 0 -1
1) "three"
2) "two"
3) "one"
4) "right"
127.0.0.1:6379>
#移除元素
Lpop list
Rpop list
#通过下标获取某一个值
lindex list 1
- set集合,无序,不可重复
sadd myset "hello"
- Hash哈希类型
127.0.0.1:6379> hset myhash field1 wang #set一个具体的key-value
(integer) 1
127.0.0.1:6379> hget myhash field1
"wang"
- Zset(有序集合)
在set的基础上,增加了一个值,set k1 v1 zset k1 score1 v1
127.0.0.1:6379> zadd myset 1 one
(integer) 1
127.0.0.1:6379> zadd myset 2 two 3 three
(integer) 2
127.0.0.1:6379> zrange myset 0 -1
1) "one"
2) "two"
3) "three"
5.三种特殊类型
(1)geospatial 地理位置(底层实现原理是Zset),我们可以使用Zset命令来操作geo
- 可以实现附近的人业务
# 添加地理位置
# 我们一般会下载城市数据,直接通过Java程序一次性导入
127.0.0.1:6379> geoadd china:city 116.40 39.90 beijing
#获取城市的位置,先经度后纬度
127.0.0.1:6379> geopos china:city beijing
1)116.40
2)39.90
#两个城市之间的直线距离
geodist china:city beijing chongqing #单位默认是M
geodist china:city beijing chongqing KM #指定单位为KM mi为英里 ft表示为英尺
#以110 30这个经纬度为中心,寻找方圆1000KM内的城市
127.0.0.1:6379> georadius china:city 110 30 1000 km
#找出以上海为中心周围400km的城市
127.0.0.1:6379> georadiusbymember china:city shanghai 400 km
#底层实现原理是Zset
#查看录入的所有城市
127.0.0.1:6379> zrange china:city 0 -1
#移除某个指定的城市
127.0.0.1:6379> zrem china:city beijing
(2)Hyperloglog
什么是基数?
A{1,3,5,7,8,7}
B{1,3,5,7,8}
基数(不重复的元素)= 5,可以接受误差
- 网站的UV(一个人访问一个网站多次,但是还算做一个人)
127.0.0.1:6379> PFadd mykey a b c d e f g a b c
#统计基数的个数
127.0.0.1:6379> PFCOUNT mykey
(integer) 9
(3)Bitmaps
统计用户信息,活跃,不活跃!登录,未登录,两个状态的,都可以使用Bitmaps
统计七天打卡信息
127.0.0.1:6379> setbit sign 0 1
127.0.0.1:6379> setbit sign 1 1
127.0.0.1:6379> setbit sign 2 0
127.0.0.1:6379> setbit sign 3 1
127.0.0.1:6379> setbit sign 4 1
127.0.0.1:6379> setbit sign 5 0
127.0.0.1:6379> setbit sign 6 1
127.0.0.1:6379> getbit sign 6
(integer) 1
6.Redis事务
127.0.0.1:6379> multi #开启事务
OK
127.0.0.1:6379> set k1 v1 #入队
QUEUED
127.0.0.1:6379> get k1
QUEUED
127.0.0.1:6379> exec #执行事务
1) OK
2) "v1"
127.0.0.1:6379> multi #开启事务
OK
127.0.0.1:6379> set k1 v1 #入队
QUEUED
127.0.0.1:6379> DISCARD #取消事务
OK
# 如果事务编写时中间有编译时的错误入队指令,那么这个事务不会成功
# 运行时有错误,错误的不会执行,其它正常可以执行
# 使用watch可以当作redis的乐观锁实现
7.Jedis
我们要使用Java操作Redis
- Jedis:是Redis官方推荐的Java连接开发工具,使用Java操作Redis中间件
1.导入对应的依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
2.Jedis连接Redis
Jedis jedis = new Jedis("127.0.0.1",6379);
System.out.println(jedis.ping()); //PONG
Jedis jedis = new Jedis("127.0.0.1",6379);
System.out.println(jedis.ping()); //PONG
System.out.println("清空数据:" + jedis.flushDB());
System.out.println("判断某个建是否存在" + jedis.exists("username"));
System.out.println("新增<'username','admin'>的键值对" + jedis.set("username","admin"));
System.out.println("新增<'username','password'>的键值对" + jedis.set("password","123546"));
Set<String> keys = jedis.keys("*");
System.out.println(keys);
System.out.println("删除键password" + jedis.del("password"));
System.out.println("查看键username所存储值的类型" + jedis.type("username"));
System.out.println("重命名key" + jedis.rename("username","name"));
System.out.println("删除当前选择数据库的所有key" + jedis.flushDB());
System.out.println("返回当前数据库中的所有key" + jedis.dbSize());
System.out.println("删除所有数据库中的所有key" + jedis.flushAll());
jdesi.close();//关闭连接
3.Jedis测试Redis事务
Jedis jedis = new Jedis("127.0.0.1",6379);
JSONObject jsonObject = new JSONObject();
jsonObject.put("user1","wyf");
jsonObject.put("user2","csq");
System.out.println("jsonObject:"+jsonObject);
System.out.println(jsonObject.toJSONString());
//开启事务
Transaction multi = jedis.multi();
String result = jsonObject.toJSONString();
jedis.watch("user1");//通过watch实现乐观锁
try {
multi.set("user1",result);
multi.set("user2",result);
//成功执行事务
multi.exec();
} catch (Exception e) {
//失败放弃事务
multi.discard();
e.printStackTrace();
} finally {
System.out.println("user1:" + jedis.get("user1"));
System.out.println("user2:" + jedis.get("user2"));
jedis.close();
}
8.SpringBoot整合Redis
说明:在spring boot2.x之后,原来使用的jedis被替换为了lettuce
jedis:采用的直连,多个线程操作的话,是不安全的,如果想避免不安全,使用jedis pool连接池,更像BIO模式
BIO: 同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即用户端有连接请求时服务器就需要启动一个线程进行处理,如果这个连接不做任何事情就会造成不必要的线程开销
lettuce:采用netty,实例可以在多个线程中进行共享,不存在线程不安全的情况,可以减少线程数据,更像NIO模式
NIO: 同步非阻塞,服务器实现模式为一个线程处理多个请求连接,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询连接有I/O请求就进行处理
1.导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2.配置依赖
redis:
host: 127.0.0.1
port: 6379
3.测试
@Autowired
private StringRedisTemplate stringRedisTemplate;
调用StringRedisTemplate中的API
9.Redis持久化
Redis是内存数据库,一旦服务器进程退出,服务器中的数据库状态也会消失,所以Redis提供了持久化功能!
1.RDB(Redis DataBase)
即内存快照,也是全量快照,当服务器宕机时,Redis中存储的数据就会丢失。这个时候就需要内存快照来恢复Redis中的数据了。
Redis提供了两个命令来生产全量的RDB文件,一个是 save ,另一个是 bgsave。
save:在主线程中执行,会导致阻塞;
bgsave:创建一个子进程,专门用于写入 RDB 文件,避免了主线程的阻塞,这也是 Redis RDB 文件生成的默认配置。
不用想也知道,我们应该使用哪种方式来生成RDB文件了。
增量快照,就是指,做了一次全量快照后,后续的快照只对修改的数据进行快照记录,这样可以避免每次全量快照的开销。在第一次做完全量快照后,T1 和 T2 时刻如果再做快照,我们只需要将被修改的数据写入快照文件就行。但是,这么做的前提是,我们需要记住哪些数据被修改了。
2.AOF(Append Only File)
将我们所有的命令都记录下来,保存的是appendonly.aof文件,aof文件随着指令变多,文件大小越来越大,修复的速度也比rdb慢,所以默认的配置是rdb持久化,恢复的时候重新写入aof文件,不适合大数据量。
10.Redis订阅发布
//订阅一个频道,等待推送的信息
127.0.0.1:6379> subscribe wyf
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "wyf"
3) (integer) 1
1) "message" #消息
2) "wyf" #频道名字
3) "hello" #消息的具体内容
1) "message"
2) "wyf"
3) "hello"
1) "message"
2) "wyf"
3) "hello11"
//发布者像频道发送消息
127.0.0.1:6379> publish wyf "hello"
(integer) 1
127.0.0.1:6379> publish wyf "hello"
(integer) 1
127.0.0.1:6379> publish wyf "hello11"
(integer) 1
应用场景:
1.实时聊天系统
2.订阅、关注
稍微复杂的情景我们使用消息中间件MQ
11.Redis主从复制
主机只能写,从机只能读
主机断开连接,从机依旧能够连接到主机,但是读取不到主机内容,如果主机返回,从机依旧可以直接获取到主机写的信息
全量复制:slave服务在接收到数据库文件数据后,将其存盘并加载到内存中(只要重新连到master,全量复制将自动被执行)
增量复制:master继续将新的收集到的命令一次传给slave,完成同步
M—> S/M—>S,中间的既可以当前一个的slave,也可以当下一个的master(层层链路)
如果没有老大(master)了,能不能选择出一个老大(master)呢,手动(哨兵模式未出之前)
如果主机断开了连接,使用 slave of no one让自己变为master
12.Redis哨兵模式(自动选取master)
测试:
目前场景一主二从
1.配置哨兵模式配置文件sentinel.conf
#sentinel monitor 被监控的名称 host port 1
sentinel monitor myredis 127.0.0.1 6379 1 #这个1代表如果主机挂了,slave投票选举新的主机
2.启动哨兵
redis-sentinel sentinel.conf
13.Redis的缓存穿透和雪崩(面试高频)
定义:用户想要查询一个数据,发现redis内存数据库没有,也就是缓存没有命中,于是向持久层数据库查询,发现也没有,于是此次查询失败。当用户很多的时候,缓存都没有命中(秒杀!),于是都去请求了持久层数据库,这给持久层数据库造成很大压力,这时候就相当于出现了缓存穿透。
-
缓存穿透的解决方案
- 布隆过滤器
- 缓存空对象(缺点是存储了很多空值的键,即使对空值设置了国企时间,还是会存在缓存层和存储层的数据有一点时间窗口的不一致)
-
缓存击穿(量太大,缓存过期,空档期请求全部砸到数据库)
缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key瞬间失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开一个洞。
-
缓存击穿解决方案
-
设置热点数据永不过期
从缓存面来看,没有设置过期时间,所以不会出现热点key过期后产生的问题。
-
加互斥锁
分布式锁:使用分布式锁,保证每一个key同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只需要等待即可,这种方式将高并发的压力转到了分布式锁,因此对分布式锁的考验很大。
-
-
缓存雪崩
定义:在某一个时间段,缓存集中过期失效,Redis宕机
-
缓存雪崩解决方案
-
redis高可用
redis有可能挂掉,多设几台redis,一台挂掉之后其它的还可以继续工作,其实就是搭建集群(异地多活)
-
限流降级(SpringCould中提到)
缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量,比如对某个key只允许一个线程查询和写缓存,其它线程等待。
-
数据预热
在正式部署之前,把数据先预先访问一遍,这样大部分访问的数据就会加载到缓存中,在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间尽量均匀。
-