欢迎光临
我们一直在努力

Redis工具类-RedisUtil

更加完整详细内容可查看【免费版Java学习笔记】和【免费版Java面试题】

免费版Java学习笔记(28w字)链接:https://www.yuque.com/aoyouaoyou/sgcqr8
免费版Java面试题(20w字)链接:https://www.yuque.com/aoyouaoyou/wh3hto
完整版Java学习笔记200w字,附有代码实现,图解清楚,仅需9.9
完整版Java面试题,150w字,高频面试题,内容详细,仅需9.9
完整版链接:
https://www.xiaohongshu.com/user/profile/63c2d512000000002601232c
祝您新的一年事事马到成功,身体健康,阖家幸福,大展宏图!

package com.ao.commonUtil.util.redis;

import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DataAccessException;
import org.springframework.data.geo.*;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.*;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.*;
import org.springframework.data.redis.connection.RedisGeoCommands;

@Slf4j
@Component
public class RedisUtil {

private final RedisTemplate<String, Object> redisTemplate;
private final ValueOperations<String, Object> valueOps;
private final ListOperations<String, Object> listOps;
private final SetOperations<String, Object> setOps;
private final HashOperations<String, String, Object> hashOps;
private final ZSetOperations<String, Object> zSetOps;
private final GeoOperations<String, Object> geoOps;
private final HyperLogLogOperations<String, Object> hllOps;

public RedisUtil(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
this.valueOps = redisTemplate.opsForValue();
this.listOps = redisTemplate.opsForList();
this.setOps = redisTemplate.opsForSet();
this.hashOps = redisTemplate.opsForHash();
this.zSetOps = redisTemplate.opsForZSet();
this.geoOps = redisTemplate.opsForGeo();
this.hllOps = redisTemplate.opsForHyperLogLog();
}

// ============================ 通用命令 ============================

/**
* 删除key
*/
public Boolean delete(String key) {
return redisTemplate.delete(key);
}

/**
* 批量删除key
*/
public Long delete(Collection<String> keys) {
return redisTemplate.delete(keys);
}

/**
* 判断key是否存在
*/
public Boolean exists(String key) {
return redisTemplate.hasKey(key);
}

/**
* 设置过期时间
*/
public Boolean expire(String key, long timeout, TimeUnit unit) {
return redisTemplate.expire(key, timeout, unit);
}

/**
* 设置过期时间(秒)
*/
public Boolean expire(String key, long seconds) {
return expire(key, seconds, TimeUnit.SECONDS);
}

/**
* 获取过期时间
*/
public Long getExpire(String key, TimeUnit unit) {
return redisTemplate.getExpire(key, unit);
}

/**
* 获取过期时间(秒)
*/
public Long getExpire(String key) {
return getExpire(key, TimeUnit.SECONDS);
}

/**
* 移除key的过期时间
*/
public Boolean persist(String key) {
return redisTemplate.persist(key);
}

/**
* 获取key的类型
*/
public String type(String key) {
return redisTemplate.type(key).code();
}

// ============================ String ============================

/**
* 设置字符串值
*/
public void set(String key, Object value) {
valueOps.set(key, value);
}

/**
* 设置字符串值并设置过期时间
*/
public void set(String key, Object value, long timeout, TimeUnit unit) {
valueOps.set(key, value, timeout, unit);
}

/**
* 设置字符串值并设置过期时间(秒)
*/
public void set(String key, Object value, long seconds) {
set(key, value, seconds, TimeUnit.SECONDS);
}

/**
* 当key不存在时设置字符串值
*/
public Boolean setIfAbsent(String key, Object value) {
return valueOps.setIfAbsent(key, value);
}

/**
* 当key不存在时设置字符串值并设置过期时间
*/
public Boolean setIfAbsent(String key, Object value, long timeout, TimeUnit unit) {
return valueOps.setIfAbsent(key, value, timeout, unit);
}

/**
* 获取字符串值
*/
public Object get(String key) {
return valueOps.get(key);
}

/**
* 获取字符串值并转换为指定类型
*/
@SuppressWarnings("unchecked")
public <T> T get(String key, Class<T> clazz) {
return (T) valueOps.get(key);
}

/**
* 自增
*/
public Long increment(String key) {
return valueOps.increment(key);
}

/**
* 自增指定值
*/
public Long increment(String key, long delta) {
return valueOps.increment(key, delta);
}

/**
* 自减
*/
public Long decrement(String key) {
return valueOps.decrement(key);
}

/**
* 自减指定值
*/
public Long decrement(String key, long delta) {
return valueOps.decrement(key, delta);
}

/**
* 追加字符串
*/
public Integer append(String key, String value) {
return valueOps.append(key, value);
}

/**
* 获取字符串长度
*/
public Long stringSize(String key) {
return valueOps.size(key);
}

// ============================ List ============================

/**
* 从左侧插入元素
*/
public Long lPush(String key, Object value) {
return listOps.leftPush(key, value);
}

/**
* 从左侧批量插入元素
* @param key
* @param values 数组形式
*/
public Long lPushAll(String key, Object… values) {
return listOps.leftPushAll(key, values);
}

/**
* 从右侧插入元素
*/
public Long rPush(String key, Object value) {
return listOps.rightPush(key, value);
}

/**
* 从右侧批量插入元素
*/
public Long rPushAll(String key, Object… values) {
return listOps.rightPushAll(key, values);
}

/**
* 从左侧弹出元素
*/
public Object lPop(String key) {
return listOps.leftPop(key);
}

/**
* 从左侧弹出元素(带超时)
*/
public Object lPop(String key, long timeout, TimeUnit unit) {
return listOps.leftPop(key, timeout, unit);
}

/**
* 从右侧弹出元素
*/
public Object rPop(String key) {
return listOps.rightPop(key);
}

/**
* 从右侧弹出元素(带超时)
*/
public Object rPop(String key, long timeout, TimeUnit unit) {
return listOps.rightPop(key, timeout, unit);
}

/**
* 获取列表长度
*/
public Long lLen(String key) {
return listOps.size(key);
}

/**
* 获取列表指定范围的元素
*/
public List<Object> lRange(String key, long start, long end) {
return listOps.range(key, start, end);
}

/**
* 根据索引获取列表元素
*/
public Object lIndex(String key, long index) {
return listOps.index(key, index);
}

/**
* 设置列表指定索引的元素
*/
public void lSet(String key, long index, Object value) {
listOps.set(key, index, value);
}

/**
* 移除列表中与value相等的元素
*/
public Long lRemove(String key, long count, Object value) {
return listOps.remove(key, count, value);
}

/**
* 裁剪列表,只保留指定范围内的元素
*/
public void lTrim(String key, long start, long end) {
listOps.trim(key, start, end);
}

/**
* 从源队列右侧弹出元素并压入目标队列左侧(原子操作)
* @param sourceKey 源队列Key
* @param destinationKey 目标队列Key
* @return 弹出并压入的元素,如果源队列为空返回null
*/
public Object rPopLPush(String sourceKey, String destinationKey) {
return redisTemplate.opsForList().rightPopAndLeftPush(sourceKey, destinationKey);
}

/**
* 阻塞式右弹出元素
* @param timeout 超时时间(秒)
* @param keys 一个或多个队列key
* @return 包含队列名和元素的列表(第一个元素是队列名,第二个是元素值)
*/
public List<Object> bRPop(int timeout, String… keys) {
return redisTemplate.execute(new RedisCallback<List<Object>>() {
@Override
public List<Object> doInRedis(RedisConnection connection) throws DataAccessException {
RedisSerializer<String> keySerializer = getKeySerializer();

byte[][] keyBytes = Arrays.stream(keys)
.map(keySerializer::serialize)
.toArray(byte[][]::new);

List<byte[]> result = connection.bRPop(timeout, keyBytes);

return deserializeResult(keySerializer, result);
}
});
}

/**
* 阻塞式左弹出元素(带详细日志)
* @param timeout 超时时间(秒)
* @param keys 一个或多个队列key
* @return 包含队列名和元素的列表,格式为 [队列名, 元素值]
*
* 示例:
* 1. bLPop(30, "queue1", "queue2")
* – 先检查queue1,如果非空则弹出元素
* – 如果queue1为空,则检查queue2
* – 如果都为空则阻塞等待30秒
* 2. 返回值示例:["queue1", "message123"]
*/
public List<Object> bRPopWithLog(int timeout, String… keys) {
log.debug("准备执行BRPOP,超时时间:{}秒,队列:{}", timeout, Arrays.toString(keys));

List<Object> result = redisTemplate.execute(new RedisCallback<List<Object>>() {
@Override
public List<Object> doInRedis(RedisConnection connection) throws DataAccessException {
long startTime = System.currentTimeMillis();
RedisSerializer<String> keySerializer = getKeySerializer();

byte[][] keyBytes = Arrays.stream(keys)
.peek(key -> log.debug("序列化队列key:{}", key))
.map(keySerializer::serialize)
.toArray(byte[][]::new);

List<byte[]> rawResult = connection.bRPop(timeout, keyBytes);

log.debug("准备执行BRPOP执行耗时:{}ms", System.currentTimeMillis() – startTime);
return deserializeResult(keySerializer, rawResult);
}
});

if (result != null) {
log.info("从队列 [{}] 获取到消息:{}", result.get(0), result.get(1));
} else {
log.debug("BLPOP操作超时,未获取到消息");
}

return result;
}

/**
* 阻塞式左弹出元素
* @param timeout 超时时间(秒)
* @param keys 一个或多个队列key
* @return 包含队列名和元素的列表(第一个元素是队列名,第二个是元素值)
*/
public List<Object> bLPop(int timeout, String… keys) {
return redisTemplate.execute(new RedisCallback<List<Object>>() {
@Override
public List<Object> doInRedis(RedisConnection connection) throws DataAccessException {
RedisSerializer<String> keySerializer = getKeySerializer();

byte[][] keyBytes = Arrays.stream(keys)
.map(keySerializer::serialize)
.toArray(byte[][]::new);

List<byte[]> result = connection.bLPop(timeout, keyBytes);

return deserializeResult(keySerializer, result);
}
});
}

/**
* 阻塞式左弹出元素(带详细日志)
* @param timeout 超时时间(秒)
* @param keys 一个或多个队列key
* @return 包含队列名和元素的列表,格式为 [队列名, 元素值]
*
* 示例:
* 1. bLPop(30, "queue1", "queue2")
* – 先检查queue1,如果非空则弹出元素
* – 如果queue1为空,则检查queue2
* – 如果都为空则阻塞等待30秒
* 2. 返回值示例:["queue1", "message123"]
*/
public List<Object> bLPopWithLog(int timeout, String… keys) {
log.debug("准备执行BLPOP,超时时间:{}秒,队列:{}", timeout, Arrays.toString(keys));

List<Object> result = redisTemplate.execute(new RedisCallback<List<Object>>() {
@Override
public List<Object> doInRedis(RedisConnection connection) throws DataAccessException {
long startTime = System.currentTimeMillis();
RedisSerializer<String> keySerializer = getKeySerializer();

byte[][] keyBytes = Arrays.stream(keys)
.peek(key -> log.debug("序列化队列key:{}", key))
.map(keySerializer::serialize)
.toArray(byte[][]::new);

List<byte[]> rawResult = connection.bLPop(timeout, keyBytes);

log.debug("BLPOP执行耗时:{}ms", System.currentTimeMillis() – startTime);
return deserializeResult(keySerializer, rawResult);
}
});

if (result != null) {
log.info("从队列 [{}] 获取到消息:{}", result.get(0), result.get(1));
} else {
log.debug("BLPOP操作超时,未获取到消息");
}

return result;
}

/**
* 阻塞式从源队列右弹出元素并左压入目标队列
* @param sourceKey 源队列key
* @param destinationKey 目标队列key
* @param timeout 超时时间(秒)
* @return 被移动的元素值
*/
public Object bRPopLPush(String sourceKey, String destinationKey, int timeout) {
return redisTemplate.execute(new RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
RedisSerializer<String> keySerializer = getKeySerializer();

byte[] source = keySerializer.serialize(sourceKey);
byte[] destination = keySerializer.serialize(destinationKey);

byte[] result = connection.bRPopLPush(timeout, source, destination);

return result != null ?
redisTemplate.getValueSerializer().deserialize(result) :
null;
}
});
}

