Redis概述
redis是一个高性能的内存键值对数据库,可用于数据库缓存,计数器,消息代理,分布式锁。支持String,List,Hash,Set,SortedSet五种数据类型。
与传统数据库采用磁盘存储数据相比,Redis是一种内存优化型数据库,读写速度快
五种数据类型
String
最简单的存储结构,根据value的不同类型,可分为以下几种情况:
- 简单字符串(Simple String):当value是一个普通字符串时,可将其视为简单字符串,这种类型的值没有任何特殊的数据结构或语义
- 整数(Integer):当value可以被解析为整数时,Redis会将其存储为整数类型。这样可以在执行一些特定的命令时提供更高效的操作。如自增(INCR)和自减(DECR)
- 浮点数(Floating Point):当value可以被解析为浮点数时,Redis会将其存储为浮点数类型。浮点数支持一些特定的操作,例如增加(INCRBYFLOAT)和减少(DECRBYFLOAT)。
这个没有用过
4. 位图(Bitmap):当value被用作位图时,可以将其视为一个二进制字符串。Redis提供了一系列的位操作命令,可以对位图进行处理,例如设置(SETBIT)、获取(GETBIT)和统计位的数量(BITCOUNT)。
String类型在Redis中的灵活性和存储容量上的限制确实能够满足大多数常见需求,并且使用简单的字节数组来表示数据,提高了读写效率。
然而,当需要存储复杂结构的数据时,String类型的处理能力相对较弱。此时,可以考虑借助其他工具或结合Redis的其他数据结构,如哈希(Hash)、列表(List)、集合(Set)或有序集合(Sorted Set)来处理复杂的数据操作。
总体而言,Redis的String类型是非常实用和常用的数据类型之一,对于大部分简单数据的存储和读取来说,String类型都会是一个很好的选择。对于更复杂的数据结构和处理需求,可以结合其他Redis数据结构来实现更灵活的功能。
String中的命令:
SET key value:设置指定key的值为value。
GET key:获取指定key的值。
INCR key:将指定key的值加1,并返回增加后的值。
DECR key:将指定key的值减1,并返回减少后的值。
INCRBY key increment:将指定key的值增加指定的增量值(整数)。
DECRBY key decrement:将指定key的值减少指定的减量值(整数)。
INCR和DECR只能操作value为整数类型key,如果对其他类型的value操作会出现
ERR value is not an integer or out of range
APPEND key value:将指定value追加到指定key的值末尾。
可以理解为StringBuilder的append()方法
STRLEN key:获取指定key的值的长度。
MSET key1 value1 key2 value2 …:同时设置多个key-value对。
MGET key1 key2 …:同时获取多个key的值。
SETEX key seconds value:设置指定key的值,并指定过期时间(以秒为单位)。
SETNX key value:如果指定key不存在,则设置它的值为value。
GETSET key value:设置指定key的值,并返回原来的值。
Hash
散列,value为无序字典,相当于Map<Object,Map<Object,Object>>,适合存储对象和结构化数据。常见的使用场景包括:
- 缓存对象:将对象以哈希结构存储在Redis中,可快速读取,修改和删除对象的某个字段
- 存储用户信息:可以将用户的ID作为key,将用户信息的各个字段存储为哈希结构的field和value。
- 计数器:使用哈希结构的field来存储计数器的名称,value存储实际的计数值,方便对计数器进行增量操作。
- 应用配置:可以将应用的配置信息以哈希结构的形式存储,方便读取和更新配置。
通过合理使用哈希结构,可以有效地管理和操作复杂的数据,并且提供了快速的读写能力。需要注意的是,当哈希结构中的字段数量较多时,可能会影响性能和内存消耗
存储的数据大概长这样
常见命令
HSET key field value:设置指定key下的指定field的值为value。
HGET key field:获取指定key下指定field的值。
HMSET key field1 value1 field2 value2 …:同时设置多个field-value对。
HMGET key field1 field2 …:同时获取多个field的值。
HGETALL key:获取指定key下所有的field-value对。
HDEL key field1 field2 …:删除指定key下的一个或多个field。
HEXISTS key field:检查指定key下的指定field是否存在。
HLEN key:获取指定key下field的数量。
HINCRBY key field increment:将指定key下的指定field的值增加指定的增量值(整数)。
HKEYS key:获取指定key下所有的field。
HVALS key:获取指定key下所有的value。
特点
- 存储多个字段和对应的值:哈希结构可以存储多个字段和对应的值,从而可以将多个相关属性组合在一起存储,类似于关联数组或字典。
- 快速查询单个字段值:通过指定哈希的key和field,可以在常量时间复杂度内快速获取到对应的值,不受哈希中字段数量的影响。
- 高效的存储和读取:哈希结构使用简单的字节数组表示数据,读写操作都是在内部的字节数组上进行,因此存储和读取的效率都非常高。
- 字段的唯一性:哈希结构中的字段名是唯一的,在同一个哈希中不允许存在重复的字段。
- 支持原子操作:Redis提供了一些针对哈希结构的原子操作,例如设置字段值、删除字段、增加字段值等,这些操作都是原子的,可以确保数据的一致性。
- 灵活性:哈希结构的字段和值可以是任意类型的数据,可用于存储复杂的数据结构,并且可以通过指定的字段进行精确查找和操作。
- 可以节省内存:如果哈希结构中只存储了部分字段,那么它相对于使用多个单独的键值对来存储相同的数据,可以节省一定的内存空间。
List
Redis中List(列表)类型是一种有序、可重复的数据结构。List中的每个元素都有一个索引,可以根据索引对元素进行访问、查找和操作
在存储数据方面,结构相当于Java中LinkedList结构,通过操作两端实现快速插入和删除
根据以上描述可得出List结构的特点:
- 有序性:List中的元素按照插入顺序进行排序,每个元素都有一个索引值,可以根据索引来访问或操作元素。
- 可重复性:List允许存储重复的元素,同一个值可以被插入多次
- 快速的访问和操作:List支持在两端(头部和尾部)进行元素的插入、删除和查找操作。因为List的底层实现是基于链表,所以在头部或尾部插入和删除元素的时间复杂度都是O(1)。
- 支持负索引:List可以使用负数作为索引,负数索引表示从列表末尾开始的位置。例如,-1表示列表中的最后一个元素
- 灵活的数据存储:List可以存储各种类型的元素,包括字符串、整数和其他复杂的数据结构。
- 支持批量操作:Redis提供了一些对List进行批量操作的命令,例如从列表的头部或尾部插入多个元素、删除指定范围的元素等。
- 可以实现队列或栈的功能:通过List的插入和删除操作,可以实现队列(FIFO)或栈(LIFO)等功能。
根据其中的特点,大概可知道一些适用场景:
- 根据可从头部或尾部插入数据和读取数据的特性,可用List实现消息队列,完成简单的消息发布与消费
- 根据有序性特点,可实现排行榜功能,根据用户积分...排行,方便获取指定范围内的数据
- 历史记录:可以使用List记录某个实体的历史操作、状态变更等,可以按时间顺序查看或回放历史记录。
常用命令
LPUSH key value1 [value2 …]:将一个或多个值插入到列表的头部。
RPUSH key value1 [value2 …]:将一个或多个值插入到列表的尾部。
LPOP key:移除并返回列表的头部元素。
RPOP key:移除并返回列表的尾部元素。
LINDEX key index:获取列表指定索引位置上的元素。
LLEN key:获取列表的长度(元素个数)。
LRANGE key start stop:获取列表指定范围的元素,start和stop为索引值。
LINSERT key BEFORE|AFTER pivot value:在列表中指定元素pivot的前或后插入一个新元素。
LSET key index value:设置列表指定索引位置上的元素的值。
LREM key count value:移除列表中指定值的元素,count表示要移除的个数。
BLPOP key1 [key2 …] timeout:从多个列表中按顺序弹出第一个非空列表的头部元素,或阻塞等待一定时间。
BRPOP key1 [key2 …] timeout:从多个列表中按顺序弹出第一个非空列表的尾部元素,或阻塞等待一定时间。
Set
Redis中Set(集合)数据结构是无需不重复的,与HashSet类似,插入元素时,都会调用hash算法得到角标,数据存储顺序无序,元素不可重复,查找快,支持交,并,差集功能,可实现共同好友等功能
常用命令
SADD key member1 [member2 …]:向指定的Set中添加一个或多个元素。
插入数据,如果已经存在的数据,返回值为0
SREM key member1 [member2 …]:从指定的Set中移除一个或多个元素。
SCARD key:获取指定Set的元素数量(集合的基数)。
SISMEMBER key member:判断指定元素是否存在于Set中。
SMEMBERS key:获取指定Set中的所有元素。
SRANDMEMBER key [count]:随机获取Set中指定数量的元素(可重复)。
SPOP key:随机移除并返回Set中的一个元素。
SDIFF key1 [key2 …]:计算多个Set之间的差集,返回差集的结果。
SINTER key1 [key2 …]:计算多个Set之间的交集,返回交集的结果。
SUNION key1 [key2 …]:计算多个Set之间的并集,返回并集的结果。
SSCAN key cursor [MATCH pattern] [COUNT count]:迭代遍历Set中的元素。
贴不了图了
SMOVE source destination member:将指定元素从source Set移动到destination Set。
贴不了图了
SortedSet
可排序集合,每个元素带有一个score属性,可基于score属性对元素排序,底层是一个跳表(SkipList)加hash表,其元素可排序,不重复
特点
- 元素的唯一性:每个元素在Sorted Set中是唯一的,重复的元素会被自动去重。
- 元素的顺序性:每个元素都与一个分数相关联,通过分数可以对元素进行排序。
- 高效的插入和删除:Sorted Set使用跳跃表实现,能够在平均O(log N)的时间复杂度内进行插入和删除操作。
- 分数范围查询:可以根据分数的范围进行查询,快速地找到符合条件的元素。
常见命令
- ZADD:向Sorted Set中添加一个或多个元素。
- ZRANGE:按照元素在Sorted Set中的索引范围获取元素。
- ZRANK:获取元素在Sorted Set中的排名(从0开始计数)。
- ZSCORE:获取元素的分数。
- ZREM:从Sorted Set中移除一个或多个元素。
Java中使用Redis
使用Jedis
方式一:
引入Jedis
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.3.2</version>
</dependency>
测试类
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import redis.clients.jedis.Jedis;
public class JedisTest {
private Jedis jedis;
@BeforeEach
void setUp() {
// 方式1
jedis = new Jedis("127.0.0.1",6379);
jedis.select(0);
}
@Test
void test1(){
String status = jedis.set("jedisKey", "value1");
System.out.println(status);
String jedisKey = jedis.get("jedisKey");
System.out.println(jedisKey);
}
@AfterEach
void release(){
if(jedis!=null){
jedis.close();
}
}
}
方式二:使用JedisPool
为了减少每次使用时的性能开销,先创建连接池
public class JedisPoolUtil {
private static JedisPool jedisPool;
static {
JedisPoolConfig config = new JedisPoolConfig();
// 连接池总数量
config.setMaxTotal(9);
// 连接池最大等待连接数
config.setMaxIdle(9);
// 连接池最小等待连接数
config.setMinIdle(0);
// 等待超时时间
config.setMaxWaitMillis(1000);
jedisPool = new JedisPool(config,"127.0.0.1",6379,1000);
}
public static Jedis getJedis(){
return jedisPool.getResource();
}
}
测试
...
@BeforeEach
void setUp() {
// 方式2
jedis = JedisPoolUtil.getJedis();
jedis.select(0);
}
...
当然,到现在为止,大多数项目都使用的SpringBoot进行开发,所以上面使用Jedis只能算是自己练练手看看还有什么方式创建而已,现在开始讲在SrpingBoot中使用Redis
SpringBoot中使用Redis
在SpringBoot中,通常使用RedisTemplate进行redis的操作
RedisTmplate包括几种序列化方式,默认采用JDK序列化方式(JdkSerializationRedisSerializer),默认方式下存储的key-value都进行了序列化,造成可读性差,内存占用较大
在有需要的情况下,可通过自定义RedisTemplate序列化替换默认方式
@Configuration
void RedisConfig{
@Bean
void RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory connectionFactory){
RedisTemplate<String,Object> redistemplate = new RedisTemplate();
redistemplate.setConnection(connectionFactory);
// 创建序列化工具
GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer();
// 为key与value设置序列化类型
redisTemplate.setKeySerializer(serializer);
redisTemplate.setHashKeySerializer(serializer);
redisTemplate.setValueSerializer(serializer);
redisTemplate.setHashValueSerializer(serializer);
return redisTemplate;
}
}
什么?你问为什么要使用GenericJackson2JsonRedisSerializer这种序列化方式?
自己用了后感觉就是,存储对象不再乱码,且,且redis保存的数据中还存在当前被序列化的类字节码对象,这样的好处是在反序列化中能快速找到当前值的对象,缺点是占用了额外的空间
所以为了节省空间,存储java对象时手动将对象序列化和反序列化,其他类型时采用String序列化器
存储对象手动序列化时,可使用springmvc默认使用json处理工具(ObjectMapper)
SpringBoot中常量的两种序列化实战方案 方式一
1.1. 自定义RedisTemplate
1.2. 修改RedisTemplate的序列化器为GenericJackson2JsonRedisSerializer
方式二
2.1. 使用StringRedisTemplate
2.2. 写入Redis时,手动将对象序列化为JSON
2.3. 读取Redis时,手动将JSON反序列化为对象
四种序列化方式
JdkSerializationRedisSerializer:
√:支持任意Java对象的序列化和反序列化,无需特殊配置。
×:序列化后的数据相对较大,使用旧版本的Java序列化格式,不适合跨语言使用。
Jackson2JsonRedisSerializer:
√:序列化为JSON格式,对人类可读,适合调试和检查。支持多态类型的序列化。
×:只能序列化能够转换为JSON的对象,对于复杂对象需要额外的配置。序列化和反序列化的性能比较一般。
GenericJackson2JsonRedisSerializer:
√:具备Jackson2JsonRedisSerializer的优点,同时支持多态类型的序列化。
×:与Jackson2JsonRedisSerializer相同,序列化和反序列化的性能一般。
StringRedisSerializer:
√:对于键和字符串类型的值,直接使用UTF-8编码进行序列化和反序列化,存储和读取效率高。
×:对于非字符串类型的值,需要自己在应用层进行序列化和反序列化转换。不适合存储复杂对象。
所以:如果需要存储任意类型的Java对象,可以使用JdkSerializationRedisSerializer。如果对可读性和跨语言支持有要求,可以选择Jackson2JsonRedisSerializer或GenericJackson2JsonRedisSerializer。如果主要操作字符串类型的键值对,可以使用StringRedisSerializer来提高性能。
缓存
什么是缓存
通俗的话讲,就是加快访问速度咯
半官方的讲,数据交换缓冲区,存储数据的临时区域,读写性能高,降低后端负载,提高读写效率,降低响应时间
在实际应用中,如果数据一直向缓存中插入,最终也会导致访问数据变慢,所以何时?怎样更新缓存?
缓存更新策略
- 内存淘汰:不用手动维护,当Redis内存不足时自动淘汰部分数据
- 超时淘汰:添加数据时设置数据超时时间(TTL),到期后自动删除
- 主动更新:在业务逻辑中,修改数据库的同时更新缓存
怎样更新缓存大概知道这三点
那么何时更新呢?
使用场景
1 低一致性需求,使用内存淘汰机制
2 高一致性,使用主动更新,并使用超时删除作为兜底方案
缓存穿透
八股文大家都背的很熟了,穿透指缓存中不存在需要查询的数据,导致每次查询都到必须到底层数据源中查询,从而绕过缓存。这可能是由于恶意攻击、频繁查询不存在的数据或缓存数据失效等原因引起的。
解决方案
方案一:缓存空值
当查询的数据不存在时,可以将空结果(如null)缓存起来,同时设置一个较短的过期时间。在后续查询中,可以命中缓存并返回空结果,避免频繁查询底层数据源
例:
public Object cachePenetration(Long id) {
String redisKey = "";
// 1. 查询缓存中是否存在数据
String redisJson = stringRedisTemplate(redisKey + id);
// 2. 存在数据直接返回
if(StringUtil.isNotBlank(redisJson)) {
return JSONUtil.toBean(redisJson, Object.class);
}
// 2.1. 存在数据但是为换行符等假数据,返回null
if(redisJson != null) {
return null;
}
// 3. 不存在数据,查询数据库
Object result = this.getById(id);
// 4. 数据库中不存在,设置null值并缓存到redis中
if(result == null) {
stringRedisTemplate.opsForValue().set(redisKey + id, JSONUtil.toJsonStr(result), 100, TimeUnit.SECONDS)
return null;
}
// 5. 存在将数据缓存到redis中
stringRedisTemplate.opsForValue().set(redisKey + id, "", 100, TimeUnit.SECONDS);
// 6. 返回对象
return null;
}
方案二:热点数据预加载
为了避免热点数据(访问量高的数据)在使用过程中出现缓存穿透问题,可以将数据预先加载到缓存中,以减少缓存穿透的影响。可以通过定时任务或在系统启动时加载这些数据
哈哈哈,这里只有理论了,代码自己试试咯
方案三:异步加载数据
当发现查询的数据不存在缓存中时,可以通过异步方式从底层数据源加载数据并更新缓存,避免阻塞查询线程,提高系统吞吐量。
@Service
public class MyService {
private final RedisTemplate<String, String> redisTemplate;
public MyService(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}
public String getData(String key) {
String data = redisTemplate.opsForValue().get(key);
if (data == null) {
loadDataAsync(key);
}
return data;
}
@Async // 标记为异步方法
public CompletableFuture<Void> loadDataAsync(String key) {
// 模拟从底层数据源加载数据的逻辑
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
String data = loadDataFromDataSource(key);
// 将加载的数据存入Redis
redisTemplate.opsForValue().set(key, data);
return CompletableFuture.completedFuture(null);
}
private String loadDataFromDataSource(String key) {
// 从底层数据源加载数据的逻辑
String data = "Data for " + key;
return data;
}
}
方案四:限制查询频率
对频繁查询不存在的数据请求,可以在应用层面设置一些限制,降低被恶意攻击风险
通过限制请求ip
通过限制接口请求次数
方案五:布隆过滤器
大家都知道布隆过滤器,但是像我这种ds,只是听过,没有实际上手实际使用过,只有些理论知识,下面时一个简单的例子
import java.util.BitSet;
import java.util.HashSet;
import java.util.Set;
public class BloomFilter {
private final int numBits;
private final int numHashFunctions;
// 用于存储位数组
private final BitSet bitSet;
// 存储具体缓存数据
private final Set<String> cacheData;
public BloomFilter(int numBits, int numHashFunctions) {
this.numBits = numBits;
this.numHashFunctions = numHashFunctions;
this.bitSet = new BitSet(numBits);
this.cacheData = new HashSet<>();
}
public boolean mightContain(String key) {
// 先通过布隆过滤器快速判断是否存在于集合中
if (!bitSet.get(hash(key))) {
return false;
}
// 再通过缓存判断是否存在
return cacheData.contains(key);
}
public void put(String key) {
// 将元素加入布隆过滤器
int[] hashes = getHashes(key);
for (int hash : hashes) {
bitSet.set(hash);
}
// 将元素加入缓存
cacheData.add(key);
}
private int[] getHashes(String key) {
int[] hashes = new int[numHashFunctions];
// 使用多个哈希函数生成多个哈希值
for (int i = 0; i < numHashFunctions; i++) {
hashes[i] = hash(key + i);
}
return hashes;
}
private int hash(String data) {
// 自定义哈希函数,可以使用不同的哈希算法
return data.hashCode() % numBits;
}
}
需要查询元素时,先调用mightContain方法判断是否需要进一步查询缓存或底层数据源,避免频繁查询不存在的数据
缓存雪崩
缓存雪崩指在同一时间段,redis中大量key失效或者redis服务宕机,导致请求大量直接访问后端数据源,从而使后端服务器负载剧增,甚至导致系统崩溃的现象。
这是概念相关的东西,那么在实际使用中何时会发生缓存雪崩呢?
发生情况
1. 缓存过期或清除时间集中:缓存中的数据往往会设置一个过期时间,当很多缓存在同一时间过期或被清除时,大量的请求会直接访问后端,造成压力过大。
2. 大规模数据更新:当系统中的大量数据同时更新时,原有的缓存将会失效,导致请求全部落到后端。
解决方法
- 设置合理的缓存过期时间:将缓存的过期时间分散开,避免大量缓存同时失效
- 引入热点数据永远不过期的策略:对于一些热点数据,可以设置其永不过期,确保关键数据的可用性。
- 限流与降级:可以通过限制请求的并发量,或者降级部分功能,以减少后端的压力。
- 备份缓存:可以采用多级缓存结构,多备份一份缓存,当一个缓存失效时,可以使用备份缓存,减轻后端请求的压力。
- 监控与预警:通过监控缓存的状态,及时发现异常情况并进行预警,以便尽早采取措施应对。
缓存击穿
缓存击穿是指在缓存系统中,一个缓存键(key)对应的数据在缓存中不存在,但是对该数据的请求非常频繁,导致大量请求直接访问后端数据源,从而增加了后端的负载压力。
出现情况
热点数据失效:某个热点数据的缓存过期或被清除,而此时有大量的请求访问该数据,导致缓存无法命中,请求直接访问后端。
高并发请求:当有大量并发请求同时访问一个缓存键对应的数据,而该数据没有缓存,也没有采取限流措施的情况下,会导致后端压力剧增
解决方案
-
加载缓存时加锁:在加载数据到缓存的过程中,使用互斥锁来保证只有一个线程加载数据,其他线程等待,从而避免重复加载的问题。
从redis中查询数据,判断是否命中,①命中就返回数据;②未命中尝试获取互斥锁,获取互斥锁成功,查询数据库,将获取的数据写入redis,释放互斥锁,返回数据,③获取互斥锁失败,休眠一段时间,再次从redis中查询数据
public Object cacheBreakdownWithLock(Long id) {
// 1. 从redis中获取数据
String redisJson = stringRedisTemplate.opsForValue().get(redisKey + id);
// 2. 判断是否命中,命中返回数据
if(StringUtil.isNotBlank(redisJson)) {
return JSONUtil.toBean(redisJson,Object.class);
}
// 3. 判断是否为空
if(redisJson != null) {
return null;
}
// 4. 尝试获取锁
boolean status = tryGetLock("lockKey");
try{
// 5. 获取锁失败,等待后重试
if(!status){
Thread.sleep(50);
return cacheBreakdownWithLock(id);
}
// 6. 获取锁成功,查询数据库,需要大量时间处理的业务
Object dataFromDb = this.getById(id);
// 7. 数据库为空缓存空值
if(dataFromDb == null) {
stringRedisTemplate.opsForValue().set(redisKey + id,"",10000,TimeUnit.SECONDS);
return null;
}
// 8. 数据库不为空缓存数据
stringRedisTemplate.opsForValue().set(redisKey + id,JSONUtil.toJsonStr(dataFromDb),10000,TimeUnit.SECONDS);
return dataFromDb;
}catch(InterruptedException e) {
throw new RunTimeException(e);
} finally {
releaseLock("lockKey");
}
}
public boolean tryGetLock(String key) {
Boolean status = stringRedisTemplate.opsForValue().setIfAbsent(key, "",1,TimeUnit.SECONDS);
return BooleanUitl.isTrue(status);
}
public void releaseLock(String key) {
stringRedisTemplate.delete(key);
}
- 设置热点数据永远不过期:对于一些热点数据,可以设置其永不过期,保证其始终缓存在缓存中,避免缓存失效带来的问题。
- 引入空值缓存:如果在后端数据源中查询得知某个缓存键对应的数据不存在,可以将该空结果缓存起来,设置一个较短的过期时间,避免频繁查询后端。
- 限流与降级:对于频繁请求的情况,可以通过限制请求的并发量或者降级部分功能,以减少后端的压力。
- 使用分布式锁:在缓存失效时,可以通过分布式锁来保证只有一个请求可以访问后端数据源,并在该请求加载数据到缓存后释放锁。
- 预加载缓存:可以在缓存过期前主动加载缓存数据,提前预加载,避免在热点数据失效时还未加载数据的情况。
第一节先到这里,第二节可能会讲到实际使用场景。
下一章:秒杀业务
谢谢大家看到了这里