1.Redis是什么?有什么优缺点?
Redis本质上是一个Key-Value类型的非关系型内存数据库。
优点:
- 读写性能极高, Redis能读的速度是110000次/s,写的速度是81000次/s。
- 支持数据持久化,支持AOF和RDB两种持久化方式。
- 支持事务, Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。
- 数据结构丰富,除了支持string类型的value外,还支持hash、set、zset、list等数据结构。
- 支持主从复制,主机会自动将数据同步到从机,可以进行读写分离。
缺点: - 数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。
- 主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题,降低了系统的可用性。
2.Linux系统下启动和关闭Redis
- Linux系统中启动redis:
-step1:cd /usr/local/bin;
-step2:redis-server /etc/redis.conf;(server后有一个空格!)
-step3:redis-cli,通过客户端连接redis。 - Linux系统中关闭redis:
-shutdown
-找到进程号,用kill命令。
3.为什么要用 Redis 做缓存?
从高并发上来说:
- 直接操作缓存能够承受的请求数是远远大于直接访问数据库的,将数据库中的部分数据转移到缓存中去,用户的部分请求会直接到缓存而不用经过数据库。
从高性能上来说:
- 用户第一次访问数据库中的某些数据时, 因为是从硬盘上读取的所以这个过程会比较慢。将该用户访问的数据存在缓存中,下一次再访问这些数据的时候就可以直接从缓存中获取了。
4.Redis 的数据类型有哪些?
五种常用数据类型:
- String
Redis 最基本数据类型。
常用方法:
set <key><value> 添加键值对
get <key> 查询对应键值
append <key><value> 将给定的<value> 追加到原值的末尾,返回字符串的长度
strlen <key> 获得值的长度
setnx <key><value> 只有在 key 不存在时 设置 key 的值
incr <key> 将key 中储存的数字值增1(只能对数字值操作,如果为空,新增值为1)
decr <key> 将 key 中储存的数字值减1(只能对数字值操作,如果为空,新增值为-1)
incrby / decrby <key><步长> 将 key 中储存的数字值增减。自定义步长。
mset <key1><value1><key2><value2> ..... 同时设置一个或多个 key-value对
mget <key1><key2><key3> ..... 同时获取一个或多个 value
msetnx <key1><value1><key2><value2> .....
同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在。
getrange <key><起始位置><结束位置> 获得值的范围,类似java中的substring
setrange <key><起始位置><value> 用 <value> 覆写<key>所储存的字符串值,从<起始位置>开始(索引从0开始)。
setex <key><过期时间><value> 设置键值的同时,设置过期时间,单位秒。
getset <key><value> 以新换旧,设置了新值同时获得旧值。
- List
Redis 列表是简单的字符串列表,按照插入顺序排序。可以在列表的头部和尾部添加元素。
常用方法:
lpush/rpush <key><value1><value2><value3> .... 从左边/右边插入一个或多个值。
lpop/rpop <key> 从左边/右边吐出一个值。值在键在,值光键亡。
rpoplpush <key1> <key2> 从<key1> 列表右边吐出一个值,插到<key2>列表左边。
lrange <key> <start> <stop> 按照索引下标获得元素(从左到右)
lindex <key><index> 按照索引下标获得元素(从左到右)
llen <key> 获得列表长度
linsert <key> before <value> <newvalue> 在<value>的前面插入<newvalue>插入值
linsert <key> after <value> <newvalue> 在<value>的后面插入<newvalue>插入值
lrem <key><n><value> 从左边删除n个value(从左到右)
lset<key><index><value> 将列表key下标为index的值替换成value
- Set
Set集合中的元素 是无序的,存取无序,且元素不能重复。
常用方法:
sadd <key><value1><value2> .... 将一个或多个 member 元素加入到集合 key 中,已经存在的 member 元素将被忽略
smembers <key> 取出该集合的所有值。
sismember <key><value> 判断集合<key>是否为含有该<value>值,有1,没有0
scard<key> 返回该集合的元素个数。
srem <key><value1><value2> .... 删除集合中的某个元素。
spop <key> 随机从该集合中吐出一个值。
srandmember <key><n> 随机从该集合中取出n个值。不会从集合中删除 。
smove <原set><目标set>value 把集合中一个值从一个集合移动到另一个集合
sinter <key1><key2> 返回两个集合的交集元素。
sunion <key1><key2> 返回两个集合的并集元素。
sdiff <key1><key2> 返回两个集合的差集元素(key1中的,不包含key2中的)
- Hash
Redis hash 是一个键值对集合。
Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象。
类似Java里面的Map<String,Object>。
常用方法:
hset <key><field><value> 给<key>集合中的<field>键赋值<value>
hget <key1><field> 从<key1>集合<field>取出 value
hexists<key1><field> 查看哈希表 key 中,给定域 field 是否存在。
hkeys <key> 列出该hash集合的所有field
hvals <key> 列出该hash集合的所有value
hincrby <key><field><increment> 为哈希表 key 中的域 field 的值加上增量 1 -1
hsetnx <key><field><value> 将哈希表 key 中的域 field 的值设置为 value ,当且仅当域 field 不存在 .
hmset <key1><field1><value1><field2><value2>... 批量设置hash的值
- Zset
有序、无重复元素,每个成员关联了一个评分(score)。集合的成员是唯一的,但是评分可以相同。
可以根据评分(score)或者次序(position)来获取一个范围内的元素。类似java中的TreeSet。
常用方法:
zadd <key><score1><value1><score2><value2>… 将一个或多个 member 元素及其 score 值加入到有序集 key 当中。
zrange <key><start><stop> [WITHSCORES] 返回有序集 key 中,下标<start><stop>之间的元素,带WITHSCORES,可以让分数一起和值返回到结果集。
zrange key 0 -1 取出所有,不包括分数
zrange key 0 -1 withscores 取出所有,包括分数
zrangebyscore key minmax [withscores] [limit offset count]
返回有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员。有序集成员按 score 值递增(从小到大)次序排列。
zrevrangebyscore key maxmin [withscores] [limit offset count]
同上,改为从大到小排列。
zincrby <key><increment><value> 为元素的score加上增量
zrem <key><value> 删除该集合下,指定值的元素
zcount <key><min><max> 统计该集合,分数区间内的元素个数
zrank <key><value> 返回该值在集合中的排名,从0开始。
三种特殊数据类型:
- Bitmap:位图,Bitmap想象成一个以位为单位数组,数组中的每个单元只能存0或者1。使用Bitmap实现统计功能,更省空间。
示例:每个独立用户是否访问过网站存放在Bitmaps中, 将访问的用户记做1, 没有访问的用户记做0, 用偏移量作为用户的id。
setbit<key><offset><value> 设置Bitmaps中某个偏移量的值(0或1)
getbit<key><offset>获取Bitmaps中某个偏移量的值
bitcount<key>[start end] 统计字符串从start字节到end字节比特值为1的数量
bitop and(or/not/xor) <destkey> [key…] 它可以做多个Bitmaps的and(交集) 、 or(并集) 、 not(非) 、 xor(异或) 操作并将结果保存在destkey中。
- Hyperloglog。HyperLogLog 是一种用于统计基数的数据集合类型。例如统计网页的UV(即Unique Visitor,不重复访客,一个人访问某个网站多次,但是还是只计算为一次)。
pfadd <key> <element> 添加指定元素到 HyperLogLog 中
pfcount <key> 计算HLL的近似基数
pfmerge <destkey> <sourcekey> 将一个或多个HLL合并后的结果存储在另一个HLL中,比如每月活跃用户可以使用每天的活跃用户来合并计算可得
- Geospatial :主要用于存储地理位置信息,并对存储的信息进行操作,适用场景如朋友的定位、附近的人、打车距离计算等。
geoadd <key> (geoadd china:city 121.47 31.23 shanghai)
geopos [member…] 获得指定地区的坐标值
geodist [m|km|ft|mi ] 获取两个位置之间的直线距离
georadius< longitude>radius m|km|ft|mi 以给定的经纬度为中心,找出某一半径内的元素
5.Idea中使用Redis
5.1导入Jedis的jar包
jedis 是redis的java客户端,通过它可以对redis进行操作。
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.2.0</version>
</dependency>
5.2连接Redis的注意事项
- redis.conf 文件中注释掉 bind 127.0.0.1;同时将protected-mode设置为 no;
- 禁用Linux的防火墙:ctrl+c 退出redis客户端,systemctl stop firewalld.service;
5.3 连接Redis
package com.atguigu.jedis;
import redis.clients.jedis.Jedis;
public class Demo01 {
public static void main(String[] args) {
Jedis jedis = new Jedis("192.168.137.3",6379);
String pong = jedis.ping();
System.out.println("连接成功:"+pong);
jedis.close();
}
}
-运行上述代码,若连接成功,输出:连接成功:PONG
5.4 Jedis操作几种数据类型的常用操作
package com.xk.jedis;
import org.junit.jupiter.api.Test;
import redis.clients.jedis.Jedis;
import java.util.List;
import java.util.Set;
public class JedisDemo1 {
public static void main(String[] args) {
Jedis jedis = new Jedis("192.168.112.129",6379);
String pong = jedis.ping();
System.out.println("连接成功:"+pong);
jedis.close();
}
@Test
public void demo5() {
//创建Jedis对象
Jedis jedis = new Jedis("192.168.112.129",6379);
jedis.zadd("china",100d,"shanghai");
Set<String> china = jedis.zrange("china", 0, -1);
System.out.println(china);//输出:[shanghai]
jedis.close();
}
//操作hash
@Test
public void demo4() {
//创建Jedis对象
Jedis jedis = new Jedis("192.168.112.129",6379);
jedis.hset("users","age","20");
String hget = jedis.hget("users", "age");
System.out.println(hget);//输出:20
jedis.close();
}
//操作set
@Test
public void demo3() {
//创建Jedis对象
Jedis jedis = new Jedis("192.168.112.129",6379);
jedis.sadd("names","lucy");
jedis.sadd("names","mary");
Set<String> names = jedis.smembers("names");
System.out.println(names);//输出:[mary, lucy]
jedis.close();
}
//操作list
@Test
public void demo2() {
//创建Jedis对象
Jedis jedis = new Jedis("192.168.112.129",6379);
jedis.lpush("key1","lucy","mary","jack");
List<String> values = jedis.lrange("key1", 0, -1);
System.out.println(values);//输出:[jack, mary, lucy]
jedis.close();
}
//操作key string
@Test
public void demo1() {
//创建Jedis对象
Jedis jedis = new Jedis("192.168.112.129",6379);
//添加
jedis.set("name","lucy");
//获取
String name = jedis.get("name");
System.out.println(name);//输出:lucy
//设置多个key-value
jedis.mset("k1","v1","k2","v2");
List<String> mget = jedis.mget("k1", "k2");
System.out.println(mget);//输出:[v1, v2]
Set<String> keys = jedis.keys("*");
for(String key : keys) {
System.out.println(key);//依次输出:name key1 names china k1 users k2
}
jedis.close();
}
}
6.Redis 事务
6.1 Redis事务的概念
Redis事务可以理解为一个打包的批量执行脚本,批量指令并非原子化操作,中间某条指令失败不会影响后续指令的执行。
6.2 Redis事务的执行步骤
- multi 开启事务
- 大量指令入队,进行组队,组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消。
- discard ,组队的过程中可以通过discard来放弃组队
- exec执行事务块内命令,执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。
- watch 监视一个或多个key,如果事务执行前key被改动,事务将打断。unwatch 取消监视。
7.悲观锁和乐观锁(Redis采用乐观锁)
- 悲观锁:悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。一个一个的执行,效率低。
- 乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。每次操作完数据后给数据加版本号,版本号不一致,则无法操作。
8.Redis 的持久化机制
持久化定义:将Redis中的数据写入到磁盘空间中。
持久化的方法:
- 快照(RDB Redis DataBase):在指定的时间间隔内将内存中的数据集快照写入磁盘。(适用于数据备份)。
底层采用写时复制技术。Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到 一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。 - 只追加文件(AOF Append Only File):以日志的形式来记录每个写操作,将Redis执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件。(保证数据不丢失)
- 两种持久化方式可同时开启,系统默认读取AOF的数据(保证数据不丢失)。
9.Redis 主从复制(多副本)
定义:主机数据更新后根据配置和策略, 自动同步到备机的master/slaver机制,Master以写为主,Slave以读为主
优点:读写分离、容灾快速回复(其中一台从机故障时,其他从机可以快速切换继续提供服务)。
主从复制原理:
- 1.当从服务器连上主服务器后,从服务器向主服务器发送PSYNC命令进行数据同步。
- 2 主服务器接到从服务器发送来的同步消息后,把主服务器数据进行持久化,rdb文件,把rdb文件发送到服务器,从服务器拿到rdb后进行读取。
- 3 每次主服务器进行写操作之后,从服务器会进行数据同步(增量复制)。
- 从服务器重新连接主服务器,会进行一次全量复制。
10.Redis 哨兵(Redis Sentinel)
10.1 哨兵定义:
当一个master宕机后,后面的slave根据投票数自动将从机转换为主机。
10.2 哨兵配置
自定义的/myredis目录下新建sentinel.conf文件,配置哨兵,填写内容:
sentinel monitor mymaster 127.0.0.1 6379 1
其中mymaster为监控对象起的服务器名称, 1 为至少有多少个哨兵同意迁移的数量。
10.3 启动哨兵
执行如下代码:
redis-sentinel /myredis/sentinel.conf
11.Redis 集群(Redis Cluster)
11.1 集群的定义
- Redis 集群实现了对Redis的水平扩容,即启动N个redis节点,将整个数据库分布存储在这N个节点中,每个节点存储总数据的1/N。
- Redis 集群通过分区(partition)来提供一定程度的可用性(availability): 即使集群中有一部分节点失效或者无法进行通讯, 集群也可以继续处理命令请求。
11.2 集群的作用
主要解决Redis分布式方面的需求,比如,当遇到单机内存,并发和流量等瓶颈的时候,Redis Cluster能起到很好的负载均衡的目的。
11.3 集群的设置参数
- 节点(Nodes):最小配置6个(三主三从),主节点提供读写操作,从节点作为备用,作为故障转移使用。
- 虚拟槽分区(Slots):一个 Redis 集群包含 16384 个插槽(hash slot),数据库中的每个键都属于这 16384 个插槽的其中一个。集群中的每个节点负责处理一部分插槽。 举个例子, 如果一个集群可以有主节点, 其中:
节点 A 负责处理 0 号至 5460 号插槽。
节点 B 负责处理 5461 号至 10922 号插槽。
节点 C 负责处理 10923 号至 16383 号插槽。
11.4 故障恢复
redis.conf中的参数 cluster-require-full-coverage
- 如果某一段插槽的主从都挂掉,而cluster-require-full-coverage 为yes ,那么 ,整个集群都挂掉。
- 如果某一段插槽的主从都挂掉,而cluster-require-full-coverage 为no ,那么,该插槽数据全都不能使用,也无法存储。
11.5 Jedis中使用集群
public class JedisClusterTest {
public static void main(String[] args) {
Set<HostAndPort>set =new HashSet<HostAndPort>();
set.add(new HostAndPort("192.168.112.129",6379));
JedisCluster jedisCluster=new JedisCluster(set);
jedisCluster.set("k1", "v1");
System.out.println(jedisCluster.get("k1"));//输出v1
}
}
11.Redis 缓存异常
11.1 缓存不一致
- 定义:使用到缓存,无论是本地内存做缓存还是使用 Redis 做缓存,那么就会存在数据同步的问题,因为配置信息缓存在内存中,而内存时无法感知到数据在数据库的修改。这样就会造成数据库中的数据与缓存中数据不一致的问题。
- 解决方案
- 先更新数据库,后更新缓存
- 先更新缓存,后更新数据库
- 先删除缓存,后更新数据库
- 先更新数据库,后删除缓存
11.2 缓存穿透
- 出现很多非常的url访问,应用服务器压力变大
- redis命中率降低,redis查询不到数据库
- 一直查询数据库
解决方案:
-
对空值进行缓存
-
设置访问白名单
-
实时监控
11.3 缓存击穿
定义: redis某个热点key过期,大量访问这个key,大量的请求访问数据库,导致数据库奔溃。
解决方案: -
预先设置热门数据:在redis高峰访问之前,把一些热门数据提前存入到redis里面,加大这些热门数据key的时长。
-
实时调整:现场监控哪些数据热门,实时调整key的过期时长。
-
使用锁,但是效率低。
11.4 缓存雪崩
定义:大量热点key过期,导致大量请求访问数据库。导致数据库崩溃。
解决方案:
-
构建多级缓存架构:nginx缓存 + redis缓存 +其他缓存(ehcache等)
-
使用锁或队列:用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。不适用高并发情况。
-
设置过期标志更新缓存:
记录缓存数据是否过期(设置提前量),如果过期会触发通知另外的线程在后台去更新实际key的缓存。 -
将缓存失效时间分散开。
12.分布式问题
Redis实现分布式锁
12.1分布式锁的三个核心要素:
- 加锁
使用setnx来加锁。
setnx k1 v1
- 解锁
当得到的锁的线程执行完任务,需要释放锁,以便其他线程可以进入。释放锁的最简单方式就是执行del指令。
del k1
- 锁超时
如果一个得到锁的线程在执行任务的过程中挂掉,来不及显式地释放锁,这块资源将会永远被锁住,别的线程也进不来。可以通过设置一个过期时间,确保锁在一段时间后自动释放。
expire key 30
解决SETNX和EXPIRE的非原子性,通过set ex nx解决。
SET key value [EX seconds] [NX]
- EX second: 设置键的过期时间为second秒;
- NX:只在键不存在时,才对键进行设置操作;
SET操作完成时,返回OK,否则返回nil。
12.2 分布式锁满足的特性
1、互斥性:在任何时刻,对于同一条数据,只有一台应用可以获取到分布式锁;
2、高可用性:在分布式场景下,一小部分服务器宕机不影响正常使用,这种情况就需要将提供分布式锁的服务以集群的方式部署;
3、防止锁超时:如果客户端没有主动释放锁,服务器会在一段时间之后自动释放锁,防止客户端宕机或者网络不可达时产生死锁;
4、独占性:加锁解锁必须由同一台服务器进行,也就是锁的持有者才可以释放锁,不能出现你加的锁,别人给你解锁了。
12.3 RedLock(Redis Distributed Lock)
原理:
-
获取当前时间戳
-
client尝试按照顺序使用相同的key,value获取所有Redis服务的锁,在获取锁的过程中的获取时间比锁过期时间短很多,这是为了不要过长时间等待已经关闭的Redis服务。并且试着获取下一个Redis实例。
比如:TTL为5s,设置获取锁最多用1s,所以如果一秒内无法获取锁,就放弃获取这个锁,从而尝试获取下个锁
-
client通过获取所有能获取的锁后的时间减去第一步的时间,这个时间差要小于TTL时间并且至少有3个Redis实例成功获取锁,才算真正的获取锁成功
-
如果成功获取锁,则锁的真正有效时间是 TTL减去第三步的时间差 的时间;比如:TTL 是5s,获取所有锁用了2s,则真正锁有效时间为3s(其实应该再减去时钟漂移);
-
如果客户端由于某些原因获取锁失败,便会开始解锁所有Redis实例;因为可能已经获取了小于3个锁,必须释放,否则影响其他client获取锁