private RedisSerializer<String> getKeySerializer() {
@SuppressWarnings("unchecked")
RedisSerializer<String> keySerializer =
(RedisSerializer<String>) redisTemplate.getKeySerializer();
return keySerializer;
}

private List<Object> deserializeResult(RedisSerializer<String> keySerializer, List<byte[]> result) {
if (result == null || result.size() < 2) {
return null;
}

List<Object> deserialized = new ArrayList<>(2);
deserialized.add(keySerializer.deserialize(result.get(0)));
deserialized.add(redisTemplate.getValueSerializer().deserialize(result.get(1)));
return deserialized;
}

// ============================ Set ============================

/**
* 向集合添加元素
*/
public Long sAdd(String key, Object… values) {
return setOps.add(key, values);
}

/**
* 从集合中移除元素
*/
public Long sRemove(String key, Object… values) {
return setOps.remove(key, values);
}

/**
* 从集合中随机移除并返回一个元素
*/
public Object sPop(String key) {
return setOps.pop(key);
}

/**
* 从集合中随机获取一个元素
*/
public Object sRandomMember(String key) {
return setOps.randomMember(key);
}

/**
* 从集合中随机获取多个元素(允许重复)
*/
public List<Object> sRandomMembers(String key, long count) {
return setOps.randomMembers(key, count);
}

