一、redis的优缺点有哪些,可以用在那些地方?
优点
-
redis是基于内存结构,性能极高(读 110000次/秒,写 81000 次/秒)
-
redis虽然是以键值对存储数据的,但是value却可以支持多种类型(string hash list set zset)
-
redis的所有操作都是原子性的,可以通过lua脚本实现redis操作的事务
-
redis的操作是单线程,但是其多路复用技术实现了高性能读写
缺点
-
Redis中的缓存数据与数据库的数据必须通过两次写操作才能保持一致(双写一致)
-
Redis作为缓存使用会存在缓存击穿、缓存穿透、缓存雪崩等问题,在应用中需要进行处理
-
Redis是基于内存存储的,虽然支持持久化,但是在某些场景下可能存下数据丢失的风险
redis的使用场景:
-
当应用程序在完成例如点赞、实时排行榜、计数器等对数据实时读写性能要求比较高,对数据的一致性要求并不高的场景,可以使用redis进行数据实时写操作(数据库)
-
-
缓存:在大多数互联网应用中,为了提高数据的访问速度、降低数据库的并发访问压力,我们可以使用redis作为查询缓存
-
-
在分布式并发场景中,可以使用redis实现分布式锁
-
在分布式会话中,可以使用redis实现共享缓存(session)
二、redis常用的数据类型及其命令有哪些?
redis客户端的启动指令:redis -cli -p port 连接指定端口的redis
进入redis客户端之后会进行密码验证:auth passwd
redis是以键值对形式进行数据存储的,但是value支持多种数据类型
-
string 字符串
-
hash 映射
-
list 队列
-
set 无序集合
-
zset 有序集合
String的常用指令如下:
## 设置值/修改值 如果key存在则进行修改
set key value
## 取值
get key
## 批量添加
mset k1 v1 [k2 v2 k3 v3 ...]
## 批量取值
mget k1 [k2 k3...]
## 自增和自减
incr key ## 在key对应的value上自增 +1
decr key ## 在key对应的value上自减 -1
incrby key v ## 在key对应的value上+v
decrby key v ## 在key对应的value上-v
## 添加键值对,并设置过期时间(TTL Time To Live)
setex key time(seconds) value
## 设置值,如果key不存在则成功添加,如果key存在则添加失败(不做修改操作)
setnx key value
## 在指定的key对应value拼接字符串
append key value
## 获取key对应的字符串的长度
strlen key
hash常用指令如下:
## 向key对应的hash中添加键值对
hset key field value
## 从key对应的hash获取field对应的值
hget key field
## 向key对应的hash结构中批量添加键值对
hmset key f1 v1 [f2 v2 ...]
## 从key对应的hash中批量获取值
hmget key f1 [f2 f3 ...]
## 在key对应的hash中的field对应value上加v
hincrby key field v
## 获取key对应的hash中所有的键值对
hgetall key
## 获取key对应的hash中所有的field
hkeys key
## 获取key对应的hash中所有的value
hvals key
## 检查key对应的hash中是否有指定的field
hexists key field
## 获取key对应的hash中键值对的个数
hlen key
## 向key对应的hash结构中添加f-v,如果field在hash中已经存在,则添加失败
hsetnx key field value
list常用命令如下:
## 存储数据 (通常我们只在固定的一段存数据 - left&right)
lpush key value # 在key对应的列表的左侧添加数据value
rpush key value # 在key对应的列表的右侧添加数据value
## 获取数据 (出栈:会将获取出的数据从列表中移除)
lpop key # 从key对应的列表的左侧取一个值
rpop key # 从key对应的列表的右侧取一个值
## 修改数据
lset key index value #修改key对应的列表的索引位置的数据(索引从左往右,从0开始)
## 查看key对应的列表中索引从start开始到stop结束的所有值
lrange key start stop
## 查看key对应的列表中index索引对应的值(不会从列表移除数据)
lindex key index
## 获取key对应的列表中的元素个数
llen key
## 从key对应的列表中截取index在[start,stop]范围的值,不在此范围的数据一律被清除掉
ltrim key start stop
## 从k1右侧取出一个数据存放到k2的左侧
rpoplpush k1 k2
set常用命令如下:
## 存储元素 :在key对应的集合中添加元素,可以添加1个,也可以同时添加多个元素
sadd key v1 [v2 v3 v4...]
## 遍历key对应的集合中的所有元素
smembers key
## 随机从key对于听的集合中获取一个值(出栈)
spop key
## 交集
sinter key1 key2
## 并集
sunion key1 key2
## 差集
sdiff key1 key2
## 从key对应的集合中移出指定的value
srem key value
## 检查key对应的集合中是否有指定的value
sismember key value
zset常用命令如下:
## 存储数据(score存储位置必须是数值,可以是float类型的任意数字;member元素不允许重复)
zadd key score member [score member...]
## 查看key对应的有序集合中索引[start,stop]数据——按照score值由小到大(start 和 stop指的不是score,而是元素在有序集合中的索引)
zrange key start top
##查看member元素在key对应的有序集合中的score
zscore key member
## 获取key对应的zset中的元素个数
zcard key
## 获取key对应的zset中,score在[min,max]范围内的member个数
zcount key min max
## 从key对应的zset中移除指定的member
zrem key6 member
## 查看key对应的有序集合中索引[start,stop]数据——按照score值由大到小
##
zrevrange key start stop
key的常用命令如下:
## 查看redis中满足pattern规则的所有的key(keys *)
keys pattern
## 查看指定的key是否存在
exists key
## 删除指定的key-value对
del key
## 获取当前key的存活时间(如果没有设置过期返回-1,设置过期并且已经过期返回-2) Time to live
ttl key
## 设置键值对过期时间
expire key seconds
pexpire key milliseconds
## 取消键值对过期时间
persist key
db常用指令如下:
## 切换数据库
select index
## 将键值对从当前db移动到目标db
move key index
## 清空当前数据库数据
flushdb
## 清所有数据库的k-v
flushall
## 查看当前db中k-v个数
dbsize
## 获取最后一次持久化操作时间
lastsave
三、redis的持久化机制有哪些?
问题分析:由于Redis是基于内存结构进行数据存储和操作的,如果使用redis作为数据库存储数据、或者作为缓存缓存数据,当遇到Redis实例异常、Redis服务器主机宕机等情况,会导致Redis存储/缓存的数据丢失。
Redis是基于内存操作,但作为一个数据库也具备数据的持久化能力;但是为了实现高效的读写操作,并不会即时进行数据的持久化,而是按照一定的规则进行持久化操作的——持久化策略
Redis提供了2种持久化策略:
-
RDB (Redis DataBase)
-
AOF(Append Only File)
RDB持久化策略
在满足特定的redis操作条件时,将内存中的数据以数据快照
的形式存储到rdb文件
中
-
原理:
RDB是redis默认的持久化策略,当redis中的写操作达到指定的次数、同时距离上一次持久化达到指定的时间就会将redis内存中的数据生成数据快照,保存在指定的rdb文件中。
-
默认触发持久化条件:
-
900s 1次:当操作次数达到1次,900s就会进行持久化
-
300s 10次:当操作次数达到10次,300s就会进行持久化
-
60s 10000次:当操作次数达到10000次,60s就会就行持久化
-
可以通过修改redis.conf文件,来设置RDB策略的触发条件
-
RDB持久化细节分析:
缺点
-
如果redis出现故障,存在数据丢失的风险,会丢失上一次持久化之后的操作数据;
-
RDB采用的是数据快照形式进行持久化,不适合实时性持久化;
-
如果数据量巨大,在RDB持久化过程中生成数据快照的子进程执行时间过长,会导致redis卡顿,因此save时间周期设置不宜过短;
优点
-
在数据量较小的情况下,执行速度比较快;
-
由于RDB是以数据快照的形式进行保存的,我们可以通过拷贝rdb文件轻松实现redis数据移植
-
AOF持久化策略
-
原理:
Apeend Only File,Redis将每一个成功的写操作指令写入到aof文件中,当redis重启的时候就执行aof文件中的指令以恢复数据,Redis默认未开启aof持久化。
-
AOF细节分析:
-
也可以通过拷贝aof文件进行redis数据移植
-
aof存储的指令,而且会对指令进行整理;而RDB直接生成数据快照,在数据量不大时RDB比较快
-
aof是对指令文件进行增量更新,更适合实时性持久化
-
redis官方建议同时开启2种持久化策略,如果同时存在aof文件和rdb文件的情况下aof优先
-
四、redis的事务命令有哪些?redis事务和mysql事务的区别是什么?
Redis中的事务命令有以下几个:
1. MULTI:标记一个事务的开始。
2. EXEC:执行事务中的所有命令。
3. DISCARD:取消当前事务。
4. WATCH:监视一个或多个键,如果在事务执行过程中这些键被修改,事务将被取消。
5. UNWATCH:取消对所有键的监视。
事务和MySQL事务的区别如下:
1. 原子性:Redis事务不支持回滚,它会按顺序执行事务中的命令,如果任何一条命令执行失败,剩下的命令仍然会被执行。而MySQL事务支持回滚,可以撤销已执行的命令。
2. 隔离性:Redis事务是单线程执行的,其他客户端的请求会在事务执行完之后才会被处理,所以在事务执行期间不会有其他客户端操作相同的数据。而MySQL事务则使用锁机制来保证隔离性。
3. 一致性:Redis事务不保证数据的一致性,因为在事务执行期间其他客户端可以修改事务中的键值。MySQL事务保证了数据的一致性,通过ACID(原子性、一致性、隔离性和持久性)特性来确保数据的完整性。
4. 持久性:Redis默认情况下将数据存储在内存中,尽管可以通过持久化机制将数据写入磁盘,但它并不保证数据在写入磁盘之前的持久性。而MySQL使用日志和检查点机制来保证数据的持久性。
总的来说,Redis事务更适合用于批量执行命令或者保证顺序执行一组命令,而MySQL事务更适合于需要严格的数据一致性和持久性的场景。
五、redis的主从复制和哨兵模式如何实现?解决了哪些问题?
使用redis作为缓存数据库目的是为了提升数据加载速度、降低对数据库的访问压力,我们需要保证redis的可用性。
-
主从配置
-
哨兵模式
-
集群配置
主从配置的使用
-
主从配置:在多个redis实例建立起主从关系,当
主redis
中的数据发生变化,从redis
中的数据也会同步变化。 -
通过主从配置可以实现redis数据的备份(
从redis
就是对主redis
的备份),保证数据的安全性; - 通过主从配置可以实现redis的读写分离
主从配置的步骤:
①先创建三个redis实例(配置三个不同端口的redis配置文件)
②在从的redis实例的配置文件中配置设置“跟从”---127.0.0.1 6380
③可以使用该指令:vim redis-slave1.conf slaveof 127.0.0.1 6380
哨兵模式的使用
哨兵模式:用于监听主库,当确认主库宕机之后,从备库(从库)中选举一个转备为主,哨兵的数量必须是奇数个 ,不然无法确定那个从库被选中
哨兵模式的配置步骤:
①先创建配置好主从配置
②创建并启动三个哨兵
拷贝sentinel.conf文件三份:sentinel-26380.conf sentinel-26382.conf sentinel-26382.conf
③配置哨兵的conf配置文件
④测试步骤
启动 主redis
启动 备1redis
启动 备2redis
再依次启动三个哨兵:
[root@theo sentinelconf]# redis-sentinel sentinel-26380.conf
六、redis的集群搭建的作用以及如何搭建?
redis集群的作用:
高可用:保证redis一直处于可用状态,即时出现了故障也有备用方案保证可用性(主从配置+哨兵模式
)
高并发:一个redis实例已经可以支持多达11w并发读操作或者8.1w并发写操作;但是如果对于有更高并发需求的应用来说,我们可以通过集群配置
来解决高并发问题
Redis集群
-
Redis集群中每个节点是对等的,无中心结构
-
数据按照slots分布式存储在不同的redis节点上,节点中的数据可共享,可以动态调整数据的分布
-
可扩展性强,可以动态增删节点,最多可扩展至1000+节点
-
集群每个节点通过主备(哨兵模式)可以保证其高可用性
Redis集群的创建步骤
①修改 redis.conf 配置文件
②启动多个配置文件下的redis实例
③创建redis集群
redis-cli --cluster create 47.96.11.185:7001 47.96.11.185:7002 47.96.11.185:7003 47.96.11.185:7004 47.96.11.185:7005 47.96.11.185:7006 --cluster-replicas 1
(47.96.11.185为你使用的服务器的 ip 地址)
④查看集群的运行状态
redis-cli --cluster info 47.96.11.185:7001
⑤平衡节点的数据槽数
redis-cli --cluster rebalance 47.96.11.185:7001
⑥删除节点
redis-cli --cluster del-node 47.96.11.185:7001 4678478aa66b6d37b23944cf7db0ac07298538a4 (4678478aa66b6d37b23944cf7db0ac07298538a4 为目标结点的id)
⑦添加节点
redis-cli --cluster add-node 47.96.11.185:7007 47.96.11.185:7002
七、redis作为缓存存在哪些问题?
使用redis做为缓存在高并发场景下有可能出现缓存击穿、缓存穿透、缓存雪崩等问题。
缓存击穿概念
缓存击穿:大量的并发请求
同时访问同一个在redis中不存在的数据
,就会导致大量的请求绕过redis同时并发访问数据库,对数据库造成了高并发访问压力。
-
特点:并发请求的数据在redis中不存在,但是在MySQL存在
-
解决缓存击穿问题
-
使用
双重检测锁
解决缓存击穿
问题 -
@Service public class ProductServiceImpl implements ProductService { @Autowired private ProductDAO productDAO; @Autowired private StringRedisTemplate stringRedisTemplate; //①②③ public ResultVO getProductById(int productId) throws JsonProcessingException { //1.从redis查询商品信息 (string: productId---json ) String s = stringRedisTemplate.boundValueOps(productId + "").get(); if(s != null){ //2.如果redis命中,则直接返回当前商品信息 Product product = new ObjectMapper().readValue(s, Product.class); return new ResultVO(200,"success",product); }else{ //线程③ //线程② //线程① Product product = null; synchronized (this) { //第二次查询redis String s2 = stringRedisTemplate.boundValueOps(productId + "").get(); if(s2 != null){ //2.如果redis命中,则直接返回当前商品信息 product = new ObjectMapper().readValue(s2, Product.class); return new ResultVO(200,"success",product); }else{ //3.如果redis没有命中,则需要查询数据库 product = productDAO.selectById(productId); System.out.println("----查询MySQL数据库"); //4.将从数据库查询到的商品信息存入redis String jsonStr = new ObjectMapper().writeValueAsString(product); stringRedisTemplate.boundValueOps(productId + "").set(jsonStr); //当线程①执行结束的时候,redis中是否已经有了数据? yes or no ! YES return new ResultVO<>(200,"success",product); } } } } }
缓存穿透的概念
-
缓存穿透:大量的并发请求访问一个数据库中不存在的数据,首先在redis中无法命中,最终所有的请求都会访问数据库,同样会导致数据库承受巨大的访问压力。
-
特点:大量请求的数据在redis中不存在,同时在MySQL也不存在
-
如果从数据库查询到数据为null,则写一个非空的数据到redis,以解决缓存穿透问题
-
如果向数据库中新增了这条数据,为了让请求可以访问新增的数据,向redis写非空数据时需要设置过期时间
-
当向 redis 写非空数据后,在向数据库添加这条数据的时候,如果redis没有达到过期时间,请求依然不能访问数据库中的数据,我们可以通过双写操作实现redis中数据与数据库数据的一致性:
向数据库添加一条数据的同时,将这条数据写入到reids
缓存雪崩概念
缓存雪崩:缓存大量的数据集中过期,导致请求这些数据的大量的并发请求会同时访问数据库
解决方案:
-
将缓存中的数据设置成不同的过期时间
-
在访问洪峰到达前缓存热点数据,过期时间设置到流量最低的时段
八、redis 的常用客户端有哪些?
Redis 的常用客户端有以下几种:
1. redis-cli:Redis官方提供的命令行客户端,可以直接在终端上使用命令与Redis进行交互。
2. Jedis:Java语言的Redis客户端,是Redis官方推荐的Java客户端之一,提供了丰富的功能和API。
3. redisTemplate: Java语言的Redis客户端,是Spring Data Redis提供的一个Redis访问模板,它是在Jedis的基础上进行封装和扩展的,添加了更多的功能和特性。
4. Lettuce:同样是Java语言的Redis客户端,作为Jedis的替代品,采用异步、可扩展的设计,支持线程安全和响应式编程模型。
5. Redisson:基于Java语言的Redis客户端,提供了丰富的分布式和集群功能,同时支持分布式锁、分布式集合等特性。
6. Node_redis:Node.js语言的Redis客户端,提供了简洁的API,与Redis进行高效的异步交互。
7. RedisDesktopManager:Redis的图形化客户端,可以在可视化界面上进行Redis的操作和管理。
这些客户端都有各自的特点和优势,可以根据具体的需求和开发环境选择合适的客户端进行使用。
九、redis 的 jedis 客户端、 redisTemplate 客户端和 Lettuce 客户端的区别是什么?
Lettuce、Jedis和RedisTemplate都是Java语言中常用的Redis客户端库,它们在实现Redis的访问和操作上有以下区别:
1. 底层实现:Jedis和RedisTemplate都是基于阻塞式I/O的客户端,而Lettuce则是基于非阻塞式I/O和异步编程模型的客户端。Jedis采用的是直接的Socket连接方式,而Lettuce使用Netty作为底层网络库,具有更好的性能和可扩展性。
2. 连接管理:Jedis和RedisTemplate在默认情况下使用连接池来管理与Redis服务器的连接,每次操作都会从连接池中获取一个连接,操作完成后再将连接返回连接池。Lettuce则采用了基于Reactive Streams的异步方式,通过Publisher-Subscriber模式实现高效的连接管理。
3. 线程模型:Jedis在使用时是单线程的,不能进行并行操作,而RedisTemplate可以在多线程环境下并发使用。Lettuce是基于异步和非阻塞的设计,可以充分利用多线程进行并行操作,提供更好的性能和扩展性。
4. 功能支持:Jedis和RedisTemplate提供了丰富的Redis操作方法和API,支持事务、流水线、发布/订阅等高级功能。Lettuce在底层设计上更加灵活,提供了异步调用、响应式编程等特性,适用于需要高并发和高性能的场景。
5. 数据序列化:Jedis和RedisTemplate默认使用Java的序列化器对数据进行序列化和反序列化。Lettuce提供了更多的序列化选项,支持更灵活的数据序列化方式,如使用Jackson、Gson等自定义的序列化器。
6. 依赖和集成:Jedis和Lettuce都是单独的Jar包,可以直接引入项目中使用。RedisTemplate是Spring Data Redis的一部分,需要引入Spring Data Redis的相关依赖,同时可以与Spring框架进行无缝集成。
综上所述,Jedis和RedisTemplate在使用上更加简单直观,适合一般的Redis操作;Lettuce则提供了更高级的功能和性能,并且适用于高并发场景下的异步操作。选择哪个客户端取决于具体的需求和项目环境。
十、基于 redis 的分布式锁是怎么实现的?
阻塞锁与非阻塞锁
阻塞锁:不断尝试获取锁,直到获取到锁为止
非阻塞锁:如果获取不到锁就放弃,但可以支持在一定时间段内的重试,在一段时间内如果没有获取到锁就放弃
公平锁和非公平锁
公平锁和非公平锁 100个线程 线程3 执行,剩下的99个要等待,当线程3执行结束以后,这99个线程到底谁获取执行权呢?
- 公平锁:按照线程的先后顺序获取锁
- 非公平锁:多个正在等待的线程随机获取锁
分布式场景中出现的问题:
问题①:
-
当一个订单中包含多个商品时,如果订单中部分商品加锁成功,但是某一个加锁失败,导致最终加锁状态失败
-
需要对已经锁定的部分商品释放锁
问题②:
-
能够先查询库存,在加锁吗?
-
不能,因为在查询库存到加锁的这个过程中,库存可能被其他线程修改。
问题③:
-
当当前线程加锁成功之后,执行业务过程中,如果当前线程出现异常导致无法释放锁,这个问题又该如何解决呢?
解决因线程异常导致无法释放锁的问题
解决方案:在对商品进行加锁时,设置过期时间,这样以来即使线程出现故障无法释放锁,在过期时间结束时也会自动“释放锁”
boolean b = stringRedisTemplate.boundValueOps(productId + "")
.setIfAbsent("value",3,TimeUnit.MINUTES);
新的问题:
当给锁设置了过期时间之后,如果当前线程t1因为特殊原因,在锁过期前没有完成业务执行,将会释放锁,同时其他线程(t2)就可以成功加锁了,当t2加锁成功之后,t1执行结束释放锁就会释放t2的锁,就会导致t2在无锁状态下执行业务。
解决因t1过期释放t2锁的问题
在加锁的时候,为每个商品设置唯一的value,当释放锁的时候,先查询redis中当前商品锁对应的值与加锁的时候设置的值是否一致,如果一致则释放锁
-
在加锁的时候,为每个商品设置唯一的value
-
String value = UUID.randomUUID().toString(); boolean b = stringRedisTemplate.boundValueOps(productId + "") .setIfAbsent(value,3,TimeUnit.MINUTES);
在释放锁的时候,先获取当前商品在redis中对应的value,如果获取的值与当前value相同,则释放锁
-
finally{ //查询操作 String v = stringRedisTemplate.boundValueOps(productId + "").get(); if(value.equals(v)){ //删除操作 stringRedisTemplate.delete(productId+""); } }
新的问题:
当释放锁的时候,在查询并判断“这个锁是当前线程加的锁”成功之后,正要进行删除时锁过期了,并且被其他线程成功加锁,一样会导致当前线程删除其他线程的锁。
-
Redis的操作都是原子性的
-
要解决如上问题,必须保证查询操作和删除操作的原子性——使用lua脚本
新的问题:
当网络不佳或者机器卡顿是可能导致线程 1中的key在购买完商品就过期,这是其他的线程就可以获取到key 从而导致线程并行
解决方法:通过看门狗线程,进行线程保护,如果线程没有执行完毕就不会过期,也就是给线程‘续命’ 只有当线程执行完成或者出现异常才会结束线程
看门狗线程工作原理:
-
监听当前线程锁的过期时间,当锁即将过期时如果业务没有执行结束,则重置锁的过期时间,保证业务线程正常执行的过程中,锁不会过期。
分布式锁框架-Redisson
基于Redis+看门狗机制
的分布式锁框架
分布式锁特点
1、互斥性 和我们本地锁一样互斥性是最基本,但是分布式锁需要保证在不同节点的不同线程的互斥。
2、可重入性 同一个节点上的同一个线程如果获取了锁之后那么也可以再次获取这个锁。
3、锁超时 和本地锁一样支持锁超时,加锁成功之后设置超时时间,以防止线程故障导致不释放锁,防止死锁。
4、高效,高可用 加锁和解锁需要高效,同时也需要保证高可用防止分布式锁失效,可以增加降级。
redission是基于redis的,redis的故障就会导致redission锁的故障,因此redission支持单节点redis、reids主从、reids集群
5、支持阻塞和非阻塞 和 ReentrantLock 一样支持 lock 和 trylock 以及 tryLock(long timeOut)。
锁的分类
1、乐观锁与悲观锁
-
乐观锁
-
悲观锁
2、可重入锁和非可重入锁
-
可重入锁:当在一个线程中第一次成功获取锁之后,在此线程中就可以再次获取
-
非可重入锁
3、公平锁和非公平锁
-
公平锁:按照线程的先后顺序获取锁
-
非公平锁:多个线程随机获取锁
4、阻塞锁和非阻塞锁
-
阻塞锁:不断尝试获取锁,直到获取到锁为止
-
非阻塞锁:如果获取不到锁就放弃,但可以支持在一定时间段内的重试
——在一段时间内如果没有获取到锁就放弃
Redission的使用
1、获取锁——公平锁和非公平锁
//获取公平锁
RLock lock = redissonClient.getFairLock(skuId);
//获取非公平锁
RLock lock = redissonClient.getLock(skuId);
2、加锁——阻塞锁和非阻塞锁
//阻塞锁(如果加锁成功之后,超时时间为30s;加锁成功开启看门狗,剩5s延长过期时间)
lock.lock();
//阻塞锁(如果加锁成功之后,设置自定义20s的超时时间)
lock.lock(20,TimeUnit.SECONDS);
//非阻塞锁(设置等待时间为3s;如果加锁成功默认超时间为30s)
boolean b = lock.tryLock(3,TimeUnit.SECONDS);
//非阻塞锁(设置等待时间为3s;如果加锁成功设置自定义超时间为20s)
boolean b = lock.tryLock(3,20,TimeUnit.SECONDS);
3、释放锁
lock.unlock();
4、应用示例
//公平非阻塞锁
RLock lock = redissonClient.getFairLock(skuId);
boolean b = lock.tryLock(3,20,TimeUnit.SECONDS);