Redis高级数据结构实战
引言
Redis作为高性能的内存数据存储,不仅支持String字符串类型,还提供了丰富的高级数据结构:Hash字典、List列表、Set集合、ZSet有序集合、Bitmap位图、HyperLogLog基数统计、Geospatial地理位置等。深入理解和正确使用这些数据结构,可以解决很多实际业务问题,如排行榜、计数器、分布式锁、消息队列、实时统计等。本文将详细介绍Redis各数据结构的特性、适用场景和最佳实践。
一、String类型
1.1 基础操作
String是Redis最基本的数据类型,可存储字符串、整数或浮点数。
@Service public class RedisStringService { private final StringRedisTemplate redisTemplate; public void basicOperations() { // 设置值 redisTemplate.opsForValue().set("user:100", "Alice"); // 设置值并指定过期时间 redisTemplate.opsForValue().set("captcha:13800138000", "123456", Duration.ofMinutes(5)); // 获取值 String value = redisTemplate.opsForValue().get("user:100"); // 批量设置 Map<String, String> userMap = new HashMap<>(); userMap.put("user:101", "Bob"); userMap.put("user:102", "Charlie"); redisTemplate.opsForMultiValue().multiSet(userMap); // 原子递增/递减 redisTemplate.opsForValue().increment("counter"); redisTemplate.opsForValue().increment("counter", 10); redisTemplate.opsForValue().decrement("counter"); } }1.2 分布式锁实现
@Service public class DistributedLockService { private final StringRedisTemplate redisTemplate; private static final String LOCK_PREFIX = "lock:"; private static final String UNLOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then " + " return redis.call('del', KEYS[1]) " + "else " + " return 0 " + "end"; public boolean tryLock(String key, String value, Duration timeout) { String lockKey = LOCK_PREFIX + key; Boolean result = redisTemplate.opsForValue() .setIfAbsent(lockKey, value, timeout); return Boolean.TRUE.equals(result); } public void unlock(String key, String value) { String lockKey = LOCK_PREFIX + key; redisTemplate.execute( new DefaultRedisScript<>(UNLOCK_SCRIPT, Long.class), Collections.singletonList(lockKey), value ); } // 防重入锁 public boolean tryReentrantLock(String key, String value, int maxRetry) { String lockKey = LOCK_PREFIX + key; String currentValue = redisTemplate.opsForValue().get(lockKey); if (currentValue != null && currentValue.equals(value)) { redisTemplate.opsForValue().increment(lockKey + ":count"); return true; } if (tryLock(key, value, Duration.ofSeconds(30))) { redisTemplate.opsForValue().set(lockKey + ":count", "1"); return true; } return false; } }二、Hash类型
1.1 基础操作
Hash适合存储对象类型的数据,比String类型更节省内存。
@Service public class RedisHashService { private final RedisTemplate<String, Object> redisTemplate; public void hashOperations() { String userKey = "user:100"; // 设置Hash字段 redisTemplate.opsForHash().put(userKey, "name", "Alice"); redisTemplate.opsForHash().put(userKey, "email", "alice@example.com"); redisTemplate.opsForHash().put(userKey, "age", "25"); // 批量设置 Map<String, Object> userMap = new HashMap<>(); userMap.put("city", "Beijing"); userMap.put("phone", "13800138000"); redisTemplate.opsForHash().putAll(userKey, userMap); // 获取单个字段 Object name = redisTemplate.opsForHash().get(userKey, "name"); // 获取所有字段和值 Map<Object, Object> allFields = redisTemplate.opsForHash().entries(userKey); // 判断字段是否存在 Boolean hasEmail = redisTemplate.opsForHash() .hasKey(userKey, "email"); // 删除字段 redisTemplate.opsForHash().delete(userKey, "phone"); // 字段值递增 redisTemplate.opsForHash().increment(userKey, "age", 1); } }1.2 购物车实现
@Service public class CartService { private final StringRedisTemplate redisTemplate; public void addToCart(String userId, String productId, int quantity) { String cartKey = "cart:" + userId; if (quantity <= 0) { redisTemplate.opsForHash().delete(cartKey, productId); return; } redisTemplate.opsForHash().put(cartKey, productId, String.valueOf(quantity)); } public Map<String, Integer> getCart(String userId) { String cartKey = "cart:" + userId; Map<Object, Object> cart = redisTemplate.opsForHash().entries(cartKey); Map<String, Integer> result = new HashMap<>(); cart.forEach((k, v) -> result.put( k.toString(), Integer.parseInt(v.toString()))); return result; } public void removeFromCart(String userId, String productId) { String cartKey = "cart:" + userId; redisTemplate.opsForHash().delete(cartKey, productId); } public void clearCart(String userId) { String cartKey = "cart:" + userId; redisTemplate.delete(cartKey); } }三、List类型
1.1 基础操作
List是双向链表,支持从两端插入和删除元素。
@Service public class RedisListService { private final RedisTemplate<String, String> redisTemplate; public void listOperations() { String listKey = "tasks"; // 从左边添加 redisTemplate.opsForList().leftPush(listKey, "task3"); redisTemplate.opsForList().leftPushAll(listKey, "task2", "task1"); // 从右边添加 redisTemplate.opsForList().rightPush(listKey, "task4"); // 获取列表长度 Long size = redisTemplate.opsForList().size(listKey); // 获取范围内元素 List<String> tasks = redisTemplate.opsForList() .range(listKey, 0, -1); // 获取所有 // 获取单个元素 String task = redisTemplate.opsForList().index(listKey, 0); // 从左边弹出 String popped = redisTemplate.opsForList().leftPop(listKey); // 阻塞弹出 String blocked = redisTemplate.opsForList() .leftPop(listKey, Duration.ofSeconds(10)); } }1.2 消息队列实现
@Service public class MessageQueueService { private final RedisTemplate<String, String> redisTemplate; public void sendMessage(String queueName, String message) { redisTemplate.opsForList().rightPush("queue:" + queueName, message); } public String receiveMessage(String queueName) { return redisTemplate.opsForList().leftPop("queue:" + queueName); } public String receiveMessageWithBlock(String queueName, Duration timeout) { return redisTemplate.opsForList() .leftPop("queue:" + queueName, timeout); } // 延时队列实现 public void sendDelayMessage(String queueName, String message, Duration delay) { String delayQueueKey = "delay:" + queueName; String msgWithScore = System.currentTimeMillis() + delay.toMillis() + ":" + message; redisTemplate.opsForZSet().add(delayQueueKey, msgWithScore, System.currentTimeMillis() + delay.toMillis()); } public String receiveDelayMessage(String queueName) { String delayQueueKey = "delay:" + queueName; Set<String> messages = redisTemplate.opsForZSet() .rangeByScore(delayQueueKey, 0, System.currentTimeMillis()); if (messages.isEmpty()) { return null; } String message = messages.iterator().next(); Long removed = redisTemplate.opsForZSet() .remove(delayQueueKey, message); if (removed > 0) { return message.substring(message.indexOf(":") + 1); } return null; } }四、Set类型
1.1 基础操作
Set是无序不重复集合,适合标签、好友关系等场景。
@Service public class RedisSetService { private final RedisTemplate<String, String> redisTemplate; public void setOperations() { String tagKey = "article:100:tags"; // 添加标签 redisTemplate.opsForSet().add(tagKey, "Java", "Spring", "Redis", "Backend"); // 获取所有标签 Set<String> tags = redisTemplate.opsForSet().members(tagKey); // 判断是否包含 Boolean hasTag = redisTemplate.opsForSet() .isMember(tagKey, "Java"); // 获取集合大小 Long size = redisTemplate.opsForSet().size(tagKey); // 删除标签 redisTemplate.opsForSet().remove(tagKey, "Redis"); // 随机获取并移除 String randomTag = redisTemplate.opsForSet().pop(tagKey); } }1.2 抽奖系统
@Service public class LotteryService { private final RedisTemplate<String, String> redisTemplate; public void addParticipant(String activityId, String userId) { String key = "lottery:" + activityId; redisTemplate.opsForSet().add(key, userId); } public boolean hasParticipated(String activityId, String userId) { String key = "lottery:" + activityId; return Boolean.TRUE.equals( redisTemplate.opsForSet().isMember(key, userId)); } public String drawLottery(String activityId) { String key = "lottery:" + activityId; return redisTemplate.opsForSet().pop(key); } public List<String> drawMultiple(String activityId, int count) { String key = "lottery:" + activityId; List<String> winners = new ArrayList<>(); for (int i = 0; i < count; i++) { String winner = redisTemplate.opsForSet().pop(key); if (winner != null) { winners.add(winner); } } return winners; } public Set<String> getParticipants(String activityId) { String key = "lottery:" + activityId; return redisTemplate.opsForSet().members(key); } }五、ZSet有序集合
5.1 基础操作
ZSet是带分数的有序集合,元素按分数排序。
@Service public class RedisZSetService { private final RedisTemplate<String, String> redisTemplate; public void zsetOperations() { String leaderboardKey = "game:leaderboard"; // 添加玩家分数 redisTemplate.opsForZSet().add(leaderboardKey, "player1", 1000); redisTemplate.opsForZSet().add(leaderboardKey, "player2", 2500); redisTemplate.opsForZSet().add(leaderboardKey, "player3", 1800); // 批量添加 Set<ZSetOperations.TypedTuple<String>> tuples = new HashSet<>(); tuples.add(ZSetOperations.TypedTuple.of("player4", 2200.0)); tuples.add(ZSetOperations.TypedTuple.of("player5", 3000.0)); redisTemplate.opsForZSet().add(leaderboardKey, tuples); // 获取排名(从小到大,0开始) Long rank = redisTemplate.opsForZSet() .rank(leaderboardKey, "player3"); // player3排名第2 // 获取逆序排名(从大到小) Long reverseRank = redisTemplate.opsForZSet() .reverseRank(leaderboardKey, "player3"); // player3排名第2 // 获取分数 Double score = redisTemplate.opsForZSet() .score(leaderboardKey, "player3"); // 分数递增 redisTemplate.opsForZSet().incrementScore( leaderboardKey, "player3", 500); // 获取top N Set<String> topPlayers = redisTemplate.opsForZSet() .reverseRange(leaderboardKey, 0, 9); // 获取分数范围内的玩家 Set<String> midPlayers = redisTemplate.opsForZSet() .rangeByScore(leaderboardKey, 1500, 2500); } }5.2 排行榜实现
@Service public class LeaderboardService { private final RedisTemplate<String, String> redisTemplate; public void updateScore(String gameId, String playerId, int score) { String key = "game:" + gameId + ":leaderboard"; redisTemplate.opsForZSet().add(key, playerId, score); } public Long getRank(String gameId, String playerId) { String key = "game:" + gameId + ":leaderboard"; Long rank = redisTemplate.opsForZSet().reverseRank(key, playerId); return rank != null ? rank + 1 : null; } public List<RankInfo> getTopN(String gameId, int n) { String key = "game:" + gameId + ":leaderboard"; Set<ZSetOperations.TypedTuple<String>> topSet = redisTemplate.opsForZSet().reverseRangeWithScores(key, 0, n - 1); List<RankInfo> result = new ArrayList<>(); int rank = 1; for (ZSetOperations.TypedTuple<String> tuple : topSet) { result.add(new RankInfo( rank++, tuple.getValue(), tuple.getScore().intValue() )); } return result; } public List<RankInfo> getRankRange(String gameId, long start, long end) { String key = "game:" + gameId + ":leaderboard"; Set<ZSetOperations.TypedTuple<String>> rangeSet = redisTemplate.opsForZSet().reverseRangeWithScores(key, start, end); List<RankInfo> result = new ArrayList<>(); long rank = start + 1; for (ZSetOperations.TypedTuple<String> tuple : rangeSet) { result.add(new RankInfo( rank++, tuple.getValue(), tuple.getScore().intValue() )); } return result; } public List<RankInfo> getRankAroundMe(String gameId, String playerId) { String key = "game:" + gameId + ":leaderboard"; Long myRank = redisTemplate.opsForZSet() .reverseRank(key, playerId); if (myRank == null) { return Collections.emptyList(); } long start = Math.max(0, myRank - 2); long end = myRank + 2; Set<ZSetOperations.TypedTuple<String>> rangeSet = redisTemplate.opsForZSet().reverseRangeWithScores(key, start, end); List<RankInfo> result = new ArrayList<>(); long rank = start + 1; for (ZSetOperations.TypedTuple<String> tuple : rangeSet) { result.add(new RankInfo( rank++, tuple.getValue(), tuple.getScore().intValue() )); } return result; } }六、Bitmap位图
6.1 基础操作
Bitmap使用位来存储数据,适合签到、在线状态等场景。
@Service public class RedisBitmapService { private final RedisTemplate<String, String> redisTemplate; public void bitmapOperations() { String signKey = "user:100:sign"; // 设置某天签到(2024年1月1日是第1天偏移量) long offset = calculateOffset("2024-01-15"); redisTemplate.opsForValue().setBit(signKey, offset, true); // 检查是否签到 Boolean signed = redisTemplate.opsForValue() .getBit(signKey, offset); // 统计签到天数 Long signDays = redisTemplate.opsForBitCount(signKey); // 统计某月签到天数 Long monthSignDays = redisTemplate.opsForBitCount(signKey, calculateOffset("2024-01-01"), calculateOffset("2024-01-31")); } private long calculateOffset(String date) { LocalDate targetDate = LocalDate.parse(date); LocalDate baseDate = LocalDate.of(2024, 1, 1); return ChronoUnit.DAYS.between(baseDate, targetDate); } }6.2 签到系统实现
@Service public class SignService { private final RedisTemplate<String, String> redisTemplate; public boolean sign(String userId, LocalDate date) { String key = getSignKey(userId, date.getYear()); long offset = ChronoUnit.DAYS.between( date.withDayOfYear(1), date); Boolean result = redisTemplate.opsForValue().setBit(key, offset, true); return !Boolean.TRUE.equals(result); // true表示今天已签到 } public boolean hasSigned(String userId, LocalDate date) { String key = getSignKey(userId, date.getYear()); long offset = ChronoUnit.DAYS.between( date.withDayOfYear(1), date); return Boolean.TRUE.equals( redisTemplate.opsForValue().getBit(key, offset)); } public int getSignCount(String userId, LocalDate startDate, LocalDate endDate) { String key = getSignKey(userId, startDate.getYear()); long startOffset = ChronoUnit.DAYS.between( startDate.withDayOfYear(1), startDate); long endOffset = ChronoUnit.DAYS.between( startDate.withDayOfYear(1), endDate); Long count = redisTemplate.opsForBitCount(key, startOffset, endOffset); return count != null ? count.intValue() : 0; } public List<Boolean> getMonthSignRecord(String userId, int year, int month) { String key = getSignKey(userId, year); LocalDate monthStart = LocalDate.of(year, month, 1); LocalDate monthEnd = monthStart.withDayOfMonth( monthStart.lengthOfMonth()); List<Boolean> result = new ArrayList<>(); for (int day = 1; day <= monthEnd.getDayOfMonth(); day++) { LocalDate date = LocalDate.of(year, month, day); long offset = ChronoUnit.DAYS.between(monthStart, date); Boolean bit = redisTemplate.opsForValue().getBit(key, offset); result.add(Boolean.TRUE.equals(bit)); } return result; } private String getSignKey(String userId, int year) { return String.format("sign:%s:%d", userId, year); } }七、HyperLogLog
7.1 UV统计实现
@Service public class UVStatisticsService { private final RedisTemplate<String, String> redisTemplate; public void addUV(String date) { String key = "uv:" + date; String userId = generateUserId(); // 生成访客ID redisTemplate.opsForHyperLogLog().add(key, userId); } public long getUV(String date) { String key = "uv:" + date; Long size = redisTemplate.opsForHyperLogLog().size(key); return size != null ? size : 0; } public long getUVRange(String startDate, String endDate) { List<String> keys = new ArrayList<>(); LocalDate start = LocalDate.parse(startDate); LocalDate end = LocalDate.parse(endDate); while (!start.isAfter(end)) { keys.add("uv:" + start.toString()); start = start.plusDays(1); } Long size = redisTemplate.opsForHyperLogLog().size(keys.toArray( new String[0])); return size != null ? size : 0; } private String generateUserId() { return UUID.randomUUID().toString(); } }八、GEO地理位置
8.1 附近的人实现
@Service public class GeoService { private final RedisTemplate<String, String> redisTemplate; public void addLocation(String userId, double longitude, double latitude) { redisTemplate.opsForGeo().add("user:locations", new Point(longitude, latitude), userId); } public List<NearbyUser> getNearbyUsers(String userId, double radiusKm) { // 获取用户位置 List<Point> positions = redisTemplate.opsForGeo() .position("user:locations", userId); if (positions == null || positions.isEmpty() || positions.get(0) == null) { return Collections.emptyList(); } Point myPosition = positions.get(0); // 搜索附近用户 GeoResults<RedisGeoCommands.GeoLocation<String>> results = redisTemplate.opsForGeo().radius("user:locations", new Circle(myPosition, new Distance(radiusKm, Metrics.KILOMETERS)), RedisGeoCommands.GeoRadiusCommandArgs .newGeoRadiusArgs() .includeDistance() .includeCoordinates() .sortAscending() .limit(100)); List<NearbyUser> nearbyUsers = new ArrayList<>(); if (results != null) { for (GeoResult<RedisGeoCommands.GeoLocation<String>> result : results.getContent()) { nearbyUsers.add(new NearbyUser( result.getContent().getName(), result.getDistance().getValue(), result.getContent().getPoint().getX(), result.getContent().getPoint().getY() )); } } return nearbyUsers; } public double calculateDistance(String userId1, String userId2) { Distance distance = redisTemplate.opsForGeo() .distance("user:locations", userId1, userId2, Metrics.KILOMETERS); return distance != null ? distance.getValue() : 0; } }九、Pipeline管道
9.1 批量操作优化
@Service public class RedisPipelineService { private final RedisTemplate<String, String> redisTemplate; public void batchOperations() { long startTime = System.currentTimeMillis(); // 使用Pipeline批量执行 List<Object> results = redisTemplate.executePipelined( (RedisCallback<Object>) connection -> { for (int i = 0; i < 1000; i++) { connection.stringCommands() .set(("key:" + i).getBytes(), ("value:" + i).getBytes()); } return null; }); long endTime = System.currentTimeMillis(); System.out.println("Pipeline执行1000次操作耗时: " + (endTime - startTime) + "ms"); } public Map<String, String> batchGet(List<String> keys) { List<Object> results = redisTemplate.executePipelined( (RedisCallback<Object>) connection -> { for (String key : keys) { connection.stringCommands().get(key.getBytes()); } return null; }); Map<String, String> map = new HashMap<>(); for (int i = 0; i < keys.size(); i++) { String value = (String) results.get(i); if (value != null) { map.put(keys.get(i), value); } } return map; } }十、最佳实践
10.1 键命名规范
// 推荐命名格式:业务:实体:属性 String key = "order:100:status"; // 订单状态 String key = "user:100:profile"; // 用户资料 String key = "session:abc123:data"; // 会话数据 String key = "cache:product:100"; // 产品缓存 // 避免的命名 String key = "data"; // 太模糊 String key = "myvalue"; // 无意义 String key = "123"; // 无描述性10.2 内存优化
@Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate( RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); // 使用StringRedisSerializer作为key的序列化器 template.setKeySerializer(new StringRedisSerializer()); template.setHashKeySerializer(new StringRedisSerializer()); // value使用JdkSerializationRedisSerializer template.setValueSerializer( new JdkSerializationRedisSerializer()); template.setHashValueSerializer( new JdkSerializationRedisSerializer()); template.afterPropertiesSet(); return template; } }总结
Redis提供了丰富的数据结构,每种结构都有其独特的适用场景。String适合简单的键值存储和计数器;Hash适合存储对象和字典类型数据;List适合消息队列和任务队列;Set适合去重和集合运算;ZSet适合排行榜和有序数据;Bitmap适合签到和状态监控;HyperLogLog适合UV统计;GEO适合附近的人和地理位置相关应用。在实际开发中,需要根据业务特点选择合适的数据结构,并注意键命名规范、内存优化和性能监控。正确使用Redis数据结构,可以构建高性能、易扩展的应用系统。