/**
* 从集合中随机获取多个元素(不重复)
*/
public Set<Object> sDistinctRandomMembers(String key, long count) {
return setOps.distinctRandomMembers(key, count);
}

/**
* 获取集合所有元素
*/
public Set<Object> sMembers(String key) {
return setOps.members(key);
}

/**
* 判断元素是否在集合中
*/
public Boolean sIsMember(String key, Object value) {
return setOps.isMember(key, value);
}

/**
* 获取集合元素数量
*/
public Long sCard(String key) {
return setOps.size(key);
}

/**
* 移动元素到另一个集合
*/
public Boolean sMove(String key, Object value, String destKey) {
return setOps.move(key, value, destKey);
}

/**
* 求两个集合的差集
*/
public Set<Object> sDifference(String key, String otherKey) {
return setOps.difference(key, otherKey);
}

/**
* 求多个集合的差集
*/
public Set<Object> sDifference(String key, Collection<String> otherKeys) {
return setOps.difference(key, otherKeys);
}

/**
* 求两个集合的差集并存储到目标集合
*/
public Long sDifferenceAndStore(String key, String otherKey, String destKey) {
return setOps.differenceAndStore(key, otherKey, destKey);
}

/**
* 求多个集合的差集并存储到目标集合
*/
public Long sDifferenceAndStore(String key, Collection<String> otherKeys, String destKey) {
return setOps.differenceAndStore(key, otherKeys, destKey);
}

/**
* 求两个集合的交集
*/
public Set<Object> sIntersect(String key, String otherKey) {
return setOps.intersect(key, otherKey);
}

/**
* 求多个集合的交集
*/
public Set<Object> sIntersect(String key, Collection<String> otherKeys) {
return setOps.intersect(key, otherKeys);
}

/**
* 求两个集合的交集并存储到目标集合
*/
public Long sIntersectAndStore(String key, String otherKey, String destKey) {
return setOps.intersectAndStore(key, otherKey, destKey);
}

/**
* 求多个集合的交集并存储到目标集合
*/
public Long sIntersectAndStore(String key, Collection<String> otherKeys, String destKey) {
return setOps.intersectAndStore(key, otherKeys, destKey);
}

/**
* 求两个集合的并集
*/
public Set<Object> sUnion(String key, String otherKey) {
return setOps.union(key, otherKey);
}

/**
* 求多个集合的并集
*/
public Set<Object> sUnion(String key, Collection<String> otherKeys) {
return setOps.union(key, otherKeys);
}

/**
* 求两个集合的并集并存储到目标集合
*/
public Long sUnionAndStore(String key, String otherKey, String destKey) {
return setOps.unionAndStore(key, otherKey, destKey);
}

/**
* 求多个集合的并集并存储到目标集合
*/
public Long sUnionAndStore(String key, Collection<String> otherKeys, String destKey) {
return setOps.unionAndStore(key, otherKeys, destKey);
}

// ============================ Hash ============================

/**
* 设置哈希字段值
*/
public void hSet(String key, String hashKey, Object value) {
hashOps.put(key, hashKey, value);
}

/**
* 批量设置哈希字段值
*/
public void hMSet(String key, Map<String, Object> map) {
hashOps.putAll(key, map);
}

/**
* 当哈希字段不存在时设置值
*/
public Boolean hPutIfAbsent(String key, String hashKey, Object value) {
return hashOps.putIfAbsent(key, hashKey, value);
}

/**
* 获取哈希字段值
*/
public Object hGet(String key, String hashKey) {
return hashOps.get(key, hashKey);
}

/**
* 获取哈希多个字段值
*/
public List<Object> hMGet(String key, Collection<String> hashKeys) {
return hashOps.multiGet(key, hashKeys);
}

/**
* 获取哈希所有字段和值
*/
public Map<String, Object> hGetAll(String key) {
return hashOps.entries(key);
}
/**
* 获取哈希表中所有字段和值(带泛型支持)
* @param key Redis键
* @return 包含所有字段和值的Map
*
* Redis命令:
* HGETALL key
*/
public <HK, HV> Map<HK, HV> hGetAll(String key, Class<HK> hashKeyType, Class<HV> valueType) {
return redisTemplate.<HK, HV>opsForHash().entries(key);
}

/**
* 删除哈希字段
*/
public Long hDelete(String key, Object… hashKeys) {
return hashOps.delete(key, hashKeys);
}

/**
* 判断哈希字段是否存在
*/
public Boolean hHasKey(String key, String hashKey) {
return hashOps.hasKey(key, hashKey);
}

/**
* 获取哈希所有字段
*/
public Set<String> hKeys(String key) {
return hashOps.keys(key);
}

/**
* 获取哈希所有值
*/
public List<Object> hValues(String key) {
return hashOps.values(key);
}

/**
* 获取哈希字段数量
*/
public Long hLen(String key) {
return hashOps.size(key);
}

