Redis 数据类型及其应用场景
Redis 是什么?
Redis是一个使用C语言编写的高性能的基于内存的非关系型数据库,基于Key/Value结构存储数据,通常用来 缓解高并发场景下对某一资源的频繁请求 ,减轻数据库的压力。它支持多种数据类型,如字符串、哈希、列表、集合、有序集合等。Redis以其高性能、高可靠性和丰富的特性而闻名,被广泛应用于缓存、消息队列、实时分析等领域。
Redis 的优势
- 高性能:Redis的所有数据都存储在内存中,支持每秒处理上百万的读写操作。
- 丰富的数据类型:Redis支持多种数据类型,可以灵活地满足不同的业务需求。
- 原子性操作:Redis提供了许多原子性操作,如INCR、DECR、RPOP等,可以避免并发问题。
- 持久化:Redis支持RDB和AOF两种持久化方式,可以保证数据的安全性。
Redis 数据类型详解以及应用场景
具体的业务场景下的代码参考(点这里噢🌹🌹🌹)
Redis数据类型 | 简单描述 | 结合Java理解 |
---|---|---|
String | Value 是String类型 | Map<String,String> JSON |
Set | Value 是Set类型Map套SetSet放的是不重复的value | Map<String,Set> |
Zset | Value 是Set类型Map套SetSet放的是不重复的value能排序 | Map<String,TreeSet> |
List | Map套ListList可以重复的value Index 下标 | Map<String,List> |
Hash | Map套Map | Map<String,Map<String,Object>> |
1. String(字符串)
String是Redis最基本的数据类型,它可以存储任何形式的字符串,包括二进制数据。在Redis中,String类型的值最大可以达到512MB。
String的常用命令
- SET:设置一个键值对
- GET:获取一个键对应的值
- INCR:对一个整数类型的键进行自增操作
- DECR:对一个整数类型的键进行自减操作
- SETEX:设置一个键值对,同时指定过期时间
String的应用场景
- 缓存:String类型可以用于缓存用户信息、商品详情等常用数据,提高系统的查询效率。
// 将用户信息缓存到Redis中
String userId = "1001";
String userInfo = "{\"id\":\"1001\",\"name\":\"Tom\",\"age\":25}";
jedis.set("user:" + userId, userInfo);
// 从Redis中获取用户信息
String cachedUserInfo = jedis.get("user:" + userId);
- 计数器:利用INCR、DECR等命令,可以实现计数器功能,如统计网站的访问量、文章的点赞数等。
// 文章点赞数+1
String articleId = "1001";
jedis.incr("article:" + articleId + ":likes");
// 获取文章点赞数
String likesStr = jedis.get("article:" + articleId + ":likes");
int likes = Integer.parseInt(likesStr);
- 分布式锁:利用SET命令的NX选项(只在键不存在时才设置值)和EX选项(设置过期时间),可以实现简单的分布式锁功能。
String lockKey = "lock:1001";
String lockValue = UUID.randomUUID().toString();
// 获取锁,过期时间30秒
String result = jedis.set(lockKey, lockValue, "NX", "EX", 30);
if ("OK".equals(result)) {
// 获取锁成功,执行业务逻辑
// ...
// 释放锁
if (lockValue.equals(jedis.get(lockKey))) {
jedis.del(lockKey);
}
} else {
// 获取锁失败,处理异常情况
// ...
}
2. List(列表)
Redis的List类型是一个双向链表,支持从头部或尾部进行插入和删除操作。一个List类型的键可以存储多个字符串值。
List的常用命令
- LPUSH:从列表左端插入一个或多个值
- RPUSH:从列表右端插入一个或多个值
- LPOP:移除并返回列表左端的第一个元素
- RPOP:移除并返回列表右端的第一个元素
- LRANGE:获取列表在给定范围上的所有值
List的应用场景
- 消息队列:List类型可以用作简单的消息队列,生产者使用LPUSH命令插入消息,消费者使用RPOP命令获取消息。
// 生产者代码
String taskQueue = "queue:tasks";
String task = "...";
jedis.lpush(taskQueue, task);
// 消费者代码
String taskQueue = "queue:tasks";
String task = jedis.rpop(taskQueue);
if (task != null) {
// 处理任务
// ...
}
- 最新列表:使用LPUSH命令可以将最新的数据插入到列表头部,使用LTRIM命令可以限制列表的长度,实现最新N个元素的列表。
String latestNewsKey = "latest:news";
String news = "...";
// 将最新新闻插入到列表头部
jedis.lpush(latestNewsKey, news);
// 只保留最新的100条新闻
jedis.ltrim(latestNewsKey, 0, 99);
// 获取最新的10条新闻
List<String> latestNews = jedis.lrange(latestNewsKey, 0, 9);
3. Set(集合)
Redis的Set类型是一个无序且不重复的字符串集合。可以对Set执行交集、并集、差集等操作。
Set的常用命令
- SADD:向集合中添加一个或多个成员
- SMEMBERS:返回集合中的所有成员
- SISMEMBER:判断成员是否存在于集合中
- SINTER:返回给定所有集合的交集
- SUNION:返回给定所有集合的并集
- SDIFF:返回给定所有集合的差集
Set的应用场景
- 标签系统:Set类型可以用于实现标签功能,一个用户可以对应多个标签,多个用户也可以对应同一个标签。
String user1Tags = "user:1001:tags";
String user2Tags = "user:1002:tags";
// 给用户添加标签
jedis.sadd(user1Tags, "music", "travel");
jedis.sadd(user2Tags, "music", "sports");
// 获取用户共同感兴趣的标签
Set<String> commonTags = jedis.sinter(user1Tags, user2Tags);
- 抽奖活动:Set类型可以用于实现抽奖功能,将所有参与用户加入到一个Set中,然后随机抽取若干个用户作为中奖者。
String lotteryKey = "lottery:users";
// 将用户加入抽奖活动
jedis.sadd(lotteryKey, "user:1001", "user:1002", "user:1003");
// 随机抽取2名中奖者
List<String> winners = jedis.srandmember(lotteryKey, 2);
4. Zset(有序集合)
Zset类型(Sorted Set)是一个有序的,不重复的字符串集合。与Set类型不同,Zset中的每个成员都关联了一个评分(score),评分用于对成员进行排序。
Zset的常用命令
- ZADD:向有序集合中添加一个或多个成员,或者更新已存在成员的评分
- ZRANGE:返回有序集合中,指定区间内的成员,成员按评分值递增排序
- ZREVRANGE:返回有序集合中,指定区间内的成员,成员按评分值递减排序
- ZRANGEBYSCORE:返回有序集合中,所有评分介于min和max之间(包括等于min或max)的成员
- ZRANK:返回有序集合中指定成员的排名
Zset的应用场景
- 排行榜:Zset类型可以用于实现各种排行榜功能,如商品销量排行、游戏玩家积分排名等。
String rankingKey = "sales:ranking";
// 添加商品销量数据
jedis.zadd(rankingKey, 100, "product:1001");
jedis.zadd(rankingKey, 80, "product:1002");
jedis.zadd(rankingKey, 120, "product:1003");
// 获取销量前3名的商品
Set<String> topProducts = jedis.zrevrange(rankingKey, 0, 2);
- 延时队列:利用Zset的评分值代表任务的执行时间,可以实现延时队列的功能。
String delayQueueKey = "delay:queue";
// 添加延时任务
long now = System.currentTimeMillis();
jedis.zadd(delayQueueKey, now + 60000, "task:1"); // 1分钟后执行
jedis.zadd(delayQueueKey, now + 300000, "task:2"); // 5分钟后执行
// 获取当前需要执行的任务
Set<String> tasks = jedis.zrangeByScore(delayQueueKey, 0, now);
5. Hash(哈希)
Redis的Hash类型可以看作是一个字符串字段(field)和字符串值(value)的映射表,特别适合用于存储对象。每个哈希可以存储多达232-1个键值对。
Hash的常用命令
- HSET:将哈希表中的字段设置为指定值
- HGET:获取存储在哈希表中指定字段的值
- HMSET:同时将多个field-value对设置到哈希表中
- HGETALL:获取在哈希表中指定key的所有字段和值
- HINCRBY:为哈希表中的字段值加上指定增量值
Hash的应用场景
- 用户信息存储:Hash类型可以用于存储用户信息,每个用户对应一个Hash,包含用户的各种属性。
String userKey = "user:1001";
// 存储用户信息
jedis.hset(userKey, "name", "Tom");
jedis.hset(userKey, "age", "25");
jedis.hset(userKey, "city", "New York");
// 获取用户信息
Map<String, String> userInfo = jedis.hgetAll(userKey);
- 购物车:Hash类型可以用于实现购物车功能,每个用户的购物车对应一个Hash,商品ID作为field,商品数量作为value。
String cartKey = "cart:1001";
// 添加商品到购物车
jedis.hset(cartKey, "product:1001", "2");
jedis.hset(cartKey, "product:1002", "1");
// 增加商品数量
jedis.hincrBy(cartKey, "product:1001", 1);
// 获取购物车信息
Map<String, String> cart = jedis.hgetAll(cartKey);
缓存预热、雪崩 、穿透 、击穿
缓存预热
缓存预热是指在应用启动时,提前将一些热点数据加载到Redis缓存中,避免请求直接访问数据库,提高系统的响应速度。常见的缓存预热方式包括:
- 使用
@PostConstruct
注解(Bean对象的生命周期第三阶段) - 第二种则是实现applicationRunner接口,重写run方法
推荐第二种 按照业务逻辑,我们需要加载完对应依赖,其他的Bean对象,再进行业务逻辑的处理
@Slf4j
@Component
public class StationDataInit implements ApplicationRunner {
/**
* 在项目启动前执行一些业务
* 可以使用@PostConstruct注解 速度很快 但是不推荐
* 推荐使用 实现 applicationRunner接口
*/
// @PostConstruct
@PostConstruct
public void init() {
log.debug("使用@PostConstruct注解实现在服务启动前去数据库里面获取信息");
}
@Override
public void run(ApplicationArguments args) throws Exception {
log.debug("使用实现 applicationRunner接口实现在服务启动前去数据库里面获取信息");
log.debug("从数据库里面获取所有的场站信息");
List<StationPO> stationPOList = stationRepository.getAllStation();
log.debug("获取数据存入到redis中");
}
缓存雪崩
缓存雪崩是指在同一时段内大量的缓存key同时失效或Redis服务不可用,导致大量请求直接访问数据库,引起数据库压力骤增,甚至宕机。常见的解决方案包括:
- 给不同的Key设置不同的过期时间,避免同时失效。
- 利用Redis集群提高可用性。
- 给缓存的访问加上超时限制,避免数据库过载。
- 提前演练,确保数据库能够承受缓存全部失效的压力。
// 给不同的Key设置不同的过期时间
jedis.setex("key1", 3600, "value1");
jedis.setex("key2", 7200, "value2");
jedis.setex("key3", 10800, "value3");
// 利用Redis哨兵或集群提高可用性
JedisPoolConfig poolConfig = new JedisPoolConfig();
// ...
RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration()
.master("mymaster")
.sentinel("127.0.0.1", 26379)
.sentinel("127.0.0.1", 26380);
JedisConnectionFactory connectionFactory = new JedisConnectionFactory(sentinelConfig, poolConfig);
缓存穿透
缓存穿透是指查询一个不存在的数据,因为不存在则不会写到缓存中,所以每次都会去请求数据库。如果大量的请求查询不存在的数据,就会给数据库带来很大的压力。常见的解决方案包括:
- 对不存在的key也缓存其value为null,设置较短的过期时间。
- 利用布隆过滤器快速判断key是否存在,避免缓存和数据库的查询。
// 缓存空值
String key = "non_existent_key";
String value = jedis.get(key);
if (value == null) {
// 从数据库查询
value = db.get(key);
if (value == null) {
// 数据库中也不存在,缓存空值,过期时间设置较短
jedis.setex(key, 60, "");
} else {
// 数据库中存在,写入缓存
jedis.setex(key, 3600, value);
}
}
// 利用布隆过滤器判断key是否存在
BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.forName("UTF-8")), 1000000);
if (!bloomFilter.mightContain(key)) {
// key不存在,直接返回
return null;
} else {
// key可能存在,查询缓存
String value = jedis.get(key);
if (value != null) {
return value;
} else {
// 缓存未命中,查询数据库
// ...
}
}
缓存击穿
缓存击穿是指一个热点Key在某个时间点过期,而恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端数据库加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端数据库压垮。常见的解决方案包括:
- 使用互斥锁,只让一个线程构建缓存,其他线程等待缓存构建完毕后从缓存中读取数据。
- 不同的Key设置不同的过期时间,避免同时失效。
String lockKey = "lock:key";
String lockValue = UUID.randomUUID().toString();
String value = jedis.get(key);
if (value == null) {
// 获取分布式锁
if ("OK".equals(jedis.set(lockKey, lockValue, "NX", "EX", 30))) {
// 获取锁成功,查询数据库
value = db.get(key);
// 写入缓存
jedis.setex(key, 3600, value);
// 释放锁
if (lockValue.equals(jedis.get(lockKey))) {
jedis.del(lockKey);
}
} else {
// 获取锁失败,等待一段时间后重试
Thread.sleep(100);
value = jedis.get(key);
}
}
总结
Redis凭借其出色的性能和丰富的数据类型,已经成为现代互联网应用不可或缺的利器。深入理解Redis的各种数据类型及其适用场景,并结合Java客户端进行开发,可以帮助我们设计出更加高效、可靠的系统。同时,在使用Redis时也要注意一些高级主题,如缓存预热、缓存雪崩、缓存穿透和缓存击穿等,这些都是保证系统稳定运行的关键。