目录
RDB持久化机制【Redis DataBase Backup】:
使用 Java Redis 客户端 Jedis 实现栈和队列的示例代码:
秒杀场景为什么要Redis的存储结构hash?不用其他的存储方式?
什么是Redis,为什么Redis很快?
Redis:是基于内存进行读写,并采用键值对的方式进行存储的非关系型数据库。采用键值对存储数据在内存或磁盘中,可以对关系型数据库起到补充作用,同时支持持久化(可以将数据保存在可掉电设备中),可以将数据同步保存到磁盘。
说Redis很快是相对于关系型数据库如mysql来说的,主要有以下因素
-
高效的数据结构,如String字符串,List列表,Set集合,ZSet有序集合,Hash哈希。这些数据结构在内存中的操作非常高效,能够快速完成各种复杂的数据操作。
-
基于内存进行读写,不需要访问数据库,所以速度快。
-
采用多路IO复用模型,减少网络IO的时间消耗,避免大量的无用操作,所以速度快。(多路 I/O 复用模型是一种高效的网络 I/O 处理机制,它允许服务器在等待多个客户端请求时,能够同时处理多个连接,而不需要为每个连接创建一个独立的线程或进程。)
-
单线程模型,避免了线程切换带来的性能损耗,虽然并发能力受到了一定的限制,所以速度快。
非关系型数据库(NoSQL)和关系型数据库(SQL)的区别
非关系型数据库(NoSQL)和关系型数据库(SQL)是两种不同的数据库类型。他们在数据模型、存储方式、查询语言、扩展性和适用场景有显著的区别。
1. 数据模型
-
关系型数据库(SQL):
- 数据模型:关系型数据库使用表格模型,数据存储在表中,表之间通过关系(如外键)进行连接。
- 结构化数据:数据是结构化的,每个表有固定的列和数据类型。
- 示例:MySQL、PostgreSQL、Oracle、SQL Server。
-
非关系型数据库(NoSQL):
- 数据模型:非关系型数据库使用多种数据模型,如键值存储、文档存储、列族存储和图数据库。
- 非结构化或半结构化数据:数据可以是非结构化或半结构化的,适合存储复杂和多样的数据类型。
- 示例:MongoDB(文档存储)、Redis(键值存储)、Cassandra(列族存储)、Neo4j(图数据库)。
2. 存储方式
-
关系型数据库(SQL):
- 行存储:数据按行存储,适合事务处理和复杂查询。
- 固定模式:数据模式是固定的,需要预先定义表结构。
-
非关系型数据库(NoSQL):
- 列存储、文档存储、键值存储等:数据存储方式多样,适合不同的应用场景。
- 灵活模式:数据模式是灵活的,可以动态添加字段和数据类型。
3. 查询语言
-
关系型数据库(SQL):
- SQL:使用结构化查询语言(SQL)进行数据查询和操作。
- 复杂查询:支持复杂的查询和事务处理。
-
非关系型数据库(NoSQL):
- 多种查询语言:不同的 NoSQL 数据库有不同的查询语言,如 MongoDB 的 MongoDB Query Language、Redis 的命令、Cassandra 的 CQL 等。
- 简单查询:通常不支持复杂的查询和事务处理,但查询速度更快。
4. 扩展性
-
关系型数据库(SQL):
- 垂直扩展:通过增加服务器的硬件资源(如 CPU、内存、磁盘)来提高性能。
- 扩展性有限:垂直扩展的成本较高,扩展性有限。
-
非关系型数据库(NoSQL):
- 水平扩展:通过增加更多的服务器节点来提高性能。
- 高扩展性:水平扩展的成本较低,扩展性更好。
5. 事务处理
-
关系型数据库(SQL):
- ACID 特性:支持事务的 ACID 特性(原子性、一致性、隔离性、持久性),确保数据的一致性和完整性。
- 复杂事务:适合处理复杂的事务操作。
-
非关系型数据库(NoSQL):
- 基本事务:大多数 NoSQL 数据库不支持复杂的事务处理,但支持基本的事务操作。
- 最终一致性:通常采用最终一致性模型,确保在一定时间内数据达到一致状态。
6. 使用场景
-
关系型数据库(SQL):
- 适用场景:适用于需要复杂查询、事务处理和数据一致性的应用,如金融系统、ERP 系统、电子商务平台等。
-
非关系型数据库(NoSQL):
- 适用场景:适用于需要高性能、高扩展性和灵活数据模型的应用,如大数据分析、实时数据处理、内容管理系统、社交媒体平台等。
Redis的5种数据存储结构和使用场景
数据存储结构包含:String字符串,List列表,Set集合,ZSet有序集合,Hash哈希。
Redis 在互联网产品中使用的场景实在是太多太多,这里分别对 Redis 几种数据类型做了整理:
1)String字符串:缓存(存储图片验证码或手机验证码)、计数器等。
字符串是 Redis 中最基本的数据类型,一个字符串键可以对应一个字符串值。
import org.springframework.data.redis.core.StringRedisTemplate;
public class RedisStringExample {
@Autowired
private StringRedisTemplate stringRedisTemplate;
public void stringExample() {
// 将一个字符串值存储到 Redis 中
stringRedisTemplate.opsForValue().set("key", "value");
// 从 Redis 中获取一个字符串值
String value = stringRedisTemplate.opsForValue().get("key");
System.out.println("value = " + value);
}
}
2)Hash哈希列表:用户信息、用户主页访问量、组合查询等。
哈希是一个 String 类型的 field 和 value 的映射表,Hash 特别适合于存储对象。
import org.springframework.data.redis.core.StringRedisTemplate;
public class RedisHashExample {
@Autowired
private StringRedisTemplate stringRedisTemplate;
public void hashExample() {
// 将一个对象存储到 Redis 中
stringRedisTemplate.opsForHash().put("user:1", "name", "Tom");
stringRedisTemplate.opsForHash().put("user:1", "age", "20");
// 从 Redis 中获取一个对象
String name = stringRedisTemplate.opsForHash().get("user:1", "name");
String age = stringRedisTemplate.opsForHash().get("user:1", "age");
System.out.println("name = " + name);
System.out.println("age = " + age);
}
}
3)List列表:做队列(排队,FIFO)。
列表是一个字符串元素的有序集合,列表中的元素可以重复。
import org.springframework.data.redis.core.StringRedisTemplate;
public class RedisListExample {
@Autowired
private StringRedisTemplate stringRedisTemplate;
public void listExample() {
// 将一个列表值存储到 Redis 中
stringRedisTemplate.opsForList().rightPush("key", "value1", "value2", "value3");
// 从 Redis 中获取一个列表值
List<String> value = stringRedisTemplate.opsForList().range("key", 0, -1);
System.out.println("value = " + value);
}
}
4)Set集合:抽奖小程序(不能重复参与抽奖),点赞和收藏(不能重复进行点赞)。(不能存储重复的数据,底层存储使用的是map的key进行存储数据)
集合是一个字符串元素的无序集合,集合中的元素不可以重复。
import org.springframework.data.redis.core.StringRedisTemplate;
public class RedisSetExample {
@Autowired
private StringRedisTemplate stringRedisTemplate;
public void setExample() {
// 将一个集合值存储到 Redis 中
stringRedisTemplate.opsForSet().add("key", "value1", "value2", "value3");
// 从 Redis 中获取一个集合值
Set<String> value = stringRedisTemplate.opsForSet().members("key");
System.out.println("value = " + value);
}
}
5)ZSet有序集合:排行榜(可以根据某个字段进行排序)。
有序集合是一个字符串元素的有序集合,有序集合中的元素不可以重复。
import org.springframework.data.redis.core.StringRedisTemplate;
public class RedisSortedSetExample {
@Autowired
private StringRedisTemplate stringRedisTemplate;
public void sortedSetExample() {
// 将一个有序集合值存储到 Redis 中
stringRedisTemplate.opsForZSet().add("key", "value1", 1.0);
stringRedisTemplate.opsForZSet().add("key", "value2", 2.0);
stringRedisTemplate.opsForZSet().add("key", "value3", 3.0);
// 从 Redis 中获取一个有序集合值
Set<String> value = stringRedisTemplate.opsForZSet().range("key", 0, -1);
System.out.println("value = " + value);
}
}
Redis如何存储一个java对象?
Redis如何存储一个Java对象【内涵案例】_MXin5的博客-CSDN博客
Redis是不能直接存储java对象的,但是可以将java对象进行序列化转换成二进制数据(byte[]),然后存储到Redis当中,然后通过反序列化将存储的对象进行取出。可以使用原生的方式和第三方的框架如fastjson,jackson等。
因为:Redis是采用key和value键值对的方式进行存储的,key和value都支持二进制安全的字符串。
你们项目是怎么用Redis的?
使用的是Springboot整合的redis,我们项目中主要使用Redis来存储注册时候的图形验证码和手机验证码,还有使用redis存储黑名单列表,还有用Redis的String存储结构来存储首页经常访问的数据,主要是因为Redis是采用键值对的方式。将数据存储在内存当中,我们对数据进行查询的时候直接从内存当中获取,减少去数据库进行查询,从而降低数据库的压力,并且提高了查询效率。
Redis为什么进行持久化?
因为Redis是存储数据是采用键值对的方式进行存储到内存当中,这样读取数据的速度非常快, 但是一旦服务器宕机,内存中的数据将全部丢失。因此需要将缓存中数据进行持久化, 通常持久化的方式有两种, RDB快照和AOF日志。
Redis如何解决高并发?
Redis作为一种高性能、高并发、可扩展的基于内存的数据库,有很多解决高并发问题的方法。以下是常见的几种方式:
-
单线程模型:Redis采用的是单线程模型,即一个Redis进程只会使用一个CPU,单个操作会依次执行,避免了多线程带来的锁竞争和线程切换等开销。同时,对于高并发的情况,Redis会通过线程复用和内部多路复用器来提高性能。
-
持久化机制:Redis支持将数据持久化到硬盘上,以防止服务器故障等异常情况导致数据丢失。Redis提供两种持久化方式:RDB和AOF。RDB【Redis DataBase】是将整个数据库快照保存到硬盘上,而AOF是将所有的写操作都实时记录到一个append-only文件中,因此无论何时,Redis都可以通过重放AOF文件的方式来恢复丢失的数据。
-
集群模式:在单节点Redis无法满足高并发需求时,可以将多个Redis服务器组成一个集群,Redis 支持主从复制和哨兵机制,确保高可用性和数据一致性。主从复制机制通过将数据复制到多个从节点,确保数据的一致性和高可用性。哨兵机制通过监控主节点的状态,在主节点发生故障时自动切换到从节点,确保系统的高可用性。
-
缓存机制:Redis常被用作缓存系统,将常用且需要频繁访问的数据存储到内存中,减少数据库的访问压力。同时,使用缓存的方式可以提高查询速度和响应时间,从而提高系统的并发能力。此外,Redis还提供了数据过期机制、LRU等缓存淘汰策略,从而避免缓存数据过期或者占用内存过大等问题。
-
LUA脚本:Redis支持使用LUA脚本来执行复杂的数据操作,利用LUA的脚本编译器和执行器对脚本进行缓存和预编译,从而提高操作的性能和并发能力。
综上所述,Redis通过单线程模型、持久化机制、集群模式、缓存机制、LUA脚本等方式,提高了系统的并发能力和性能,满足高并发场景的需求。
怎么防止Redis宕机数据丢失问题?
Redis本身是一种基于内存的数据库,数据存储在内存中,因此一旦Redis服务器宕机,之前存储在内存中的数据会全部丢失。为了避免Redis宕机数据丢失问题,可以使用以下几种方法:
-
使用Redis的持久化机制:Redis提供了两种持久化机制,即RDB和AOF。RDB是将整个数据库快照保存到硬盘上,而AOF则是将每个写操作记录到一个append-only文件中。这些持久化文件可以被用来在Redis重启时恢复数据。可以根据实际情况选择合适的持久化方式,并设置定期保存持久化文件的频率,防止数据丢失。
-
启用Redis的主从复制(或哨兵机制):可以在多台机器上运行Redis,并使用主从复制的方式进行数据备份。将一个Redis实例作为主节点,其余的Redis实例作为从节点,主节点将数据同步到各个从节点上,保持数据的同步。(故障切换)如果主节点宕机,可以手动或自动将一个从节点提升为新的主节点,确保系统的高可用性。
-
做好备份: 定期将Redis的数据备份到一个可靠的存储介质中,例如硬盘或者云存储。一旦遇到数据丢失的情况,可以通过备份数据来进行数据的恢复。
综上所述,使用持久化机制、主从复制和备份等方式,可以有效避免Redis宕机数据丢失问题。
Redis持久化机制是什么?
Redis持久化机制就是将内存中的数据持久化到磁盘的过程,就叫作持久化(数据可保存在可掉电设备当中)
Redis持久化机制主要有两种方式,
RDB持久化机制【Redis DataBase Backup】:
快照持久化:RDB持久化机制是将Redis存储的数据定期生成快照文件(通常为'.rdb'文件)并保存到到磁盘当中。
定期保存:RDB还可以设置定期保快照文件,当一段时间内当Redis数据变更达到一定量时就会保存快照文件。
快速恢复:由于快照文件包含了所有的数据,Redis可以通过加载快照文件快速恢复数据。
数据一致性:快照文件包含了某一时刻的所有数据,因此数据是一致的。
适用场景:适用于变更频繁较低,数据量比较大的场景。
由于快照文件包含了所有的当设置了RDB机制后,Redis会根据用户的配置策略,定期将数据集的状态进行持久化,存储于硬盘文件中。默认情况下,Redis会在多长时间之后进行一次全量备份,配置如下:
save 900 1 # 900秒中至少有1个key被修改过
save 300 10 # 300秒钟中至少有10个key被修改过
save 60 10000 # 60秒钟中至少有10000个key被修改过
以上配置表示当Redis检测到一定数量的键被修改后,会触发持久化操作,持久化整个数据集到硬盘上。对于快照的生成,Redis采用fork()操作来生成一个新的进程,然后新进程将当前状态写入磁盘文件,最后通过rename()操作覆盖旧的快照文件,整个过程是在内存中操作,所以性能极高。但是,如果系统宕机,则可能会导致最近一次快照之后的数据丢失。
AOF持久化机制【Append Only File】:
日志持久化:将Redis执行的每条写入命令记录到一个日志文件(通常'.aof'文件)中并保存到磁盘。
实时保存:AOF可以配置每次写入操作后立即将日志文件写入到磁盘当中,确保数据的实时持久化。
数据恢复:Redis可以通过日志文件的所有写操作命令恢复数据。
数据一致性:AOF提供了更高的数据一致性,因为它记录了每次的写操作命令。
适用场景:适用于数据变更频率较高、数据一致性要求较高的场景。
当Redis需要重建数据集的时候,只需要把日志文件中的命令重新执行一遍即可。相比RDB持久化机制,AOF持久化机制更加可靠。在AOF持久化机制下,用户可以配置3种写日志的方式:always(默认)、everysec和no,其中默认的always方式每次写入命令都会同步到硬盘,而everysec只会每秒写入一次,no则表示完全不写入日志。用户也可以在特定的时候通过发送一个BGREWRITEAOF命令来重写日志文件,删除其中的过时命令,从而减小日志的体积。
综上所述,Redis的持久化机制是将内存中的数据写入到磁盘中,以确保Redis的持久性和可靠性。RDB持久化机制通过生成快照来备份数据,虽然性能更为优秀,但是有一定的数据可靠性风险。而AOF持久化机制则通过记录每一次的写入命令来记录数据,可靠性更高,但是需要占用更多的磁盘空间和带来一定的性能损耗。
RDB和AOF的差别?
RDB和AOF是Redis用于持久化数据的两种方式,两者的主要区别如下:
保存方式不同:
RDB是将Redis存储的数据(可以定期的)生成一个快照文件保存到磁盘当中,AOF则是记录每一次Redis的写入命令(可以实时的的)记录到日志文件当中保存到磁盘当中。
数据一致性:
RDB由于是定期生成快照文件,可能会丢失最近的数据变更。AOF则记录了每次写入命令到日志文件当中,能够提高较高的数据一致性。
恢复数据时间:
RDB恢复数据时,只需要加载快照文件即可,恢复速度较快。AOF恢复数据时,需要执行每次写入命令,恢复速度较慢。
适用场景:
RDB适用于变更频率较低、数据量较大的场景。AOF适用于变更频率较高,数据一致性要求较高的场景。
Redis内存满了不够了怎么办?
Redis是基于内存进行读写、采用键值对方式进行存储的非关系型数据库,所以内存对于Redis来说是至关重要的。
方式一:增加服务器内存(Redis可以使用服务器内存,当然我们通常会通过设置在redis.windows.conf 中 maxmemory参数限制Redis内存的使用)。
方式二:使用Redis内存淘汰策略,删掉一些老旧数据。
-
volatile-lru :从已设置过期时间的键中挑选最近最少使用的数据淘汰
-
volatile-ttl:从已设置过期时间的键中挑选将要过期的数据淘汰
-
volatile-random:从已设置过期时间的键中任意选择数据淘汰
-
allkeys-lru:从所有键中 挑选最近最少使用的数据淘汰
-
allkeys-random:从所有键中任意选择数据淘汰
-
no-enviction:不能淘汰数据
volatile:不稳定的:可以理解为有设置过期时间的。
lru【least recently used】:最近最少使用的。
ttl【time to live】:生存时间,还剩多长时间。
allkeys:所有的redis键。
方式三:Redis使用集群,多个Redis进行存储数据。(还可以解决高并发问题)
扩展:Redis缓存每命中一次Redis中设置过期时间的数据时,就会给命中的数据增加一定的ttl(过期时间)。一段时间后, 热数据的ttl都会较大, 不会自动失效, 而冷数据基本上过了设定的ttl就马上失效了
Redis怎么实现栈和队列?
Redis可以通过列表(List)数据结构实现栈和队列的功能。
栈(Stack):使用Redis的List列表的数据结构,通过lpush()命令和lpop()命令实现先进后出。
队列(Queue):使用Redis的List列表的数据结构,通过lpush()命令和rpop()命令实现先进后出。
使用 Java Redis 客户端 Jedis 实现栈和队列的示例代码:
import redis.clients.jedis.Jedis;
public class RedisStackQueueExample {
public static void main(String[] args) {
// 初始化 Jedis 客户端
Jedis jedis = new Jedis("localhost");
// 定义列表名称
String stackName = "my_stack"; //栈
String queueName = "my_queue"; //队列
// 将元素压入栈中,从栈顶开始插入
jedis.lpush(stackName, "element1");
jedis.lpush(stackName, "element2");
jedis.lpush(stackName, "element3");
// 从栈中弹出元素,从栈顶开始弹出
System.out.println(jedis.lpop(stackName)); // 输出:element3
System.out.println(jedis.lpop(stackName)); // 输出:element2
System.out.println(jedis.lpop(stackName)); // 输出:element1
// 将元素插入队列尾部
jedis.lpush(queueName, "element1");
jedis.lpush(queueName, "element2");
jedis.lpush(queueName, "element3");
// 从队头获取元素
System.out.println(jedis.rpop(queueName)); // 输出:element1
System.out.println(jedis.rpop(queueName)); // 输出:element2
System.out.println(jedis.rpop(queueName)); // 输出:element3
}
}
为什么要对Redis实现淘汰策略?
因为Redis存储数据是基于内存的,Redis虽然快,但是内存成本还是比较高的,而且基于内存Redis不适合存储太大量的数据。Redis也可以通过增加服务器内存或者使用集群(主从复制、哨兵机制)的方式。
Redis的key加一个过期时间,原生的操作命令是什么?
Redis可以为指定的 Key 设置过期时间来控制 Key 的有效期,即在一定的时间后自动删除 Key 和 Value。为 Key 设置过期时间可以使用 EXPIRE
或 EXPIREAT
命令。
EXPIRE key seconds
:这个命令用于将指定key
的过期时间设置为seconds
秒,表示在seconds
秒后,指定的key
将自动过期并被删除。如果seconds
参数设置为0,则表示将指定的key
过期时间被清除。示例如下:redis> SET mykey "Hello" OK redis> EXPIRE mykey 10 1 redis> TTL mykey # 查看mykey的过期时间 10 redis> # 等待10秒后 redis> GET mykey # 此时mykey已经过期,返回为null (nil)
EXPIREAT key timestamp
:这个命令用于将指定key
的过期时间设置为指定的时间戳(timestamp
),即指定key
所设置的过期时间是一个绝对时间。在指定的timestamp
时间点,指定的key
将自动过期并被删除。示例如下:redis> SET mykey "Hello" OK redis> EXPIREAT mykey 1293840000 # 设置mykey的过期时间为 2011年1月1日 00:00:00 1 redis> TTL mykey # 查看mykey的过期时间 4046099 redis> # 等待到指定时间点后 redis> GET mykey # 此时mykey已经过期,返回为null (nil)
以下是 Java Redis 客户端 Jedis 实现 Key 设置过期时间的示例代码:
import redis.clients.jedis.Jedis;
public class RedisKeyExpireExample {
public static void main(String[] args) {
// 初始化 Jedis 客户端
Jedis jedis = new Jedis("localhost");
// 定义 Key 名称
String key = "mykey";
// 设置 Key 的过期时间为 60 秒
jedis.expire(key, 60);
// 设置 Key 的过期时间为指定 Unix 时间戳
jedis.expireAt(key, 1647654752);
}
}
Java Redis 客户端 Jedis 实现永久存储的示例代码:
import redis.clients.jedis.Jedis;
public class RedisPersistExample {
public static void main(String[] args) {
// 初始化 Jedis 客户端
Jedis jedis = new Jedis("localhost");
// 定义 Key 名称和 Value 值
String key = "mykey";
String value = "hello world";
// 存储 Key 值到 Redis 中,并取消 Key 的过期时间
jedis.set(key, value);
jedis.persist(key);
}
}
当 Key 被取消过期时间之后,该 Key 将永远存在于 Redis 中,除非使用 DEL
命令手动删除。
Redis事务和Mysql事务的区别?
事务的四大特性(ACID):原子性Atomicity、一致性Consistency、隔离性Isolation、持久性Durability。
原子性:MySQL 事务满足原子性,即一组操作要么同时成功,要么同时失败。
持久性:一旦提交事务不能进行回滚,MySQL 事务确保一旦事务提交,数据将持久化到数据库中。
隔离性:(事务之间互不影响)MySQL 事务提供多种隔离级别,确保事务之间的隔离。
一致性:(事务执行前后都必须保证数据的总和是一致的) 要求任何写到数据库的数据都必须满足预先定义的规则,它基于其他三个特性实现的(转账的前后金额是一致的,少100,那边就会多100)
区别:
Mysql的事务是基于undo/redo日志(undolog和redolog日志),记录修改数据前后的状态来实现的数据的提交和数据的回滚。
而Redis的事务是基于队列实现的(Redis中事务不会回滚,即一组操作中某些命令执行失败了,其他操作不会回滚。)
Mysql中的事务满足原子性和持久性:即一组操作要么同时成功,要么同时失败。
Redis中的事务不满足原子性和持久性,即一组操作中某些命令执行失败了,其他操作不会回滚。
因此对于比较重要的数据,应该存放在mysql中。
使用Redis如何实现消息广播?
理论:
Redis是使用发布和订阅来实现广播的。
订阅者通过 SUBSCRIBE channel命令订阅某个频道 , 发布者通过 PUBLISH channel message向该频道发布消息,该频道的所有订阅者都可以收到消息。
使用Java操作Redis的代码示例,实现发布/订阅消息广播:
订阅者:
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPubSub;
public class Subscriber {
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
jedis.auth("password"); // 如果Redis服务器启用了密码认证,需要先通过该方法进行认证
jedis.subscribe(new JedisPubSub() {
@Override
public void onMessage(String channel, String message) {
System.out.println("[Received] Channel: " + channel + ", Message: " + message);
}
}, "channel1");
}
}
发布者:
import redis.clients.jedis.Jedis;
public class Publisher {
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
jedis.auth("password"); // 如果Redis服务器启用了密码认证,需要先通过该方法进行认证
jedis.publish("channel1", "Hello, world!");
}
}
在实际使用中,需要根据需要修改Redis服务器的IP地址、端口和密码,以及订阅的频道名称和发布的消息内容。同时需要引入Jedis客户端库的依赖,例如在Maven项目中,可以在pom.xml
文件中添加以下内容
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.6.3</version>
</dependency>
为什么要使用Redis做缓存?
主要是Redis基于内存进行读取,读取数据快。客户端可以直接从缓存中获取数据,避免去数据库中进行获取数据,减少数据库的I/O操作,提高了数据的查询效率,降低了对数据库的压力。
我们一般会将经常查询的热点数据,不会经常改变的热点数据,保存到缓存中,提高查询效率,从而提高用户的体验度。
缓存的执行流程?
1.客户端发起查询的请求
2.会首先判断缓存中是否有数据
-
如果有,直接返回
-
如果没有,就从数据库查询,再把数据同步到缓存
3.返回数据给客户端
4.当下一次同样的请求访问时,就直接从缓存中获取数据。
你们怎么保证Redis和Mysql的数据一致性?
如果是对数据进行查询,首先会去缓存中进行查询,如果有就直接返回数据给客户端,如果没有我们就会去数据库进行查询,并且将数据同步到缓存中,然后将数据返回给客户端。
原因:当数据发生变化的时候,需要同时去更新Mysql和Redis的数据,由于Mysql和Redis的数据的更新是有先后顺序的,并不能满足事务的ACID特性,所以就会出现数据不一致的问题。
所以在这种情况下能选择的方案有以下几种:
1.先删除缓存再更新数据库(先操作缓存)
先删除缓存再更新数据库在理想情况下是可以解决先更新数据库再更新缓存带来的数据不一致问题,但在某些情况下(例如网络延迟),多个线程并发访问的时候,线程1先删除缓存,再去修改数据库的时候由于网络延迟,线程2在此时去查询数据,发现缓存中没有数据,然后去将数据库中的旧数据又同步到缓存当中了,这时候就会导致数据库中存储的新数据,redis缓存中存储的是老数据,就会导致数据不一致的问题。
2.延迟双删
延迟双删就是用来解决先删除缓存再操作数据库带来的数据不一致问题。我们在线程1删除缓存再更新数据库后,然后延迟几百毫秒再去删除Redis缓存。能够避免redis缓存中的数据和数据库的数据是一致的。我们不能避免其他线程在这几百毫秒读取脏数据,如何避免呢?那就要保证删除缓存和操作数据库的操作是原子性的来保证数据的强一致性,那就是加锁。但是加锁会影响系统的响应效率(吞吐量),我们引入redis就会为了提高系统的响应效率,所以说这样就得不偿失了,可用性和一致性只能保证一个。为了保证系统的高可用性我们不会采用数据的强一致性而是采用数据的最终一致性。
上述延迟时间具体多长时间要根据1.评估数据库操作的时间,2.网络延迟等因素。
3.先更新数据库再删除缓存(先操作数据库)
如果先更新数据库再删除缓存,在高并发情况下,在线程1更新数据库的同时有一个或多个线程进行访问,就会读取到缓存中的脏数据,从而导致数据不一致问题,但是可以保证数据的最终的一致性。
但是还是会出现极端情况,就是在删除Redis缓存的时候由于某些原因导致删除缓存失败了,这时候缓存中就一直存储的是脏数据,只有等待redis存储的数据过期才能拿到最新的数据。如何解决呢?
使用延迟双删+MQ重试机制。
我们可以使用删除重试的机制,无论是先更新数据库还是先删除redis缓存,都有可能删除redis失败的问题,通过引入MQ,如果删除redis缓存失败,我们异步发送消息道MQ中,通过系统监听MQ,一旦监听到Redis删除失败的消息,就触发重试机制进行重新删除Redis缓存。
上述还是有一定的缺点就是代码的耦合度太高了。如何解决呢?可以通过cannal进行解耦。
4.canal实现数据一致性
canal(阿里巴巴开源组件)主要用途是用于对Mysql数据库增量日志解析,提供增量数据订阅和消费。通过Canal组件监控Mysql的binlog日志把更新后的数据同步到Redis当中。
线程1进行删除缓存操作,由于在更新数据库的时候因为网络延迟,线程2查询数据又将旧的数据同步到redis缓存当中,所以这段时间读取到的数据一直都是脏数据。在更新数据库成功后,canal监听到mysql的增量日志,然后延迟删除redis缓存。如果删除redis缓存失败,则通过异步发送消息到MQ当中,然后canal通过监听MQ进行重试删除redis缓存,实现数据的最终一致性。
什么是canal?
canal是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费。简而言之,canal就是一个同步增量数据的一个工具。
Canal 的工作原理:主要基于数据库的日志文件(如 MySQL 的 binlog),通过解析日志文件来捕获数据变化。从而将这些数据变化同步到redis、kafka等当中,实现数据的一致性。
跟canal类似的还有flink CDC和DataX,这些都是用来做数据同步的工具。
SpringCache常用注解
cache:缓存,evict:驱逐(删除)
Springcache是一种基于注解的缓存技术框架。
@EnableCaching:打在主启动类上,开启缓存功能。
@Cacheable: 用于缓存方法的结果。先查询缓存中是否有,有就直接返回,没有就去数据库进行查询,并同步到缓存中。
@CacheEvict:用于在方法执行后删除缓存中的数据。可以用于清除过期或不再需要的缓存数据。
@CachePut: 用于在方法执行后更新缓存中的数据。
@Caching: 用于组合多个缓存注解进行使用。可以在一个方法上同时使用多个 @Cacheable
、@CacheEvict
和 @CachePut
注解。
@CacheConfig: 用于在类级别上配置缓存的公共配置。可以抽取类中所有 @Cacheable
、@CacheEvict
和 @CachePut
注解的公共配置。
举一个例子来说明Spring Cache
假设我们有一个UserService服务,它提供了查询用户信息的方法getUserById,这个方法接受一个用户ID,返回一个User对象。我们希望这个方法的性能比较高,所以想使用缓存来优化它。
首先,在UserService类中添加@EnableCaching注解,开启缓存功能:
@Service
@EnableCaching
public class UserService {
...
}
然后,在getUserById方法上添加@Cacheable注解,表示使用缓存:
@Service
@EnableCaching
public class UserService {
@Cacheable(value = "userCache", key = "#userId")
public User getUserById(Long userId) {
// 从数据库中查询用户信息,并返回User对象
}
}
这个注解表示使用userCache这个缓存,这个缓存的key是根据用户ID(userId)生成的。
接着,在Spring的配置文件中,我们需要配置一个缓存管理器,例如Ehcache:
<bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager">
<property name="cacheManager">
<bean class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
<property name="configLocation" value="classpath:ehcache.xml" />
</bean>
</property>
</bean>
这个缓存管理器使用了Ehcache库,并且配置文件为ehcache.xml。
最后,在web层调用UserService服务的getUserById方法,就可以使用缓存了:
@RestController
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/user/{userId}")
public User getUserById(@PathVariable Long userId) {
return userService.getUserById(userId);
}
}
当第一次调用getUserById方法时,缓存中没有数据,方法会从数据库中查询用户信息,并返回User对象。在返回之前,Spring Cache会将这个对象存储到缓存中。之后再次调用getUserById方法时,Spring Cache会先检查缓存中是否有对应的数据,如果有则直接返回缓存中的数据,从而提高了方法的性能。
Redis缓存击穿,穿透,雪崩?如何解决?
什么是Redis缓存 雪崩、穿透、击穿?【详解】_redis什么是熔断器_MXin5的博客-CSDN博客
缓存击穿:
指的是Redis中某个热点key,在高并发的情况下,这个热点数据过期了或被淘汰了,导致本来访问这个热点的所有请求中打到数据库上,导致数据库压力剧增,这种现象就叫做缓存击穿。
解决方案:
1.将热点key不设置过期时间。这种方式不太好。
2.使用锁,单体使用互斥锁,集群使用分布式锁。在访问数据库的时候进行加锁,当热点key过期的时候,只有一个线程能够获取到这个锁(互斥锁/分布式锁),线程获取到数据后将数据同步到redis缓存中,其他线程就直接从缓存中获取数据,就可以避免大量的请求都会落到数据库上。
缓存穿透:
缓存穿透指查询不存在的数据。用户访问的数据在Redis缓存中和数据库中都没有这样的数据,然后用户不断地使用脚本发送这个请求【查询数据库中id为-1的数据,数据库中的id是自增长从0开始的,没有负数】,这种数据直接穿透缓存,打到数据库上,导致数据库压力剧增,这种现象就叫做缓存穿透。
解决办法:
1.缓存空对象。即使Redis和数据库都没用这样的数据,也将空对象同步到Redis缓存当中,这样下次同样的请求再次查询的时候,直接从Redis缓存中进行获取,而不会去数据库进行查询。
2.对这个IP进行拉黑。但是他也可以换不同的ip进行访问。
3.对这个请求参数进行合法校验。如果不合法直接return掉。
4.使用BloomFilter(布隆过滤器),布隆过滤器粗略来说就是可以使用布隆过滤器存储数据库中的数据,如果从布隆过滤器中查询不到,那么数据库中一定没有。
缓存雪崩:
就是大量的redis缓存中的key在同一时间大面积的失效(过期),大量的请求直接打到数据库上,导致数据库压力剧增,这种现象就是缓存雪崩。
-
解决方案:
- 为key设置不同的过期时间,在设置过期时间的时候随机初始化这个失效时间,避免都在同一个时间过期。
- 使用Redis集群部署,将热点key平均的分布在不同redis节点上,降低单个节点的压力。
- 不设置过期时间(暴力方式),虽然可以解决雪崩问题,但是会导致内存压力增大。
- 增加定时任务,定时的去刷新这个缓存,假如redis缓存过期时间为3小时,2小时50分的时候就重新刷新redis缓存在设置3小时过期时间。
你们Redis用来做什么?使用的什么结构?
字符串(String)
-
字符串类型适用于存储简单的数值型数据或字符串型数据,如缓存、计数器、最近登录用户、订单号等等。
实际需求:用 Redis 储存商品库存数据,当销售一件商品时,实时减少库存。
// 获取 Redisson 实例
RedissonClient redisson = Redisson.create();
// 获取字符串对象
String stockKey = "stock:001";
// 设置库存数量
redisson.getBucket(stockKey).set("100");
// 模拟销售一件商品
long decrement = redisson.getBucket(stockKey).decrementAndGet();
if (decrement < 0) {
System.out.println("商品库存不足,无法完成交易");
}
列表(List)
-
列表类型适用于缓存商品列表、文章列表,最新的操作历史记录、消息队列等,也可用于实现Java中的栈和队列。
实际需求:用 Redis 储存商品订单数据,按照购买时间排序。
// 获取 Redisson 实例
RedissonClient redisson = Redisson.create();
// 获取列表对象
String orderListKey = "orders";
// 添加订单数据到列表中
Order order = new Order("001", new Date());
redisson.getList(orderListKey).add(order);
// 按照购买时间排序
redisson.getList(orderListKey).sortBy(new Comparator<Order>() {
@Override
public int compare(Order o1, Order o2) {
return o1.getPurchaseTime().compareTo(o2.getPurchaseTime());
}
});
集合(Set)
-
集合类型适用于去重查询场景,如某个商店的颜色集合、喜好集合等,适合实现朋友圈等互相关注场景。
实际需求:用 Redis 储存用户关注列表数据,实现互相关注查询。
// 获取 Redisson 实例
RedissonClient redisson = Redisson.create();
// 获取集合对象
String followingSetKey = "user:001:following";
String followersSetKey = "user:001:followers";
// 增加关注
redisson.getSet(followingSetKey).add("user:002");
// 获取关注列表
Set<String> followingSet = redisson.getSet(followingSetKey).readAll();
// 获取粉丝列表
Set<String> followersSet = redisson.getSet(followersSetKey).readAll();
// 判断是否互相关注
if (followingSet.contains("user:002") && followersSet.contains("user:002")) {
System.out.println("互相关注");
}
有序集合(Sorted Set)
-
有序集合类型适用于按照分数或者权重进行排序、筛选场景,如商品积分排名、排行榜、热搜词等,实现Java中的 TreeMap 和 PriorityQueue。
实际需求:用 Redis 储存商品评价数据,按照评分分值排序。
// 获取 Redisson 实例
RedissonClient redisson = Redisson.create();
// 获取有序集合对象
String reviewSortedSetKey = "product:001:reviews";
// 添加评价数据到有序集合中
Review review = new Review("user:001", 4.5);
redisson.getScoredSortedSet(reviewSortedSetKey).add(review.getScore(), review);
// 获取评价列表(按照评分从高到低排序)
Set<Review> reviewSet = redisson.getScoredSortedSet(reviewSortedSetKey).valueRangeReversed(0, -1);
哈希表(Hash)
-
哈希表类型适用于存储和查询更复杂的场景,如各种用户信息,人物角色的属性信息,等等。
总体而言,基于五种不同的 Redis 数据结构,在 Java 开发过程中可以方便地实现缓存、计数器、用户列表、排行榜、点赞、粉丝、朋友圈等各种数据存储需求,并且能够获取到快速的性能以及灵活性。
实际需求:用 Redis 储存用户信息数据,根据邮箱地址查询用户数据。
// 获取 Redisson 实例
RedissonClient redisson = Redisson.create();
// 获取哈希表对象
String userHashKey = "user:001";
// 增加用户信息数据到哈希表中
User user = new User("user@mail.com", "用户", "123456");
Map<String, String> userMap = new HashMap<>();
userMap.put("name", user.getName());
userMap.put("password", user.getPassword());
redisson.getMap(userHashKey).putAll(userMap);
// 根据邮箱查询用户信息数据
Map<String, String> userInfo = redisson.getMap(userHashKey).getAll(Set.of("name", "password
项目是如何做服务降级的?
比如在商品业务中,需要实时从redis中查询库存,通过设置hystrix的最大信号量,以此来防止redis雪崩。当并发过高,请求数超过最大信号量,触发降级,直接向客户端返回兜底数据:”活动太火爆啦,请稍候重试“。
下面用一个实际代码进行举例:
假设我们有一个用户登录系统,该系统的登录接口每秒可以支持数万的并发请求。如果在某一天突然出现大量黑客攻击或者异常高的请求量,超出了系统的承载范围,此时系统可能会发生宕机或者崩溃。为了保证服务的稳定性和可用性,我们可以采用服务降级的方式来解决此问题。
在这种情况下,我们可以使用熔断降级技术来进行服务降级,具体实现步骤如下:
- 定义一个熔断器组件,用于监测接口请求的相应时间和调用失败的比例,并根据一定的阈值触发熔断机制。
// CircuitBreaker类:熔断器组件,用于监测接口请求的相应时间和调用失败的比例,并根据一定的阈值触发熔断机制。
public class CircuitBreaker {
private int consecutiveFailures = 0; // 连续失败次数
private int failuresThreshold; // 失败次数阈值
private long coolDownTime; // 熔断恢复时间
private boolean isTripped; // 是否处于熔断状态
// 构造函数,初始化熔断器组件。
public CircuitBreaker(int failuresThreshold, long coolDownTime) {
this.failuresThreshold = failuresThreshold;
this.coolDownTime = coolDownTime;
isTripped = false; // 初始化熔断器状态为未熔断
}
// 记录接口成功相应的方法,重置连续失败次数,并恢复熔断器状态。
public synchronized void recordSuccess() {
consecutiveFailures = 0;
if (isTripped) { // 如果当前处于熔断状态,则恢复熔断器状态
System.out.println("熔断器恢复正常...");
isTripped = false;
}
}
// 记录接口失败的方法,增加连续失败次数,并触发熔断机制。
public synchronized void recordFailure() {
consecutiveFailures++; // 连续失败次数+1
if (consecutiveFailures >= failuresThreshold && !isTripped) { // 如果连续失败次数超过了阈值,并且当前未处于熔断状态,则触发熔断机制
System.out.println("接口熔断..."); // 打印熔断日志信息
isTripped = true; // 设置熔断器状态为已熔断
new Thread(() -> { // 使用新线程进行熔断器恢复操作
try {
Thread.sleep(coolDownTime); // 等待熔断恢复时间
} catch (InterruptedException e) {
e.printStackTrace();
}
isTripped = false; // 恢复熔断器状态
consecutiveFailures = 0; // 重置连续失败次数
System.out.println("熔断器恢复正常...");
}).start();
}
}
// 获取当前熔断器状态。
public boolean isTripped() {
return isTripped;
}
}
- 在登录接口中添加熔断器组件,监测接口请求的相应时间和调用失败的比例。
// LoginService类:用户登录系统的接口服务类。
public class LoginService {
private CircuitBreaker circuitBreaker; // 引入熔断器组件
// 构造函数,初始化熔断器组件。
public LoginService() {
circuitBreaker = new CircuitBreaker(10, 5000); // 连续失败次数阈值为10,熔断恢复时间为5000毫秒
}
// 用户登录接口,返回登录结果。
public boolean login(String username, String password) {
// 如果当前熔断器状态为已熔断,则直接返回错误信息。
if (circuitBreaker.isTripped()) {
System.out.println("接口已被熔断,请稍后再试...");
return false;
}
try {
// 模拟登录接口,随机抛出异常
if (Math.random() < 0.8) {
throw new RuntimeException("系统异常...");
}
// 模拟登录验证
if (username.equals("admin") && password.equals("123456")) {
circuitBreaker.recordSuccess(); // 登录成功,记录接口成功相应
System.out.println("登录成功...");
return true;
} else {
circuitBreaker.recordFailure(); // 登录失败,记录接口失败相应
System.out.println("用户名或密码错误...");
return false;
}
} catch (Exception e) { // 登录失败,记录接口失败相应
circuitBreaker.recordFailure();
System.out.println("系统异常...");
return false;
}
}
}
通过上述实现,当连续失败次数超过阈值时,接口请求会被熔断,此时将直接返回错误信息,保证系统的稳定性和可用性。当熔断器状态恢复后,接口请求会被重新响应。
基于Redis实现的分布式锁?
实现分布式锁的三种方式_nacos分布式锁_MXin5的博客-CSDN博客
1基于数据库实现:通常基于主键,或者唯一索引来实现分布式锁,但是性能比较差,一般不建议使用
2基于Redis实现分布式锁:可以使用setnx来加锁 ,但是需要设置锁的过期时间来防止死锁,所以要结合expire使用.为了保证setnx和expire两个命令的原子性,可以使用set命令组合【将setnx和expire结合成一行代码】。总之自己封装Redis的分布式锁是很麻烦的,我们可以使用Redisson来实现分布式锁,Redisson已经封装好了。
3.基于zookeeper : 使用临时顺序节点+监听实现,线程进来都去创建临时顺序节点,第一个创建节点的线程获取到锁,后面的节点监听自己的上一个节点的删除事件,如果第一个节点被删除,释放锁后,第二个节点就成为第一个节点,获取到锁,执行自己的操作。在项目中可以使用curator,这个是Apache封装好的基于zookeeper的分布式锁方案。
Redis如何实现分布式锁,用什么命令?
可以使用setnx(k,v)命令来加锁 ,setnx命令(set if not exists)只有在当且仅当key不存在的时候才设置value并返回true表示加锁成功,如果key值存在,不会做任何操作并返回false表示加锁失败。为了防止某些原因造成死锁,所以要对分布式锁设置过期时间,所以要结合expire使用。为了保证setnx和expire两个命令的原子性,可以使用set命令组合。
// 返回值为1表示获取锁成功,返回值为0表示获取锁失败,已经有线程获取到锁了。
if(jedis.setnx(lock_stock,1) == 1){ //获取锁
expire(lock_stock,5) //设置锁超时
try {
业务代码
} finally {
jedis.del(lock_stock) //释放锁
}
}
// 用来解决原子性问题,在设置锁过期时间之前出现异常,导致没有设置锁过期时间,出现死锁。
if(set(lock_stock,1,"NX","EX",5) == 1){ //获取锁并设置超时
try {
业务代码
} finally {
del(lock_stock) //释放锁
}
}
项目中怎么使用分布式锁的?
自己封装Redis的分布式锁是很麻烦的,我们可以使用Redissoin来实现分布式锁,Redissoin已经封装好了Redis实现分布式锁的步骤。
使用场景:比如说订单支付成功和订单超时取消不能同时执行,必须等待前者或后者执行完成才能执行,就可以使用Redission的分布式锁。【再比如吃饭和拉便便的两个业务方法,只能一个先一个后,不然就会出现冲突和出现bug。】
进行代码举例说明Redission实现分布式锁:
本例中测试类中创建了3个线程,分别对共享资源进行访问,通过设置分布式锁,保证同一时刻只能有一个线程对资源进行访问。
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import java.util.concurrent.TimeUnit;
public class RedissonTest {
public static void main(String[] args) {
// 创建Redisson客户端
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redissonClient = Redisson.create(config);
// 定义锁的名称
String lockName = "my_lock";
// 创建三个线程,同时访问共享资源
Thread t1 = new Thread(() -> {
// 获取分布式锁
RLock lock = redissonClient.getLock(lockName);
lock.lock();
try {
System.out.println("Thread 1 start executing");
Thread.sleep(5000); // 模拟业务执行时间
System.out.println("Thread 1 finish executing");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放锁
lock.unlock();
}
});
Thread t2 = new Thread(() -> {
// 获取分布式锁
RLock lock = redissonClient.getLock(lockName);
lock.lock();
try {
System.out.println("Thread 2 start executing");
Thread.sleep(5000); // 模拟业务执行时间
System.out.println("Thread 2 finish executing");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放锁
lock.unlock();
}
});
Thread t3 = new Thread(() -> {
// 获取分布式锁
RLock lock = redissonClient.getLock(lockName);
lock.lock();
try {
System.out.println("Thread 3 start executing");
Thread.sleep(5000); // 模拟业务执行时间
System.out.println("Thread 3 finish executing");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放锁
lock.unlock();
}
});
// 启动三个线程
t1.start();
t2.start();
t3.start();
}
}
运行上述代码,输出结果如下:
Thread 1 start executing
Thread 1 finish executing
Thread 2 start executing
Thread 2 finish executing
Thread 3 start executing
Thread 3 finish executing
可以看到,通过设置分布式锁,确保了同一时刻只有一个线程能够对共享资源进行访问(谁获取到了就谁用),从而避免了竞态条件。
Redisson的看门狗原理?
Redisson对基于Redis实现分布式锁进行了封装,对于锁超时问题 (Redission在设置的过期时间以内还没有执行完对共享资源的操作,锁就过期了),它提供了看门狗原理,负责定时(默认过期时间的30秒/3)检测所有Redis中的key的过期时间,自动进行对Redis中过期key进行删除,和对Redis中的key到了过期时间还没有完成的任务的锁的及时续期。为分布式锁提供了一种有效的锁的自动续期策略。
高并发分布式锁如何优化?
1.通过缩小锁的粒度
锁锁住的代码范围越少,那些代码没有并发问题,一定要放在锁的外面,缩小锁的粒度从而提高系统的性能。
2.通过分段锁
假如我们之前在高并发场景使用一个key存储10000的库存,然后我们采用分段锁的思路设置10个key,每个key包含100个库存,然后对每一个key都要加上锁,然后我们通过代码设置轮询的方式去获取每个key去扣减库存,这样是不是并发就是之前的10倍。
接口幂等一般怎么设计?
接口的幂等性怎么设计?_Mark66890620的博客-CSDN博客_接口的幂等性怎么设计
接口幂等就是客户端发送重复的请求时,服务器能够返回的结果是一样的。
接口幂等一般可以通过以下方式实现:
使用唯一标识符
为每个请求分配唯一的标识符,可以使用UUID等机制生成唯一标识符。在接收到客户端请求时,服务器会先检查该请求是否已经处理过,如果已经处理,则直接返回之前的响应。否则,服务器会执行该请求并缓存响应结果和标识符,以便后续的重复请求可以直接返回之前的响应。
使用数据库唯一约束
在数据库中使用唯一约束(主键)来确保操作的幂等性。例如,在插入数据时,使用唯一约束来确保数据不会重复插入。
使用幂等键
在请求中包含一个幂等键,服务器端根据幂等键来判断请求是否已处理。例如,在支付接口中,使用订单号作为幂等键,可以避免重复下单。
如何实现防止重复提交?
1.前端使用按钮置灰
按钮置灰,点击一次无法再次点击,但是刷新页面,这时候做了按钮置灰也没有用,就可以继续点击了,会出现一定的问题。
2.后端/前端生成唯一标识(UUID)
进入到下单页面,调用后端接口获取唯一标识(订单号)或直接前端生成唯一标识(订单号),用户提交订单判断是否存在重复订单号,如果存在则下单失败,如果不存在则下单成功。
3.从业务的本质入手
通过将用户id+分隔符+商品id拼接,设置为redis的key,并设置过期时间,到了过期时间自动删除,这样在短时间内重复使用相同的key进行调用提交订单的接口,会先去判断redis中是否存在,如果存在,说明重复提交了。
什么是Redission的信号量(Semaphore)?
Redisson的信号量是一种分布式的计数信号量。作用:通过维护一个计数器来控制并发访问同一个资源的数量,确保系统在高并发场景下的稳定性和可靠性。
具体来说,信号量可以用于以下场景:
- 控制并发访问:在多线程环境下,控制并发访问资源的数量,确保系统的稳定性和一致性。
- 限流:限制同时访问某个资源的线程数量,从而避免系统过载。
- 资源管理:控制对有限资源的访问,如数据库连接等。
当一个系统中某些接口需要限制同时访问的请求数量时,可以使用 Redisson 的信号量作为限流工具。
下面是一个简单的 Java 代码例子,通过 Redisson 信号量实现对某个 API 的访问率限制:
import org.redisson.Redisson;
import org.redisson.api.RSemaphore;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.TimeUnit;
public class ApiRateLimiter {
private final static Logger logger = LoggerFactory.getLogger(ApiRateLimiter.class);
// 最大访问并发数
private static final int MAX_CONCURRENT_REQUESTS = 10;
// 每秒最大访问次数
private static final int MAX_ACCESS_PER_SECOND = 5;
// 创建 Redisson 客户端
private static RedissonClient redissonClient = getRedissonClient();
private static RedissonClient getRedissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://localhost:6379")
.setDatabase(0)
.setConnectionMinimumIdleSize(5)
.setConnectionPoolSize(100);
return Redisson.create(config);
}
public boolean tryAcquire(String api, long timeout, TimeUnit unit) throws InterruptedException {
// 使用 API 名称作为信号量的名称
RSemaphore semaphore = redissonClient.getSemaphore(api);
// 尝试获取信号量,等待一定时间
boolean success = semaphore.tryAcquire(MAX_ACCESS_PER_SECOND, timeout, unit);
if (!success) {
logger.info("API \"" + api + "\" is overloaded.");
return false;
}
return true;
}
}
在上述示例代码中,我们通过 Redisson 的 RSemaphore
获取了一个信号量对象,以 API 名称作为信号量的名称,并且设置信号量的初始值为 5(为了防止第一次请求直接超出最大访问次数)。接着,在 tryAcquire
方法中,通过 tryAcquire
方法尝试获取信号量,等待指定时间。
如果成功获取信号量,则可以访问 API,否则等待时间到期仍未获取到信号量,则认为 API 超负荷,并且返回 false
。
常见的秒杀场景使用到Redission的信号量:
Redisson 的信号量可以用于秒杀活动的库存控制。下面是一个简单的 Java 代码例子,演示如何使用 Redisson 的信号量来实现秒杀活动的库存控制:
import org.redisson.Redisson;
import org.redisson.api.RSemaphore;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.TimeUnit;
public class SeckillService {
private final static Logger logger = LoggerFactory.getLogger(SeckillService.class);
// 秒杀商品的库存
private static final int INVENTORY = 100;
// 秒杀活动的名称,作为 Redisson 信号量的名称
private static final String SECKILL_EVENT = "seckill_event";
// 创建 Redisson 客户端
private static final RedissonClient redissonClient = getRedissonClient();
private static RedissonClient getRedissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://localhost:6379")
.setDatabase(0)
.setConnectionMinimumIdleSize(5)
.setConnectionPoolSize(100);
return Redisson.create(config);
}
public void seckill(String userId) throws Exception {
// 获取 Redisson 信号量
RSemaphore semaphore = redissonClient.getSemaphore(SECKILL_EVENT);
// 尝试获取信号量,等待 1 秒钟
boolean success = semaphore.tryAcquire(1, TimeUnit.SECONDS);
if (!success) {
logger.info(userId + " - Seckill Failed: Stock is empty.");
throw new Exception("Stock is empty.");
}
try {
// 进行秒杀业务逻辑处理
if (INVENTORY > 0) {
logger.info(userId + " - Seckill Success: inventory = " + INVENTORY);
INVENTORY--;
} else {
logger.info(userId + " - Seckill Failed: Stock is empty.");
throw new Exception("Stock is empty.");
}
} finally {
// 释放 Redisson 信号量
semaphore.release();
}
}
}
在上述示例代码中,我们先设置了秒杀商品的库存 INVENTORY
为 100,然后利用 Redisson 的信号量功能,通过 redissonClient.getSemaphore
获取名称为 SECKILL_EVENT
的信号量对象。
在 seckill
方法中,我们首先通过 tryAcquire
尝试获取 Redisson 信号量,等待 1 秒钟,如果没有获取到,则表示没有库存了,直接抛出异常并返回。如果获取到了信号量,则进行秒杀业务逻辑处理,在业务处理完成后,通过 release
方法释放 Redisson 信号量。
这样,当用户同时发起多个秒杀请求时,只有有限的请求能够成功获取 Redisson 信号量,进而执行秒杀业务逻辑,然后释放 Redisson 信号量,其他请求则被拦截并抛出异常。这样,可以有效地避免因高并发而导致的商品超卖等问题。
秒杀场景为什么要Redis的存储结构hash?不用其他的存储方式?
在查询的时候需要查询单个商品和商品集合的,还有可能需要对商品进行修改,使用hash的好处就是直接通过key去获取单个对象或者集合。
其他的存储结构,查询单个都不是很方便,需要进行遍历查询,非常损耗性能,这时候使用hash直接使用key就可以进行获取。
查询单个商品使用get(redisTemplate.opsForHash().get)
查询集合列表使用values(redisTemplate.opsForHash().values)
Redis的部署模式
Redis四种部署模式(原理、优缺点及解决方案)_少年做自己的英雄的博客-CSDN博客_redis部署模式
一、单节点模式
单节点模式只有一个主节点Master,结构简单但是会出现单点故障且不能实现高可用。
二、主从复制模式
主从模式是一种高可用的解决方案,是一种复制方式,它分为一个主节点服务器和一个或多个从节点服务器。主节点负责所有的写请求和部分读请求,从节点只负责读请求并复制主节点的数据。当主节点发生故障的时候,需要手动的可以将其中的一个从节点提升至主节点,保证系统的高可用。
解决了:主从模式解决了Redis的读取的压力,但不能解决写的压力,不能解决单点故障。
三、哨兵模式
哨兵模式的出现用于解决主从复制模式中无法自动升级主节点的问题,哨兵模式通过监控主从节点的状态,当主节点挂掉的时候,自动选择一个最优从节点升级为主节点【选举】,保证了额系统的可靠性和稳定性。
但哨兵如果挂了怎么办?哨兵一般都会是一个集群,是集群高可用的心脏,一般由3-5个节点组成,即使个别节点挂了,集群还可以正常运行。
四、集群模式
Redis集群模式是通过将数据分散存储到多个主从节点上,从而实现分布式的数据存储和访问。Redis集群分为两种模式:分片式集群和复制式集群。在分片式集群中,数据被分为多个片段,不同的数据片段存储在不同的节点上。在复制式集群中,每个节点都是数据的完整的副本。
什么情况下Redis集群不可用?
-
网络问题:Redis的可用性依赖于网络的稳定性和可靠性,如果网络出现故障、延迟、丢包等问题,可能导致Redis集群不可用。
-
硬件故障:硬件故障是造成Redis集群不可用的常见原因之一。例如,硬盘故障或者内存故障都可能导致Redis实例崩溃并且数据丢失。
-
磁盘空间不足:Redis需要足够的磁盘空间来存储数据和日志文件,如果磁盘空间不足,Redis可能无法工作。
-
错误的配置:一些错误的配置选项可能导致Redis集群不可用。例如,如果Redis集群的最大客户端连接数限制设置过低,可能导致Redis无法正常处理客户端请求。
-
Redis版本问题:某些版本的Redis存在已知的bug或性能问题,在使用时需要注意避免这些问题导致Redis集群不可用。
-
高负载:Redis集群的性能容量与负载有关,在过高的负载下,Redis集群可能出现慢查询、负载不均衡、内存溢出等问题,导致Redis集群不可用。
Redis存储结构底层有没有了解?什么是SDS?
Redis的数据存储结构是多种多样的,包括字符串、列表、集合、哈希表、有序集合等。
SDS(Simple Dynamic String)是Redis中常用的一种字符串实现方式。SDS除了存储字符串内容之外,还维护了字符串长度、可用容量以及类型信息等元数据。这使得Redis可以在不重新分配内存的情况下进行字符串的长度修改、拼接、截取等操作,同时也方便了Redis进行数据类型判断和内存管理。
SDS的底层实现主要包括以下几个部分:
-
头部信息:SDS的头部信息保存了该字符串的字节数、已经使用的空间大小以及使用次数等元数据。
-
字符数组:存储实际字符串内容的字符数组。
-
空间管理器:Redis使用空间管理器(如SDS header、tail、free等指针)来跟踪SDS的空间分配和释放情况,以及管理SDS扩容和缩小的行为。
总之,SDS是一种支持可变长度字符串的数据结构,并且能够提供高效的字符串操作和内存管理。Redis使用SDS作为字符串的底层实现,这也体现了Redis在性能和可扩展性方面的优越性能。