/**
* 哈希字段自增
*/
public Long hIncrementLong(String key, String hashKey, long delta) {
return hashOps.increment(key, hashKey, delta);
}

/**
* 哈希字段自增(浮点数)
*/
public Double hIncrementDouble(String key, String hashKey, double delta) {
return hashOps.increment(key, hashKey, delta);
}

// ============================ ZSet ============================

/**
* 向有序集合添加元素
*/
public Boolean zAdd(String key, Object value, double score) {
return zSetOps.add(key, value, score);
}

/**
* 批量向有序集合添加元素
*/
public Long zAdd(String key, Set<ZSetOperations.TypedTuple<Object>> tuples) {
return zSetOps.add(key, tuples);
}

/**
* 批量向有序集合添加元素 添加元素类型为Map key是元素 value是分数
*/
public Boolean zAddMap(String key, Map<Object, Double> scoreMembers) {
if (scoreMembers == null || scoreMembers.isEmpty()) {
return false;
}

Set<ZSetOperations.TypedTuple<Object>> tuples = new HashSet<>();
for (Map.Entry<Object, Double> entry : scoreMembers.entrySet()) {
ZSetOperations.TypedTuple<Object> tuple =
new DefaultTypedTuple<>(entry.getKey(), entry.getValue());
tuples.add(tuple);
}

redisTemplate.opsForZSet().add(key, tuples);
return true;
}

/**
* 从有序集合中移除元素
*/
public Long zRemove(String key, Object… values) {
return zSetOps.remove(key, values);
}

/**
* 增加元素的分数
*/
public Double zIncrementScore(String key, Object value, double delta) {
return zSetOps.incrementScore(key, value, delta);
}

/**
* 获取元素的排名(从小到大)
*/
public Long zRank(String key, Object value) {
return zSetOps.rank(key, value);
}

/**
* 获取元素的排名(从大到小)
*/
public Long zRevRank(String key, Object value) {
return zSetOps.reverseRank(key, value);
}

/**
* 获取有序集合元素数量
*/
public Long zCard(String key) {
return zSetOps.zCard(key);
}

/**
* 获取元素的分数
*/
public Double zScore(String key, Object value) {
return zSetOps.score(key, value);
}

/**
* 获取有序集合指定范围的元素(按分数从小到大)
*/
public Set<Object> zRange(String key, long start, long end) {
return zSetOps.range(key, start, end);
}

/**
* 获取有序集合指定范围的元素(按分数从大到小)
*/
public Set<Object> zReverseRange(String key, long start, long end) {
return zSetOps.reverseRange(key, start, end);
}

/**
* 获取有序集合指定分数范围的元素
*/
public Set<Object> zRangeByScore(String key, double min, double max) {
return zSetOps.rangeByScore(key, min, max);
}

/**
* 获取有序集合指定分数范围的元素(带偏移量和数量)
*/
public Set<Object> zRangeByScore(String key, double min, double max, long offset, long count) {
return zSetOps.rangeByScore(key, min, max, offset, count);
}

/**
* 获取有序集合指定分数范围的元素(带偏移量和数量,按分数从大到小)
*/
public Set<Object> zReverseRangeByScore(String key, double min, double max, long offset, long count) {
return zSetOps.reverseRangeByScore(key, min, max, offset, count);
}

/**
* 获取有序集合中指定区间的元素(逆序,带分数)
* @param key Redis键
* @param start 开始索引(包含)
* @param end 结束索引(包含)
* @return 包含成员和分数的集合,格式为:Map.Entry<成员, 分数>
*/
public Set<Map.Entry<Object, Double>> zReverseRangeWithScores(String key, long start, long end) {
Set<ZSetOperations.TypedTuple<Object>> typedTuples = redisTemplate.opsForZSet()
.reverseRangeWithScores(key, start, end);

if (typedTuples == null) {
return Collections.emptySet();
}

Set<Map.Entry<Object, Double>> result = new LinkedHashSet<>(typedTuples.size());
for (ZSetOperations.TypedTuple<Object> tuple : typedTuples) {
result.add(new AbstractMap.SimpleEntry<>(tuple.getValue(), tuple.getScore()));
}
return result;
}

/**
* 获取有序集合指定分数范围的元素(带分数)
*/
public Set<ZSetOperations.TypedTuple<Object>> zRangeByScoreWithScores(String key, double min, double max) {
return zSetOps.rangeByScoreWithScores(key, min, max);
}

/**
* 获取有序集合指定分数范围的元素(带分数,带偏移量和数量)
*/
public Set<ZSetOperations.TypedTuple<Object>> zRangeByScoreWithScores(String key, double min, double max, long offset, long count) {
return zSetOps.rangeByScoreWithScores(key, min, max, offset, count);
}

/**
* 获取有序集合指定分数范围的元素(带分数,按分数从大到小)
*/
public Set<ZSetOperations.TypedTuple<Object>> zReverseRangeByScoreWithScores(String key, double min, double max) {
return zSetOps.reverseRangeByScoreWithScores(key, min, max);
}

/**
* 获取有序集合指定分数范围的元素(带分数,带偏移量和数量,按分数从大到小)
*/
public Set<ZSetOperations.TypedTuple<Object>> zReverseRangeByScoreWithScores(String key, double min, double max, long offset, long count) {
return zSetOps.reverseRangeByScoreWithScores(key, min, max, offset, count);
}

/**
* 统计有序集合指定分数范围内的元素数量
*/
public Long zCount(String key, double min, double max) {
return zSetOps.count(key, min, max);
}

/**
* 移除有序集合指定排名范围的元素
*/
public Long zRemoveRange(String key, long start, long end) {
return zSetOps.removeRange(key, start, end);
}

/**
* 移除有序集合指定分数范围的元素
*/
public Long zRemoveRangeByScore(String key, double min, double max) {
return zSetOps.removeRangeByScore(key, min, max);
}

// ============================ Geo ============================

/**
* 添加地理位置
*/
public Long geoAdd(String key, double longitude, double latitude, Object member) {
return geoOps.add(key, new RedisGeoCommands.GeoLocation<>(member, new Point(longitude, latitude)));
}

