前言
本文是我在日常学习中对redis方面学习的全面总结,分为三大模块。
1. 入门篇总结了redis的基础知识,限于入门redis,省略了redis的安装和客户端基础命令操作,着重与java客户端以及在java环境下如何操作redis
2. 进阶篇总结了redis的持久化,分布式锁,缓存,简单写了一点事务相关方面,没有总结集群,高级数据结构应用等,因为没有总结的方面我平时开发过程中用到的也甚少,了解不全面。
3. 应用篇则列举了我在日常开发中用到redis的使用场景
一. Redis入门篇
目录
1. opsForValue()
1.redis概念
Redis(Remote Dictionary Server)是一个开源的、使用 C 语言编写的、支持网络、可基于内存也可持久化的日志型、键值(Key - Value)数据库。一句话来说,redis是键值类型的,以内存存储为主的,支持持久化的非关系型数据库。
2. 特点
-
单线程高性能
- 由于数据存储在内存中,Redis 的读写性能极高。它能够在单线程的情况下处理大量的并发连接,每秒可以处理超过 10 万次的读写操作。这是因为 Redis 采用了非阻塞 I/O 多路复用技术
- 例如,在一个电商网站的秒杀活动中,大量用户同时请求查询商品库存或者下单。Redis 可以快速地处理这些请求,判断库存是否充足等操作,从而保障系统的高效运行。
-
数据结构丰富
- Redis 支持多种数据结构,包括字符串(String)、哈希(Hash)、列表(List)、集合(Set)、有序集合(Sorted Set)等。
- 字符串(String):可以存储任何形式的字符串数据,如用户的姓名、JSON 格式的配置信息等。例如,可以使用一个字符串键来存储网站的标题,“SET website_title "My Awesome Website"”。
- 哈希(Hash):适合存储对象的属性。例如,存储用户对象,其中用户 ID 是键,用户的姓名、年龄等属性可以作为哈希中的字段和值。像 “HSET user:1 name "John" age 30 "”。
- 列表(List):可以用来实现消息队列等功能。例如,一个新闻网站可以使用列表来存储待发布的新闻标题,新的新闻标题可以从一端插入(LPUSH),而发布时可以从另一端取出(RPOP)。
- 集合(Set):可以用于存储不重复的元素,如存储用户的关注列表或者标签集合。例如,“SADD user:1:tags "sports" "technology"” 表示为用户 1 添加 “sports” 和 “technology” 两个标签。
- 有序集合(Sorted Set):在集合的基础上,每个元素还带有一个分数,可以根据分数进行排序。例如,在一个游戏排行榜中,可以使用有序集合,玩家的 ID 作为成员,玩家的得分作为分数,通过分数来对玩家进行排名。
-
支持事务
- Redis 通过 MULTI、EXEC、WATCH 等命令来支持事务。事务可以保证一组命令要么全部执行成功,要么全部不执行。
- 例如,在一个转账的场景中,从一个账户扣除金额和向另一个账户增加金额这两个操作可以放在一个事务中。使用 MULTI 开始一个事务,然后执行 DECRBY(减少金额)和 INCRBY(增加金额)命令,最后使用 EXEC 来提交事务。如果在事务执行过程中出现问题(如其中一个键不存在等情况),整个事务会回滚。
-
可扩展性
- Redis 可以通过主从复制(Master - Slave)和分片(Sharding)来进行扩展。
- 主从复制:一个 Redis 主节点(Master)可以有多个从节点(Slave)。主节点负责写操作,从节点负责读操作。从节点会自动同步主节点的数据更新。这样可以通过增加从节点来分担读请求的压力,提高系统的并发读取能力。
- 分片:当数据量过大时,可以将数据分散到多个 Redis 节点上,每个节点只负责一部分数据。这就像把一个大的仓库分成多个小仓库来管理,通过合理的分片策略,可以存储海量的数据并且保持较高的性能。
-
高可用性
- 结合主从复制和 Sentinel(哨兵)机制,Redis 可以实现高可用性。Sentinel 可以监控 Redis 主从节点的状态。当主节点出现故障时,Sentinel 能够自动将其中一个从节点提升为新的主节点,从而保证系统的持续运行。例如,在一个分布式的 Web 应用中,即使 Redis 主节点因为硬件故障或者网络问题无法工作,Sentinel 可以快速地完成主从切换,使得应用程序的 Redis 读写操作能够继续进行,减少对业务的影响。
3. 在Java中操作Redis
3.1. java客户端
redis的java客户端有很多,常用的共有三种:
1. Jedis
2. Lettuce
3. Spring Data Redis
3.2. Spring Data Redis
1. 概述
是 Spring 框架中用于简化 Redis 操作的一个模块。它提供了一种方便的方式来在 Spring 应用程序中集成和使用 Redis,避免了直接使用 Redis 客户端库时的一些复杂性。通过提供高层次的抽象和模板类,开发人员可以更加专注于业务逻辑,而不是底层的 Redis 连接和命令操作。
Spring Boot提供了对应的Starter,maven坐标:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2. 核心组件
Spring Data Redis中提供了两个高度封装的类
1. RedisTemplate
- 功能:这是 Spring Data Redis 的核心模板类。它提供了丰富的方法来执行各种 Redis 操作,包括对字符串、哈希、列表、集合、有序集合等数据结构的操作。
RedisTemplate
是线程安全的,可以在多个线程环境下使用。
2. StringRedisTemplate
- 功能:
StringRedisTemplate
是RedisTemplate
的一个特殊化版本。它主要用于操作 Redis 中的字符串类型数据。与RedisTemplate
不同的是,它在序列化和反序列化时只针对字符串类型进行处理,使得操作更加简单和高效,尤其适用于只需要处理字符串数据的场景。
3. 数据序列化
- Spring Data Redis 支持多种数据序列化方式。默认情况下,
RedisTemplate
使用JdkSerializationRedisSerializer
,它会将对象序列化为字节数组进行存储,这种方式可以存储任何 Java 对象,但存储后的内容不太直观。StringRedisTemplate
默认使用StringRedisSerializer
,只对字符串进行简单的序列化和反序列化。 - 开发人员可以根据需要自定义序列化方式。例如,使用
Jackson2JsonRedisSerializer
可以将对象序列化为 JSON 格式的字符串存储在 Redis 中,这样在其他语言或工具访问 Redis 数据时更容易解析。
自定义序列化:
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
// 设置键的序列化方式为String
template.setKeySerializer(new StringSerializer());
// 设置值的序列化方式为Jackson2JsonRedisSerializer
Jackson2JsonRedisSerializer<Object> valueSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
valueSerializer.setObjectMapper(new ObjectMapper());
template.setValueSerializer(valueSerializer);
return template;
}
}
4. 事务支持
Spring Data Redis 支持 Redis 事务。可以通过RedisTemplate
的execute
方法来执行事务操作。在事务中,可以将多个 Redis 操作封装在一个SessionCallback
或RedisCallback
接口的实现中,这些操作要么全部成功执行,要么全部回滚。
示例:
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
@Component
public class RedisTransactionExampleComponent {
@Resource
private RedisTemplate<String, Object> redisTemplate;
public void transactionExample() {
redisTemplate.execute((RedisOperations<String, Object> operations) -> {
operations.multi();
operations.opsForValue().set("key1", "value1");
operations.opsForValue().set("key2", "value2");
return operations.exec();
});
}
}
5. 与 Spring Boot 集成
集成 Spring Data Redis 非常方便。只需要在pom.xml
文件中添加spring-data-redis
依赖,并在application.properties
或application.yml
文件中配置 Redis 的连接信息(如主机地址、端口、密码等)。
配置文件
spring:
redis:
database: 0 //数据库
host: 192.168.52.129 //主机地址
port: 6379 // 端口号
lettuce:
pool:
max-active: 8 // 连接池中最大的活跃连接数为 8
max-idle: 0 // 最大的空闲连接数为 0
max-wait: 100ms //当连接池中的连接都被占用时,后续的连接获取请求最多等待 100 毫秒
操作
@Resource
private StringRedisTemplate stringRedisTemplate;
stringRedisTemplate.opsForValue().set(key, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
String oldCode = stringRedisTemplate.opsForValue().get(key);
4. 数据类型 (java环境下操作)
redis支持的数据类型最常用的有String,List,Hash,Set,Zset,分别对应Java客户端中的opsForValue(),opsForList(),opsForHash(),opsForSet(),opsForZSet(),在这里只讨论java环境下操作redis。
1. opsForValue()
//新增一个字符串类型的值,key是键,value是值
redisTemplate.opsForValue().set("stringkey", "stringvalue");
//如果键不存在则新增,存在则不改变已经有的值
redisTemplate.opsForValue().setIfAbsent("newkey", "newvalue");
//获取key键对应的值
redisTemplate.opsForValue().get("stringkey");
//截取key键对应值的字符串,从开始下标位置开始到结束下标的位置(包含结束下标)的字符串。下标从1开始,如果stringkey对应的值为value,那么下面这个方法返回为:al
redisTemplate.opsForValue().get("stringkey", 2, 3);
//获取原来key键对应的值并重新赋新值
redisTemplate.opsForValue().getAndSet("stringkey", "newvalue");
//覆盖从指定位置开始的值
redisTemplate.opsForValue().set("stringkey", "a", 1);
//multiSet:设置map集合到redis;multiSetIfAbsent:如果对应的map集合名称不存在,则添加,如果存在则不做修改
Map map = new HashMap();
map.put("key1", "value1");
map.put("key2", "value2");
map.put("key3", "value3");
redisTemplate.opsForValue().multiSet(map);
//multiGet获得相应的值
List list = new ArrayList();
list.add("key1");
list.add("key2");
list.add("key3");
List<String> valueList = redisTemplate.opsForValue().multiGet(list);
for (String value2 : valueList) {
System.out.println("通过multiGet(Collection<K> keys)方法获取map值:" + value2);
}
//multiSetIfAbsent 设置相应的值,map集合中的key与之前一样,valeu设置的不一样
Map map1 = new HashMap();
map1.put("key1", "value11");
map1.put("key2", "value22");
map1.put("key3", "value33");
List list1 = new ArrayList();
list1.add("key1");
list1.add("key2");
list1.add("key3");
redisTemplate.opsForValue().multiSetIfAbsent(map1);
List<String> valueList1 = redisTemplate.opsForValue().multiGet(list1);
for (String value1 : valueList1) {
System.out.println("通过multiGet(Collection<K> keys)方法获取map值:" + value1);
}
//以增量的方式将值存储在变量中,第二个参数为递增因子
redisTemplate.opsForValue().set("doublevalue", 1);
redisTemplate.opsForValue().increment("doublevalue", 1.2);//以1.2递增,递增后为2.2
redisTemplate.opsForValue().increment("doublevalue", 1);//以1递增,递增后为3.2
//在原有的值基础上新增字符串到末尾
redisTemplate.opsForValue().set("stringkey", "value");
redisTemplate.opsForValue().append("stringkey", "appendValue");
// 获取指定字符串的长度
Long size = redisTemplate.opsForValue().size("stringkey");
//setBit: key键对应的值value对应的ascii码,在offset的位置(从左向右数)变为value;getBit:判断指定的位置ASCII码的bit位是否为true或者false
redisTemplate.opsForValue().set("stringkey", "value");
redisTemplate.opsForValue().setBit("stringkey", 1, true);
boolean flag= redisTemplate.opsForValue().getBit("stringkey", 1);
2. opsForList()
//获取指定下标间的值
redisTemplate.opsForList().range("rightList", 0, -1);//获取所有值
//从集合左边插入值
redisTemplate.opsForList().leftPush("list","a");
redisTemplate.opsForList().leftPush("list","a");
redisTemplate.opsForList().leftPush("list","b");
//从集合左边开始在v值后边插入新值v1
redisTemplate.opsForList().leftPush("list", "a", "e");
//从左边批量插入新值
List<String> strings = Arrays.asList("j", "q", "k");
redisTemplate.opsForList().leftPushAll("list", strings);
//从左边批量插入新值
redisTemplate.opsForList().leftPushAll("list", "j", "q", "k");
//如果key存在,从左边插入新值
redisTemplate.opsForList().leftPushIfPresent("list", "j");
//默认移除key中最左的一个值
redisTemplate.opsForList().leftPop("list");
//指定过期时间后删除key中最左的一个值
redisTemplate.opsForList().leftPop("list",1,TimeUnit.MINUTES);
//移除k1中最右的值,并将移除的值插入k2中最左侧
redisTemplate.opsForList().rightPopAndLeftPush("list", "list2");
//指定过期时间后,移除k1中最右的值,并将移除的值插入k2中最左侧
redisTemplate.opsForList().rightPopAndLeftPush("list", "list2",1,TimeUnit.MINUTES);
//从右侧插入新值
redisTemplate.opsForList().rightPush("rightList",'a');
redisTemplate.opsForList().rightPush("rightList",'b');
redisTemplate.opsForList().rightPush("rightList",'c');
//从右查找v1,并在v1右侧插入新值v2
redisTemplate.opsForList().rightPush("rightList", "b", "e");
//从右侧批量插入
redisTemplate.opsForList().rightPushAll("rightList", "e", "f","g");
//如果key存在,在右侧新插入value,否则不插入
redisTemplate.opsForList().rightPushIfPresent("rightList", "a");
//默认从最右侧移除一个值
redisTemplate.opsForList().rightPop("rightList");
//指定过期时间后,从最右侧移除一个值
redisTemplate.opsForList().rightPop("rightList",1,TimeUnit.MINUTES);
//获取指定位置的值(index从左往右,从0开始)
String string1 = (String) redisTemplate.opsForList().index("rightList", 2);
//获取对应key的集合长度
Long size = redisTemplate.opsForList().size("rightList");
//在指定坐标位置插入(替换)新值
redisTemplate.opsForList().set("rightList",2,"e");
//remove(K key, long count, Object value)
从存储在键中的列表中删除等于值的元素的第一个计数事件。
count> 0:删除等于从左到右移动的值的第一个元素;
count< 0:删除等于从右到左移动的值的第一个元素;
count = 0:删除等于value的所有元素。
redisTemplate.opsForList().remove("rightList", 0, "c");
3. opsForHash()
//新增hashMap值
redisTemplate.opsForHash().put(key,mapKey1,mapValue1);
redisTemplate.opsForHash().put(key,mapKey2,"mapValue2");
//获取指定key中键为mapKey1的值
Object o = redisTemplate.opsForHash().get(key, mapKey1);
//获取key对应的所有map键值对
Map hashValue = redisTemplate.opsForHash().entries(key);
//获取key对应的map中所有的键
Set hashValue = redisTemplate.opsForHash().keys(key);
//获取key对应的map中所有的值
List hashValue = redisTemplate.opsForHash().values(key);
//判断key对应的map中是否有指定的键
Boolean aBoolean = redisTemplate.opsForHash().hasKey(key, "map1");
//获取key对应的map的长度
Long hashValue = redisTemplate.opsForHash().size(key);
//如果key对应的map不存在,则新增到map中,存在则不新增也不覆盖
redisTemplate.opsForHash().putIfAbsent(key, mapKey, "value");
//直接以map集合的方式添加key对应的值
Map newMap = new HashMap();
newMap.put("mapKey1","map1");
newMap.put("mapKey2","map2");
redisTemplate.opsForHash().putAll(key,newMap);
//获取指定key对应的map集合中,指定键对应的值的长度
Long aLong = redisTemplate.opsForHash().lengthOfValue(key, mapKey);
//使key对应的map中,键var2对应的值以long1自增
Long increment = redisTemplate.opsForHash().increment(key, mapKey, 1);
//删除key对应的map中的键值对
Long delete = redisTemplate.opsForHash().delete(key, mapKey1, mapKey2);
4. opsForSet()
//向key中批量添加值 add(K key, V… var2)
redisTemplate.opsForSet().add("set", "aa", "bb", "cc");
redisTemplate.opsForSet().add("set", "ee");
//获取key中的值 members(K key)
Set set = redisTemplate.opsForSet().members("set");
System.out.println("set = " + set);
//获取key对应集合的长度 size(K key)
Long set = redisTemplate.opsForSet().size("set");
System.out.println("set = " + set);
//随机获取key对应的集合中的元素 randomMembers(K key, long long1)
Object set = redisTemplate.opsForSet().randomMember("set");
System.out.println("set = " + set);
//随机获取key对应集合中指定个数的元素 randomMembers(K key, long long1)
List set = redisTemplate.opsForSet().randomMembers("set", 2);
System.out.println("set = " + set);
//判断key对应的集合中是否包含元素o1 isMember(K key, Object o1)
Boolean member = redisTemplate.opsForSet().isMember("set", "aa");
System.out.println("member = " + member);
//获取两个集合中的交集元素
Set intersect = redisTemplate.opsForSet().intersect("set", "set2");
System.out.println("intersect = " + intersect);
//获取多个key对应集合之间的交集
List list = new ArrayList();
list.add("set2");
list.add("set3");
Set set = redisTemplate.opsForSet().intersect("set", list);
System.out.println("set = " + set);
//获取key集合与另一个otherKey集合之间的交集元素,并将其放入指定的destKey集合中
Long aLong = redisTemplate.opsForSet().intersectAndStore("set", "set2", "set4");
System.out.println("aLong = " + aLong);
//获取两个集合的合集,并且去重
Set union = redisTemplate.opsForSet().union("set", "set2");
System.out.println("union = " + union);
//获取多个集合的合集,去重
List list = new ArrayList();
list.add("set2");
Set union = redisTemplate.opsForSet().union("set", list);
System.out.println("union = " + union);
//获取两个集合之间的合集,并放入指定key对应的新集合中
Long aLong = redisTemplate.opsForSet().unionAndStore("se3", "set2", "set4");
System.out.println("aLong = " + aLong);
//获取多个集合之间的合集,并放入指定key对应的新集合中
List list = new ArrayList();
list.add("set2");
list.add("set3");
redisTemplate.opsForSet().unionAndStore("set", list, "set4");
//随机获取key对应集合中指定个数的元素,并且去重 distinctRandomMembers(K key, long long1)
Set set = redisTemplate.opsForSet().distinctRandomMembers("set", 3);
System.out.println("set = " + set);
//将key1对应集合中的值v1,转移到key2集合中 move(K key1, V v1, K key2)
Boolean move = redisTemplate.opsForSet().move("set", "aa", "set2");
System.out.println("move = " + move);
//随机弹出key对应集合中的一个元素 pop(K key)
Object set = redisTemplate.opsForSet().pop("set");
System.out.println("set = " + set);
//随机移除key对应集合中的count个元素 pop(K key, long count)
List set = redisTemplate.opsForSet().pop("set", 2);
System.out.println("set = " + set);
//批量移除key对应集合中指定的元素 remove(K key, Object… values)
redisTemplate.opsForSet().remove("set", "cc","aa");
//匹配获取键值对 scan(K key, ScanOptions options)
Cursor<Object> cursor = redisTemplate.opsForSet().scan("set", ScanOptions.scanOptions().match("ee").build());
while (cursor.hasNext()){
Object object = cursor.next();
System.out.println("object = " + object);
}
//获取key与其他集合之间的差值 difference(K key, Collection otherKeys)
List list = new ArrayList();
list.add("set2");
Set set = redisTemplate.opsForSet().difference("set", list);
System.out.println("set = " + set);
//获取key与另一个otherKey所对应的集合之间的差值,并将结果存入指定的destKey中
//differenceAndStore(K key, K otherKey, K destKey)
Long aLong = redisTemplate.opsForSet().differenceAndStore("set", "set2", "set3");
System.out.println("aLong = " + aLong);
//获取key与另外一些otherKeys集合之间的差值,并将结果存入指定的destKey中
//differenceAndStore(K key, Collection otherKeys, K destKey)
List list = new ArrayList();
list.add("set2");
Long aLong = redisTemplate.opsForSet().differenceAndStore("set", list, "set3");
System.out.println("aLong = " + aLong);
5. opsForZSet
//add:向有序集合中添加一个成员,同时指定该成员的分数
redisTemplate.opsForZSet().add("myzset", "member1", 0.5);
redisTemplate.opsForZSet().add("myzset", "member2", 0.8);
redisTemplate.opsForZSet().add("myzset", "member3", 1.2);
//range:获取有序集合中指定范围内的成员集合(按分数从低到高排序)
Set<Object> members = redisTemplate.opsForZSet().range("myzset", 0, -1);
//reverseRange:获取有序集合中指定范围内的成员集合(按分数从高到低排序)
Set<Object> members = redisTemplate.opsForZSet().reverseRange("myzset", 0, -1);
//zCard:获取有序集合中的成员数量
Long size = redisTemplate.opsForZSet().zCard("myzset");
//score:获取有序集合中指定成员的分数
Double score = redisTemplate.opsForZSet().score("myzset", "member1");
//从有序集合中移除指定的成员
Long removedMembers = redisTemplate.opsForZSet().remove("myzset", "member1", "member2");
//统计有序集合中指定分数范围内的成员数量
Long count = redisTemplate.opsForZSet().count("myzset", 1.0, 2.0);
//incrementScore:将指定成员的分数增加指定数值
Double newScore = redisTemplate.opsForZSet().incrementScore("myzset", "member1", 0.2);
//rank:获取指定成员在有序集合中的排名(按分数从低到高排序)
Long rank = redisTemplate.opsForZSet().rank("myzset", "member1");
//reverseRank:获取指定成员在有序集合中的排名(按分数从高到低排序)
Long reverseRank = redisTemplate.opsForZSet().reverseRank("myzset", "member1");
6. opsForGeo()
这个接口提供了一系列用于操作 Redis 地理空间数据(Geospatial data)的方法。通过这些方法,可以方便地在 Redis 中存储、查询地理坐标相关的数据。该方法使用场景并不是很多,在这只记录我开发中的小例子:
// 将地理坐标注册进 redis
String key = STORE_LOCATIONS_KEY + storeDTO.getIId();
// 创建地理坐标对象,存储店铺 ID 和坐标点(经度和纬度)
RedisGeoCommands.GeoLocation<String> locations = new RedisGeoCommands.GeoLocation<>(store.getId().toString(), new Point(storeDTO.getY(), storeDTO.getX()));
// 将地理坐标添加到指定的 Redis 键中
stringRedisTemplate.opsForGeo().add(key, locations);
-----------------------------------------------------------------------------------------
// 查询地理位置
// 1.拿数据
String key = STORE_LOCATIONS_KEY + storePageQueryDTO.getIId();
// 在 Redis 中搜索指定范围内的地理坐标
GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo()
.search(
key,
GeoReference.fromCoordinate(y, x),
new Distance(50000),
RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().limit(end)
);
// 2.解析数据
if (results == null) {
// 如果没有结果,返回空的分页结果
return new PageResult(0, Collections.emptyList());
}
List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = results.getContent();
List<Long> ids = new ArrayList<>(list.size());
Map<String, Distance> distanceMap = new HashMap<>(list.size());
// 遍历搜索结果,提取店铺 ID 和距离信息
list.stream().skip(from).forEach(result -> {
// 获取商店 id
String idsStr = result.getContent().getName();
ids.add(Long.valueOf(idsStr));
// 获取距离
final Distance distance = result.getDistance();
distanceMap.put(idsStr, distance);
});
二. Redis进阶篇
1.持久化
redis有两大持久化机制,RDB和AOF
1. RDB 持久化
原理:
- RDB 持久化是一种快照式的持久化方式。Redis 会在满足一定条件时,将当前内存中的数据集生成一个快照,并将其保存到磁盘上。这个快照是一个二进制文件,其中包含了 Redis 数据库在某一时刻的所有数据。
触发条件:
- 自动触发:可以通过配置文件(
redis.conf
)中的save
参数来设置自动保存的策略。例如,save 900 1
表示在 900 秒(15 分钟)内,如果有至少 1 个键被修改,就会自动触发 RDB 快照保存;save 300 10
表示 300 秒(5 分钟)内至少有 10 个键被修改时触发。这些配置可以根据实际需求进行调整,以平衡数据安全性和性能。 - 手动触发:可以使用
SAVE
或BGSAVE
命令来手动触发 RDB 保存。SAVE
命令会阻塞 Redis 服务器,直到 RDB 文件创建完成,这期间 Redis 不能处理其他客户端的请求。而BGSAVE
命令会在后台异步执行快照保存操作,不会阻塞服务器的正常运行,但在执行BGSAVE
期间如果又有新的save
条件满足,Redis 会拒绝新的BGSAVE
请求。
RDB 文件格式与存储位置
- RDB 文件是一个经过压缩的二进制文件,其格式是 Redis 自定义的。默认情况下,RDB 文件存储在 Redis 安装目录下,文件名为
dump.rdb
。可以通过配置文件中的dbfilename
参数来修改文件名,通过dir
参数来指定文件存储的目录。
恢复数据
- 当 Redis 服务器启动时,如果检测到存在 RDB 文件,就会自动加载该文件来恢复数据。这个过程是自动进行的,加载速度相对较快,因为 RDB 文件的格式是专门为快速加载数据而设计的。
2. AOF 持久化
原理
AOF 持久化是将 Redis 执行的每一条写命令(如SET
、HSET
、LPUSH
等)以追加的方式写入一个文件(AOF 文件)中。当 Redis 服务器需要恢复数据时,只要重新执行 AOF 文件中的所有写命令,就可以还原数据的状态。
AOF 文件的写入与同步策略
- 写入策略:Redis 会先将写命令写入一个缓冲区,然后根据配置的策略将缓冲区中的命令写入 AOF 文件。写入策略有三种:
- always:每次执行一个写命令后,就立即将命令写入 AOF 文件并同步到磁盘,这种方式最安全,但性能较差,因为磁盘 I/O 操作很频繁。
- everysec(默认策略):每秒将缓冲区中的写命令写入 AOF 文件并同步到磁盘一次。这种方式在性能和数据安全性之间取得了较好的平衡,即使服务器在一秒内崩溃,最多也只会丢失一秒钟的数据。
- no:由操作系统决定何时将缓冲区中的写命令写入 AOF 文件并同步到磁盘。这种方式性能最好,但数据安全性最低。
AOF 文件的重写(rewrite):
- 随着 Redis 的运行,AOF 文件会不断增大,因为它记录了所有的写命令。为了避免 AOF 文件过大,Redis 提供了 AOF 文件重写机制。AOF 重写并不是简单地对原文件进行压缩,而是根据当前内存中的数据重新生成一个新的 AOF 文件。在重写过程中,Redis 会读取内存中的所有数据,将其转换为一系列的写命令,这些命令可以在加载时重新构建当前的数据集,同时会尽量减少冗余命令。例如,如果对一个键进行了多次修改,重写后的 AOF 文件只会记录最终的修改结果。
- AOF 重写可以手动触发(使用
BGREWRITEAOF
命令),也会自动触发。自动触发的条件与服务器的配置有关,例如,当 AOF 文件的大小超过了配置文件中设置的阈值(如auto - aof - rewrite - min - size
和auto - aof - rewrite - percentage
参数)时,就会自动触发 AOF 重写。
数据恢复
- 当 Redis 服务器启动时,如果开启了 AOF 持久化并且存在 AOF 文件,Redis 会优先使用 AOF 文件来恢复数据。它会逐行读取 AOF 文件中的写命令,并按照顺序执行这些命令,从而还原数据的状态。在执行 AOF 文件中的命令时,如果发现命令格式错误或者执行出现异常,Redis 会尽可能地继续执行其他正确的命令,并在日志中记录错误信息。
3. 两种持久化机制的比较与选择
- 数据安全性
- AOF 持久化的数据安全性相对较高,因为它记录了每一条写命令,即使在最坏的情况下,也只会丢失最后一次同步到磁盘之间的命令(如果采用默认的
everysec
写入策略)。而 RDB 持久化是基于快照的,如果在两次快照之间 Redis 服务器崩溃,那么这期间的数据修改将丢失。
- AOF 持久化的数据安全性相对较高,因为它记录了每一条写命令,即使在最坏的情况下,也只会丢失最后一次同步到磁盘之间的命令(如果采用默认的
- 性能影响
- RDB 持久化对性能的影响相对较小,因为它是定期进行快照保存,而且快照操作是在内存中对数据进行序列化,然后再写入磁盘。AOF 持久化由于需要记录每一条写命令,并且根据写入策略频繁地进行磁盘 I/O 操作(尤其是采用
always
写入策略时),会对性能产生较大的影响。
- RDB 持久化对性能的影响相对较小,因为它是定期进行快照保存,而且快照操作是在内存中对数据进行序列化,然后再写入磁盘。AOF 持久化由于需要记录每一条写命令,并且根据写入策略频繁地进行磁盘 I/O 操作(尤其是采用
- 文件大小与恢复速度
- RDB 文件是经过压缩的二进制文件,通常比 AOF 文件小,并且 RDB 文件的恢复速度比 AOF 文件快,因为它只需要加载一个快照文件,而 AOF 文件需要逐行执行写命令来恢复数据。在实际应用中,可以根据具体的需求来选择合适的持久化机制,或者同时使用两种机制来提高数据的安全性和可用性。例如,在对性能要求较高,并且能够容忍一定程度的数据丢失的场景下,可以优先考虑 RDB 持久化;而在对数据安全性要求极高的场景下,如金融系统等,可以主要使用 AOF 持久化。
2. 事务和lua脚本
- 事务与 Lua 脚本
- 事务处理:理解 Redis 事务的特性,通过
MULTI
、EXEC
、WATCH
等命令实现事务操作。但要注意 Redis 事务的原子性与传统数据库事务原子性的区别,Redis 事务主要是保证命令序列的一次性执行,但不保证命令执行过程中的错误回滚(部分命令错误仍会执行其他命令)。例如,在一个转账场景中,通过事务来保证从一个账户扣钱和另一个账户加钱的操作完整性。 - Lua 脚本应用:学习在 Redis 中使用 Lua 脚本,Lua 脚本可以将多个 Redis 命令组合在一起,实现原子性操作。并且 Lua 脚本在 Redis 服务器端执行,减少了网络开销,提高了性能。例如,通过 Lua 脚本实现一个复杂的业务逻辑,如在满足一定条件下同时修改多个键的值。
- 事务处理:理解 Redis 事务的特性,通过
3.Redis分布式锁
特点:
满足在分布式系统下的多进程可见,并且互斥。
多进程可见,互斥,高可用,高性能,安全性
普通锁
jvm内部的锁
只能监听一个jvm里的线程,而分布式部署的话依然会出现并发问题
分布式锁:
jvm外部的锁
多进程可见
基于redis实现分布式锁
原理:利用redis的SETNX的特点,可实现锁的互斥性。
获取锁:SETNX lock thread1
释放锁: 1:DEL key
2:设置过期时间
实现获取锁和释放锁
public class SimpleRedisLock implements Lock{
private StringRedisTemplate stringRedisTemplate;
private String name;
private static final String KEY_PREFIX = "lock:";
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
this.stringRedisTemplate = stringRedisTemplate;
this.name = name;
}
@Override
public boolean tryLock(long timeoutSec) {
//获取线程标识
final String threadId = ID_PREFIX + Thread.currentThread().getId();
//获取锁
final Boolean aBoolean = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(aBoolean);
}
@Override
public void unlock() {
//判断表示是否一致
final String threadId = ID_PREFIX + Thread.currentThread().getId();
final String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
if (threadId.equals(id)){
//释放锁
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
}
4.缓存
缓存时redis应用最广泛的地方之一,我当初就是从缓存方面入的redis
1. 概念
缓存是一种存储数据副本的技术,用于临时存储经常访问的数据,以减少数据访问的延迟和减轻后端数据源(如数据库)的负载。在 Redis 缓存场景中,数据从后端数据源(如关系型数据库)首次读取后,存储在 Redis 中,后续相同数据的请求可以直接从 Redis 获取,而无需再次查询后端数据源。
2. 优势
- 性能提升:Redis 存储数据在内存中,内存的读写速度比磁盘快几个数量级。例如,从内存读取数据可能在几十纳秒级别,而从磁盘读取数据可能在几毫秒级别。当应用程序频繁访问某些数据时,使用 Redis 缓存可以大大减少数据访问时间,从而提高应用程序的整体性能。
- 减轻后端压力:通过缓存常用数据,减少了后端数据源(如数据库)的查询请求数量。例如,在一个电商网站中,如果商品信息被缓存到 Redis 中,大部分用户查看商品详情的请求可以由 Redis 处理,而只有缓存未命中(如商品信息更新后缓存尚未更新)时才需要查询数据库,这样可以避免数据库因过多请求而出现性能瓶颈。
3. 缓存更新策略
- 定时过期与懒加载更新:定时过期策略是指为缓存数据设置固定的过期时间,当时间到达后,缓存数据自动失效。懒加载更新则是在缓存数据失效后,当再次请求该数据时,才从后端数据源重新加载并更新缓存。这种策略简单有效,但可能会导致缓存失效后的首次请求延迟增加,因为需要重新加载数据。例如,对于一个每小时更新一次统计数据的缓存,可以设置过期时间为 1 小时,过期后下一次请求时再重新从数据库获取最新数据。
- 主动更新:主动更新策略是在后端数据源的数据发生变化时,主动更新缓存中的数据。这种策略可以保证缓存数据的及时性,但实现起来相对复杂,需要在数据更新的代码中同时处理缓存更新。例如,在电商系统中,当商品价格更新后,立即使用
SET
命令更新 Redis 缓存中的商品价格信息。
4.缓存击穿、穿透和雪崩问题及解决方案
- 缓存击穿
- 问题描述:缓存击穿是指一个热点键(频繁被访问的键)过期的瞬间,大量请求同时穿透缓存直接访问后端数据源,导致后端数据源压力骤增。例如,一个热门商品的缓存过期,而此时大量用户同时访问该商品详情页,这些请求都会直接查询数据库。
- 解决方案:可以使用互斥锁来解决缓存击穿问题。当一个请求发现热点键过期时,先获取一个互斥锁,只有获取到锁的请求可以去后端数据源查询数据并更新缓存,其他请求等待。例如,使用 Redis 的 SETNX 命令实现互斥锁,获取锁成功的请求查询数据库后,使用 SET 命令更新缓存并释放锁,其他等待的请求在缓存更新后可以直接从缓存获取数据。
- 缓存穿透
- 问题描述:缓存穿透是指查询一个不存在的数据,由于缓存和后端数据源都没有该数据,导致每次请求都穿透缓存直接查询后端数据源。如果攻击者故意大量请求不存在的数据,可能会导致后端数据源崩溃。例如,通过不断查询不存在的商品 ID,使数据库承受大量无效查询。
- 解决方案:可以在缓存层对不存在的数据进行缓存,将空值(或一个特殊标记)存入缓存,并设置较短的过期时间。例如,当查询一个不存在的商品 ID 时,在 Redis 中存入一个特殊值
null
,并设置过期时间为 1 分钟,这样后续相同的请求就可以直接从缓存获取空值,而不会再查询数据库。另外,也可以使用布隆过滤器来预先判断数据是否可能存在,布隆过滤器可以快速判断一个元素是否在一个集合中,虽然有一定的误判率,但可以有效减少缓存穿透的情况。
- 缓存雪崩
- 问题描述:缓存雪崩是指在某一时刻,大量缓存同时过期或 Redis 服务出现故障,导致大量请求直接访问后端数据源,使后端数据源无法承受如此大的压力而崩溃。例如,所有商品缓存设置了相同的过期时间,在过期的瞬间全部失效,大量商品查询请求直接涌向数据库。
- 解决方案:可以通过设置缓存过期时间的随机化来避免缓存雪崩。例如,将缓存的过期时间设置为一个时间段内的随机值,而不是固定值。另外,使用 Redis 的主从复制和集群模式来提高 Redis 服务的可用性,当一个节点出现故障时,其他节点可以继续提供服务,减少缓存雪崩的影响。还可以通过限流和降级策略来保护后端数据源,当请求量过大时,限制部分请求或者降低服务质量,优先保证核心功能的正常运行。
5. 与数据库双写一致问题
- 先更新数据库,再更新缓存
- 问题:存在两个操作都可能失败的风险。如果先更新数据库成功,但更新缓存失败,那么缓存中的数据就会过时。另外,这种策略在高并发场景下可能会导致数据不一致。假设两个线程同时更新同一个数据,线程 A 先更新数据库,然后更新缓存;线程 B 在 A 更新数据库后、更新缓存前读取了旧数据并更新了缓存,这样就会导致缓存中的数据被错误地更新。
- 原理:这种策略是在数据库中的数据发生变化后,立即更新缓存中的相应数据。例如,在一个电商系统中,当商品价格在数据库中被修改后,马上使用
SET
命令更新 Redis 缓存中的商品价格信息。这样可以保证缓存中的数据始终是最新的。 - 先更新缓存,再更新数据库
- 原理:先修改缓存中的数据,使其反映即将发生的数据库更新,然后再更新数据库。这种方式在某些场景下可以减少缓存不一致的时间。
- 问题:同样存在更新失败的风险。如果更新缓存成功,但更新数据库失败,那么缓存中的数据就会与数据库不一致。而且这种策略在高并发场景下更容易出现数据不一致的情况,因为数据库更新可能会因为各种原因(如并发冲突、数据库故障等)而失败,导致缓存中的数据与数据库中的实际数据不匹配。
- 先删除缓存,再更新数据库
- 原理:这是一种常用的策略。当数据需要更新时,首先删除对应的缓存,然后再更新数据库。后续如果有请求访问该数据,由于缓存已被删除,会从数据库中获取最新的数据并更新缓存。例如,当修改商品信息时,先使用
DEL
命令删除 Redis 中的商品缓存,然后更新数据库中的商品信息。 - 问题:可能会出现短暂的数据不一致。在删除缓存后、更新数据库完成前,如果有请求访问该数据,会因为缓存不存在而从数据库获取旧数据并更新缓存,导致缓存中的数据是旧的。不过这种不一致是短暂的,在数据库更新完成后,后续的请求会更新缓存为正确的数据。
- 原理:这是一种常用的策略。当数据需要更新时,首先删除对应的缓存,然后再更新数据库。后续如果有请求访问该数据,由于缓存已被删除,会从数据库中获取最新的数据并更新缓存。例如,当修改商品信息时,先使用
-
异步更新机制
- 消息队列的应用
- 原理:当数据库中的数据发生更新时,通过消息队列发送一个消息,告知缓存需要更新。缓存系统订阅这个消息队列,当收到消息后,再进行缓存更新。这样可以将数据库更新和缓存更新解耦,提高系统的灵活性和可扩展性。例如,使用 RabbitMQ 或 Kafka 等消息队列,在数据库更新操作完成后,发送一个包含更新数据键的消息,缓存服务通过监听消息队列,获取消息后根据键来更新缓存。
- 优点:可以应对高并发场景下的大量更新请求。消息队列可以缓冲更新请求,避免缓存系统因为频繁更新而出现性能问题。同时,即使缓存更新失败,也可以通过消息队列的重试机制或者补偿机制来确保缓存最终能够得到更新。
- 后台定时任务更新缓存
- 原理:设置一个后台定时任务,定期检查数据库中的数据与缓存中的数据是否一致。如果发现不一致,就更新缓存。这种方式适用于对数据实时性要求不是特别高的场景。例如,对于一些统计数据,每隔一段时间(如 10 分钟)检查数据库中的统计结果与缓存中的是否一致,若不一致则更新缓存。
- 优点:减轻了数据库更新时立即更新缓存的压力。可以在系统负载较低的时候进行缓存更新,对系统的整体性能影响较小。但是这种方式不能及时反映数据的变化,可能会导致缓存中的数据在一段时间内与数据库不一致。
- 消息队列的应用
三. Redis应用篇
应用篇记录我在日常开发中用到redis的场景。
1. 点赞功能
核心:判断用户是否已经点赞 ,利用redis的ZSet类型的不可重复的属性
1: 判断用户是否已经点赞
①给note加上isLike属性
@TableField(exist = false)
private Boolean isLike;
②查询笔记操作的时候要查询当前用户是否已经点赞
private void isNoteLiked(Note note){
final Long userId = UserHolder.getUser();
if (userId == null){
//用户未登录,不用查询是否点过赞
return;
}
String key = NOTE_LIKED_KEY + note.getId();
final Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
note.setIsLike(BooleanUtil.isTrue(score != null));
}
---------------------------------------------------------------------------------
@Override
public PageResult pageQuery(NotePageQueryDTO notePageQueryDTO) {
PageHelper.startPage(notePageQueryDTO.getPageNum(), notePageQueryDTO.getPageSize());
Page<Note> page =noteMapper.pageQuery(notePageQueryDTO);
final long total = page.getTotal();
final List<Note> result = page.getResult();
//判断用户是否已经点赞
result.forEach(this::isNoteLiked);
return new PageResult(total, result);
}
③点赞功能的实现
@Override
public JsonResult likeNote(Long id) {
//获取当前用户
Long userId = UserHolder.getUser();
//判断当前用户是否已经点赞
String key = NOTE_LIKED_KEY + id;
final Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
if (score == null){
//没点赞
//数据库点赞数+1
update().setSql("liked = liked + 1").eq("id", id).update();
//将点赞用户set进redis
stringRedisTemplate.opsForZSet().add(key, userId.toString(),System.currentTimeMillis());
}else {
//已经点赞
//数据库点赞-1
update().setSql("liked = liked - 1").eq("id", id).update();
//将点赞用户从set中移除
stringRedisTemplate.opsForZSet().remove(key, userId.toString());
}
return JsonResult.success();
}
2. 点赞排行榜
@Override
public JsonResult queryLikes(Long id) {
String key = NOTE_LIKED_KEY + id;
Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);
if (top5 == null) {
return JsonResult.success(Collections.emptyList());
}
List<Long> userIds = top5.stream().map(Long::valueOf).collect(Collectors.toList());
//查询用户
final String idStr = StrUtil.join(",", userIds);
List<UserDTO> list = userService
.query().in("id", userIds)
.last("ORDER BY FILED (id,"+ idStr +")").list()
.stream().map(user -> {
final UserDTO userDTO = new UserDTO();
BeanUtils.copyProperties(user, userDTO);
return userDTO;
})
.collect(Collectors.toList());
return JsonResult.success(list);
}
3. 共同关注功能
利用redis中set的交集功能
@Override
public JsonResult common(Long id) {
Long userId = UserHolder.getUser();
String key1 = FOLLOW_KEY + userId;
String key2 = FOLLOW_KEY + id;
Set<String> idsStr = stringRedisTemplate.opsForSet().intersect(key1, key2);
if (idsStr == null) {
return JsonResult.success(ListUtil.empty());
}
List<Long> ids = idsStr.stream().map(Long::valueOf).collect(Collectors.toList());
final List<UserDTO> userDTOS = userService.listByIds(ids).stream().map(user -> {
final UserDTO userDTO = new UserDTO();
BeanUtils.copyProperties(user, userDTO);
return userDTO;
}).collect(Collectors.toList());
return JsonResult.success(userDTOS);
}
4. 附近店铺功能
@Override
public JsonResult queryByType(Integer tid, Integer current, Double x, Double y, ProductPageRequest pageRequest) {
//判断是否需要根据坐标查询
if (x == null || y == null){
PageHelper.startPage(pageRequest.getPageNum(),pageRequest.getPageSize());
List<Product> product = productMapper.findProduct(pageRequest);
PageInfo<Product> pageInfo = new PageInfo<>(product);
return new JsonResult(200,pageInfo);
}
//计算分页参数
int from = (current - 1) * pageRequest.getPageSize();
int end = current * pageRequest.getPageSize();
//查询redis 按距离顺序,分页。 结果:pid,distance
String key = PRODUCT_GEO_KEY + tid;
GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo()
//GEOSEARCH key BYLONLAT x y BYRADIUS 10 WITHDISTANCE
.search(
key,
GeoReference.fromCoordinate(x, y),
new Distance(50000),
RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance()
.limit(end)
);
//解析pid
if (results == null) {
return new JsonResult(200,Collections.emptyList());
}
List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = results.getContent();
//截取从from到end的部分
List<Long> pids = new ArrayList<>(list.size());
Map<String, Distance> distanceMap = new HashMap<>(list.size());
list.stream().skip(from).forEach(result ->{
String pidStr = result.getContent().getName();
pids.add(Long.valueOf(pidStr));
Distance distance = result.getDistance();
distanceMap.put(pidStr, distance);
});
//根据pids查询商品
List<Product> products = productMapper.findByPids(pids);
for (Product product : products) {
product.setDistance(distanceMap.get(product.getPid().toString()).getValue());
}
return new JsonResult(200,products);
}
5. 用户签到功能
利用redis中的BitMap来实现
//用户签到功能
@Override
public JsonResult sign() {
//获取用户
final Long userId = UserHolder.getUser();
//获取当前日期
final LocalDateTime now = LocalDateTime.now();
final String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
//获取今天是本月的第几天
final int dayOfMonth = now.getDayOfMonth();
String key = USER_SIGN_KEY + userId + keySuffix;
//写入redis
stringRedisTemplate.opsForValue().setBit(key, dayOfMonth - 1, true);
return JsonResult.success();
}
//签到统计功能
@Override
public JsonResult signCount() {
//获取用户
Long userId = UserHolder.getUser();
//获取当前日期
LocalDateTime now = LocalDateTime.now();
String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
//获取今天是本月的第几天
int dayOfMonth = now.getDayOfMonth();
String key = USER_SIGN_KEY + userId + keySuffix;
List<Long> result = stringRedisTemplate.opsForValue().bitField(
key,
BitFieldSubCommands.create()
.get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(0));
if (result == null || result.isEmpty()) {
return JsonResult.success(0);
}
Long num = result.get(0);
if (num == null || num == 0){
return JsonResult.success(0);
}
int count = 0;
while (true){
if ((num & 1) == 0){
//未签到
break;
}else {
//已签到
count++;
}
num >>>= 1;
}
return JsonResult.success(count);
}
标签:缓存,Java,list,Redis,学习,set,key,redisTemplate
From: https://blog.csdn.net/duehebfbf/article/details/143614833