/**
* 批量添加地理位置
*/
public Long geoAdd(String key, Map<String, Point> memberCoordinateMap) {
List<RedisGeoCommands.GeoLocation<Object>> locations = new ArrayList<>();
memberCoordinateMap.forEach((member, point) ->
locations.add(new RedisGeoCommands.GeoLocation<>(member, point)));
return geoOps.add(key, locations);
}

/**
* 获取地理位置
*/
public List<Point> geoPos(String key, Object… members) {
return geoOps.position(key, members);
}

/**
* 计算两个地理位置之间的距离
*/
public Distance geoDist(String key, Object member1, Object member2, Metric metric) {
return geoOps.distance(key, member1, member2, metric);
}

/**
* 获取指定位置半径范围内的地理位置
*/
public GeoResults<RedisGeoCommands.GeoLocation<Object>> geoRadius(String key, double longitude, double latitude,
double distance, Metric metric,
RedisGeoCommands.GeoRadiusCommandArgs args) {
return geoOps.radius(key, new Circle(new Point(longitude, latitude), new Distance(distance, metric)), args);
}

/**
* 获取指定成员半径范围内的地理位置
*/
public GeoResults<RedisGeoCommands.GeoLocation<Object>> geoRadiusByMember(String key, Object member,
double distance, Metric metric,
RedisGeoCommands.GeoRadiusCommandArgs args) {
return geoOps.radius(key, member, new Distance(distance, metric), args);
}

/**
* 删除目标坐标
* @param key GEO键
* @param members 目标ID(可变参数)
* @return 删除成功的数量
*/
public Long geoRem(String key, String… members) {
return geoOps.remove(key, members);
}

// ============================ BitMap ============================

/**
* 设置位图
*/
public Boolean setBit(String key, long offset, boolean value) {
return redisTemplate.opsForValue().setBit(key, offset, value);
}

/**
* 获取位图
*/
public Boolean getBit(String key, long offset) {
return redisTemplate.opsForValue().getBit(key, offset);
}

/**
* 统计位图中值为1的位数
*/
public Long bitCount(String key) {
return redisTemplate.execute((RedisCallback<Long>) connection -> connection.bitCount(key.getBytes()));
}

/**
* 统计位图指定范围内值为1的位数
*/
public Long bitCount(String key, long start, long end) {
return redisTemplate.execute((RedisCallback<Long>) connection ->
connection.bitCount(key.getBytes(), start, end));
}

/**
* 位图操作(AND/OR/XOR/NOT)
*/
public Long bitOp(RedisStringCommands.BitOperation op, String destKey, String… srcKeys) {
return redisTemplate.execute((RedisCallback<Long>) connection ->
connection.bitOp(op, destKey.getBytes(), Arrays.stream(srcKeys).map(String::getBytes).toArray(byte[][]::new)));
}

/**
* 查找位图中第一个等于指定值的位的位置
*
* @param key 键名
* @param bitValue 查找的位值(true表示1,false表示0)
* @return 返回第一个匹配位的位置(从0开始),如果未找到则返回-1
*/
public Long bitPos(String key, boolean bitValue) {
return redisTemplate.execute((RedisCallback<Long>) connection ->
connection.bitPos(key.getBytes(), bitValue));
}

/**
* 查找 Bitmap 中第一个符合条件的位的位置
*
* @param key Redis键
* @param bitValue 查找的位值(true为1,false为0)
* @param start 起始位索引(包含)
* @param end 结束位索引(包含)
* @return 第一个符合条件的位的位置,如果未找到则返回null
*/
public Long bitPos(String key, boolean bitValue, long start, long end) {
return redisTemplate.execute((RedisCallback<Long>) connection -> {
// 将参数转换为 Redis 命令所需的字节数组
byte[][] args = new byte[4][];
args[0] = key.getBytes();
args[1] = String.valueOf(bitValue ? 1 : 0).getBytes();
args[2] = String.valueOf(start).getBytes();
args[3] = String.valueOf(end).getBytes();

// 执行原生 Redis 命令: BITPOS key bit start end
Object result = connection.execute("BITPOS", args);

// 处理返回结果
if (result instanceof Long) {
Long position = (Long) result;
return position == -1 ? null : position;
}
return null;
});
}

// ============================ HyperLogLog ============================

/**
* 添加元素到HyperLogLog
*/
public Long pfAdd(String key, Object… values) {
return hllOps.add(key, values);
}

/**
* 获取HyperLogLog的基数估算值
*/
public Long pfCount(String key) {
return hllOps.size(key);
}

/**
* HyperLogLog 统计基数
* @param keys 一个或多个键
* @return 基数(独立元素数量)
*/
public Long pfCount(String… keys) {
return hllOps.size(keys);
}

/**
* 合并多个HyperLogLog
*/
public Long pfMerge(String destKey, String… sourceKeys) {
return hllOps.union(destKey, sourceKeys);
}
}

一、概述

RedisUtil 是基于 Spring Data Redis 封装的 Redis 操作工具类,专为 Spring 项目设计(标注 @Component 注解)。其核心功能是通过 Spring 提供的 RedisTemplate 统一管理 Redis 连接与序列化,并封装 Redis 全类型数据结构(String、List、Set、Hash、ZSet 等)及通用命令的操作方法,简化 Redis 开发流程。

二、优势

  • Spring 无缝集成:标注 @Component,可通过 @Autowired 依赖注入,无需手动实例化,符合 Spring 依赖注入规范。
  • 全数据结构支持:覆盖 Redis 所有核心数据结构(String、List、Set、Hash、ZSet、Geo、BitMap、HyperLogLog),满足多样化业务需求。
  • 简化操作流程:封装复杂的序列化、连接管理逻辑,提供简洁 API,开发者无需关注底层实现细节。

三、核心组件与初始化

1. 核心属性

工具类通过 RedisTemplate 初始化各类数据结构的操作对象(Operations),核心属性如下:

属性名

类型

作用描述

对应 Redis 数据结构

valueOps

ValueOperations<String, Object>

字符串(String)类型操作对象

String

listOps

ListOperations<String, Object>

列表(List)类型操作对象

List

setOps

SetOperations<String, Object>

集合(Set)类型操作对象

Set

hashOps

HashOperations<String, String, Object>

哈希(Hash)类型操作对象

Hash

zSetOps

ZSetOperations<String, Object>

有序集合(ZSet)类型操作对象

ZSet

geoOps

GeoOperations<String, Object>

地理信息(Geo)类型操作对象

Geo

hllOps

HyperLogLogOperations<String, Object>

基数统计(HyperLogLog)类型操作对象

HyperLogLog

2. 初始化流程

通过构造函数注入 RedisTemplate<String, Object>,并初始化上述操作对象,确保依赖注入的完整性:

public RedisUtil(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
this.valueOps = redisTemplate.opsForValue();
this.listOps = redisTemplate.opsForList();
// 其他操作对象初始化…
}

四、核心方法分类解析

工具类按功能模块划分为 9 大类别,覆盖 Redis 全类型操作:

1. 通用命令

提供 Redis 全局操作方法,适用于所有数据结构:

方法名

功能描述

关键参数说明

delete(String key)

删除指定键

key:目标键名

delete(Collection<String> keys)

批量删除键

keys:待删除键的集合

exists(String key)

判断键是否存在

返回 true 表示存在,false 不存在

expire(String key, long timeout, TimeUnit unit)

设置键过期时间

timeout:超时时间;unit:时间单位(秒/分等)

getExpire(String key)

获取剩余过期时间(秒)

返回剩余秒数,-1 表示永久有效

persist(String key)

移除键的过期时间(设为永久)

2. String 类型操作

针对 Redis 字符串类型的基础操作,支持设置值、过期时间、自增自减等:

核心方法

功能描述

典型场景

set(String key, Object value)

设置键值对

存储用户会话、缓存简单数据

set(String key, Object value, long timeout, TimeUnit unit)

带过期时间的键值对

临时验证码、限时缓存

setIfAbsent(String key, Object value)

键不存在时设置值(原子操作)

分布式锁、防止重复提交

get(String key)

获取键对应的值

读取缓存数据

increment(String key) / decrement(String key)

数值自增/自减

计数器(访问量、订单数)

3. List 类型操作

支持列表的双向插入、弹出、范围查询等操作,适用于消息队列、历史记录等场景:

核心方法

功能描述

特色功能

lPush(String key, Object value) / rPush(…)

从左侧/右侧插入元素

支持批量插入(lPushAll/rPushAll)

lPop(String key) / rPop(…)

从左侧/右侧弹出元素

支持阻塞式弹出(lPop 带超时参数)

lRange(String key, long start, long end)

获取指定范围元素

start/end 为索引(0 开始,-1 表示末尾)

bLPopWithLog(int timeout, String… keys)

带日志的阻塞式左弹出

支持多队列监听,超时自动返回,便于调试

rPopLPush(String sourceKey, String destinationKey)

原子性移动物品

从源队列右弹并左压入目标队列,适用于可靠消息队列

4. Set 类型操作

针对无序不可重复集合的操作,支持交集、并集、差集等运算:

核心方法

功能描述

实用场景

sAdd(String key, Object… values)

向集合添加元素

标签管理、数据去重

sMembers(String key)

获取集合所有元素

查看集合完整数据

sIsMember(String key, Object value)

判断元素是否在集合中

权限校验、数据存在性检查

sIntersect(String key, String otherKey)

求两个集合的交集

共同好友、相似内容推荐

sUnion(String key, Collection<String> otherKeys)

求多个集合的并集

数据合并、全量统计

5. Hash 类型操作

模拟“键-字段-值”三层结构,适用于存储对象属性(如用户信息、商品详情):

核心方法

功能描述

优势特点

hSet(String key, String hashKey, Object value)

设置哈希字段值

单个属性更新

hMSet(String key, Map<String, Object> map)

批量设置哈希字段值

初始化对象多个属性

hGetAll(String key)

获取所有字段和值

完整获取对象信息

hKeys(String key) / hValues(String key)

获取所有字段/值

遍历对象属性或属性值

hIncrementLong(String key, String hashKey, long delta)

字段值自增

计数器(如商品销量、用户积分)

6. ZSet 类型操作

基于分数(score)排序的有序集合,适用于排行榜、优先级队列等场景:

核心方法

功能描述

关键参数

zAdd(String key, Object value, double score)

添加元素及分数

score:排序权重分数

zRange(String key, long start, long end)

按分数升序获取元素

start/end:排名范围

zReverseRange(String key, long start, long end)

按分数降序获取元素

适用于 TopN 排行榜

zScore(String key, Object value)

获取元素分数

查看元素权重值

zIncrementScore(String key, Object value, double delta)

增加元素分数

动态调整排名(如积分增加)

zRangeByScore(String key, double min, double max)

按分数范围获取元素

筛选指定分数区间的元素

7. Geo 地理信息操作

支持地理位置存储、距离计算、范围搜索,适用于“附近的人”、POI 搜索等场景:

核心方法

功能描述

关键参数

geoAdd(String key, double longitude, double latitude, Object member)

添加地理位置

longitude:经度;latitude:纬度

geoPos(String key, Object… members)

获取地理位置坐标

返回经纬度坐标(Point)列表

geoDist(String key, Object m1, Object m2, Metric metric)

计算两点距离

Metric:距离单位(米/千米等)

geoRadius(String key, double lon, double lat, double distance, Metric metric, …)

搜索指定半径内的位置

按中心点和半径搜索周边位置

geoRem(String key, String… members)

删除地理位置

移除无效的地理坐标

8. BitMap 位图操作

通过位运算高效存储二进制数据,适用于签到、活跃用户统计等场景:

核心方法

功能描述

优势

setBit(String key, long offset, boolean value)

设置指定位的值

节省空间(1字节=8位)

getBit(String key, long offset)

获取指定位的值

快速查询单个位状态

bitCount(String key)

统计值为1的位数

统计活跃天数、用户数等

bitOp(RedisStringCommands.BitOperation op, String destKey, String… srcKeys)

位图运算(AND/OR等)

批量计算(如交集统计活跃用户)

bitPos(String key, boolean bitValue)

查找第一个指定值的位位置

快速定位有效数据起始位置

9. HyperLogLog 基数统计

以极小空间实现海量数据的基数估算(如独立访客数 UV):

核心方法

功能描述

适用场景

pfAdd(String key, Object… values)

添加元素到 HyperLogLog

记录访问用户ID、浏览内容ID

pfCount(String key)

获取基数估算值

统计独立访客数、不重复内容数

pfCount(String… keys)

合并多个键的基数统计

跨时间段/渠道的 UV 合并统计

pfMerge(String destKey, String… sourceKeys)

合并多个 HyperLogLog

将多个数据集合并为一个新集合

五、使用示例

在 Spring 服务中通过依赖注入使用 RedisUtil:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class UserService {

@Autowired
private RedisUtil redisUtil;

// 示例1:存储用户信息到 Hash
public void saveUser(Long userId, String username, Integer age) {
String key = "user:" + userId;
redisUtil.hSet(key, "username", username);
redisUtil.hSet(key, "age", age);
redisUtil.expire(key, 86400); // 设置24小时过期
}

// 示例2:使用 List 实现消息队列
public void sendMessage(String queueName, String message) {
redisUtil.rPush(queueName, message); // 右压入队列
}

// 示例3:使用 ZSet 实现排行榜
public void addScore(String rankKey, String userId, double score) {
redisUtil.zAdd(rankKey, userId, score); // 添加用户分数
}

public List<Object> getTop10Rank(String rankKey) {
// 获取分数从高到低的前10名
return redisUtil.zReverseRange(rankKey, 0, 9);
}
}

六、单元测试代码

package com.ao.commonUtil.util.redis;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.geo.Point;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.domain.geo.Metrics;

import java.util.*;

/**
* StringRedisTemplate 测试
*/
@SpringBootTest
public class RedisUtilTest {

@Autowired
private RedisUtil redisUtil;

@Test
// ==================== 通用操作示例 ====================
public void commonOperationsExample() {
String key = "example:common:key1";
String value = "common_value";

System.out.println("===== 通用操作示例 =====");

// 设置键值
redisUtil.set(key, value);
System.out.println("设置键值: " + key + " = " + value);

// 获取键值
Object retrievedValue = redisUtil.get(key);
System.out.println("获取键值: " + key + " = " + retrievedValue);

// 设置过期时间
redisUtil.expire(key, 60);
System.out.println("设置键过期时间: 60秒");

// 检查键是否存在
boolean exists = redisUtil.exists(key);
System.out.println("键是否存在: " + exists);

// 获取剩余过期时间
long ttl = redisUtil.getExpire(key);
System.out.println("键剩余过期时间: " + ttl + "秒");

// 获取键类型
String type = redisUtil.type(key);
System.out.println("键类型: " + type);

// 删除键
redisUtil.delete(key);
System.out.println("删除键: " + key);
}

@Test
// ==================== String 类型示例 ====================
public void stringOperationsExample() {
String key = "example:string:key1";
String value = "string_value";

System.out.println("\\n===== String 操作示例 =====");

// 设置字符串值
redisUtil.set(key, value);
System.out.println("设置字符串: " + key + " = " + value);

// 获取字符串值
Object retrieved = redisUtil.get(key);
System.out.println("获取字符串: " + retrieved);

// 自增操作
String counterKey = "example:string:counter";
long incrResult = redisUtil.increment(counterKey);
System.out.println("自增结果: " + incrResult);

// 自增指定值
long incrByResult = redisUtil.increment(counterKey, 5);
System.out.println("自增5结果: " + incrByResult);

// 自减操作
long decrResult = redisUtil.decrement(counterKey);
System.out.println("自减结果: " + decrResult);

// 自减指定值
long decrByResult = redisUtil.decrement(counterKey, 3);
System.out.println("自减3结果: " + decrByResult);

// 追加字符串
redisUtil.append(key, "_appended");
System.out.println("追加字符串后: " + redisUtil.get(key));

// 获取字符串长度
long size = redisUtil.stringSize(key);
System.out.println("字符串长度: " + size);

// 清理
redisUtil.delete(Arrays.asList(key, counterKey));
}

@Test
// ==================== List 类型示例 ====================
public void listOperationsExample() {
String key = "example:list:key1";

System.out.println("\\n===== List 操作示例 =====");

// 右推入元素
redisUtil.rPush(key, "A");
redisUtil.rPush(key, "B");
redisUtil.rPush(key, "C");
System.out.println("右推入元素: A, B, C");

// 左推入元素
redisUtil.lPush(key, "X");
System.out.println("左推入元素: X");

// 获取列表范围
List<Object> range = redisUtil.lRange(key, 0, -1);
System.out.println("列表内容: " + range);

// 左弹出元素
Object leftPop = redisUtil.lPop(key);
System.out.println("左弹出元素: " + leftPop);

// 右弹出元素
Object rightPop = redisUtil.rPop(key);
System.out.println("右弹出元素: " + rightPop);

// 获取列表大小
long size = redisUtil.lLen(key);
System.out.println("列表大小: " + size);

// 根据索引获取元素
Object element = redisUtil.lIndex(key, 0);
System.out.println("索引0的元素: " + element);

// 设置指定索引的元素
redisUtil.lSet(key, 0, "new_value");
System.out.println("设置索引0的元素为new_value");

// 移除元素
redisUtil.lRemove(key, 1, "B");
System.out.println("移除1个B元素");

// 清理
redisUtil.delete(key);
}

@Test
// ==================== Hash 类型示例 ====================
public void hashOperationsExample() {
String key = "example:hash:user:1001";

System.out.println("\\n===== Hash 操作示例 =====");

// 设置单个字段
redisUtil.hSet(key, "name", "张三");
redisUtil.hSet(key, "age", 30);
redisUtil.hSet(key, "email", "zhangsan@example.com");
System.out.println("设置哈希字段: name, age, email");

// 获取单个字段
Object name = redisUtil.hGet(key, "name");
System.out.println("获取name字段: " + name);

// 获取所有字段
Map<String, Object> userMap = redisUtil.hGetAll(key);
System.out.println("获取所有字段: " + userMap);

// 检查字段是否存在
boolean hasEmail = redisUtil.hHasKey(key, "email");
System.out.println("email字段是否存在: " + hasEmail);

// 批量设置字段
Map<String, Object> newFields = new HashMap<>();
newFields.put("phone", "13800138000");
newFields.put("address", "北京");
redisUtil.hMSet(key, newFields);
System.out.println("批量设置字段: phone, address");

// 删除字段
redisUtil.hDelete(key, "phone");
System.out.println("删除phone字段");

// 字段自增
long newAge = redisUtil.hIncrementLong(key, "age", 1);
System.out.println("age字段自增1后: " + newAge);

// 清理
redisUtil.delete(key);
}

@Test
// ==================== Set 类型示例 ====================
public void setOperationsExample() {
String key = "example:set:key1";

System.out.println("\\n===== Set 操作示例 =====");

// 添加元素
redisUtil.sAdd(key, "A", "B", "C");
System.out.println("添加元素: A, B, C");

// 获取所有元素
Set<Object> members = redisUtil.sMembers(key);
System.out.println("集合成员: " + members);

// 检查元素是否存在
boolean containsB = redisUtil.sIsMember(key, "B");
System.out.println("集合是否包含B: " + containsB);

// 获取集合大小
long size = redisUtil.sCard(key);
System.out.println("集合大小: " + size);

// 随机获取元素
Object random = redisUtil.sRandomMember(key);
System.out.println("随机元素: " + random);

// 移除元素
redisUtil.sRemove(key, "A");
System.out.println("移除元素A");

// 移动元素到另一个集合
String destKey = "example:set:key2";
redisUtil.sMove(key, "B", destKey);
System.out.println("移动元素B到" + destKey);

// 清理
redisUtil.delete(Arrays.asList(key, destKey));
}

@Test
// ==================== ZSet 类型示例 ====================
public void zSetOperationsExample() {
String key = "example:zset:key1";

System.out.println("\\n===== ZSet 操作示例 =====");

// 添加带分数的元素
redisUtil.zAdd(key, "A", 10);
redisUtil.zAdd(key, "B", 20);
redisUtil.zAdd(key, "C", 30);
redisUtil.zAdd(key, "D", 40);
System.out.println("添加带分数元素: A(10), B(20), C(30), D(40)");

// 获取分数范围内的元素
Set<Object> range = redisUtil.zRangeByScore(key, 15, 35);
System.out.println("分数15-35之间的元素: " + range);

// 获取元素排名
Long rank = redisUtil.zRank(key, "B");
System.out.println("元素B的排名: " + rank);

// 获取元素分数
Double score = redisUtil.zScore(key, "C");
System.out.println("元素C的分数: " + score);

// 增加元素分数
Double newScore = redisUtil.zIncrementScore(key, "A", 5);
System.out.println("元素A分数增加5后: " + newScore);

// 获取集合大小
long size = redisUtil.zCard(key);
System.out.println("有序集合大小: " + size);

// 删除元素
redisUtil.zRemove(key, "A");
System.out.println("删除元素A");

// 清理
redisUtil.delete(key);
}

@Test
// ==================== BitMap 类型示例 ====================
public void bitmapOperationsExample() {
String key = "example:bitmap:user:sign:1001";

System.out.println("\\n===== BitMap 操作示例 =====");

// 设置位图
redisUtil.setBit(key, 1, true);
redisUtil.setBit(key, 3, true);
redisUtil.setBit(key, 5, true);
System.out.println("设置位图: 第1,3,5位为1");

// 获取位图
boolean bit1 = redisUtil.getBit(key, 1);
boolean bit2 = redisUtil.getBit(key, 2);
System.out.println("第1位: " + bit1);
System.out.println("第2位: " + bit2);

// 统计位数为1的数量
long count = redisUtil.bitCount(key);
System.out.println("位数为1的总数: " + count);

// 清理
redisUtil.delete(key);
}

@Test
// ==================== Geo 类型示例 ====================
public void geoOperationsExample() {
String key = "example:geo:cities";

System.out.println("\\n===== Geo 操作示例 =====");

// 添加地理位置
redisUtil.geoAdd(key, 116.404, 39.915, "北京");
redisUtil.geoAdd(key, 121.474, 31.230, "上海");
redisUtil.geoAdd(key, 113.264, 23.129, "广州");
System.out.println("添加地理位置: 北京, 上海, 广州");

// 获取地理位置
List<Point> positions = redisUtil.geoPos(key, "北京", "上海");
System.out.println("北京位置: " + positions.get(0));
System.out.println("上海位置: " + positions.get(1));

// 计算距离
Distance distance = redisUtil.geoDist(key, "北京", "上海", Metrics.KILOMETERS);
System.out.println("北京到上海距离: " + distance.getValue() + "公里");

// 获取半径范围内的位置
GeoResults<RedisGeoCommands.GeoLocation<Object>> results = redisUtil.geoRadius(
key, 116.404, 39.915, 1000, Metrics.KILOMETERS,
RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().limit(5));
System.out.println("北京1000公里范围内的城市:");
results.forEach(geoResult -> {
RedisGeoCommands.GeoLocation<Object> location = geoResult.getContent();
System.out.println(location.getName() + " – 距离: " + geoResult.getDistance().getValue() + "公里");
});

// 清理
redisUtil.delete(key);
}

@Test
// ==================== HyperLogLog 类型示例 ====================
public void hyperLogLogOperationsExample() {
String key = "example:hll:uv";

System.out.println("\\n===== HyperLogLog 操作示例 =====");

// 添加元素
redisUtil.pfAdd(key, "user1", "user2", "user3", "user4", "user5");
System.out.println("添加5个用户到HyperLogLog");

// 获取基数估算值
long count = redisUtil.pfCount(key);
System.out.println("独立用户数估算: " + count);

// 添加更多元素
redisUtil.pfAdd(key, "user6", "user7", "user3", "user8");
System.out.println("再添加4个用户(其中1个重复)");

// 获取新的基数估算值
long newCount = redisUtil.pfCount(key);
System.out.println("新的独立用户数估算: " + newCount);

// 合并HyperLogLog
String key2 = "example:hll:uv2";
redisUtil.pfAdd(key2, "user9", "user10", "user3");
String mergedKey = "example:hll:uv_merged";
redisUtil.pfMerge(mergedKey, key, key2);
System.out.println("合并后的独立用户数估算: " + redisUtil.pfCount(mergedKey));

// 清理
redisUtil.delete(Arrays.asList(key, key2, mergedKey));
}
}

赞(0)
未经允许不得转载:171主机测评 » Redis工具类-RedisUtil
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址