• [技术干货] Spring AI学习:工具调用实践(基于和风天气api)
    工具调用总体实现:设计工具类:@Componentpublic class WeatherInquiryTools { @Autowired private WeatherService weatherService; @Tool(description = "根据城市名称查询城市LocationID") public String getLocationId(@ToolParam(description = "城市名称") String cityName){ return weatherService.getLocationId(cityName).getLocation().get(0).getId(); } @Tool(description = "根据城市LocationID查询实时温度") public String getWeather(@ToolParam(description = "城市LocationID") String locationId){ return weatherService.getWeather(locationId).getNow().getTemp(); }}将工具交给ai客户端: 对话测试: 具体实现:实体类:@NoArgsConstructor@AllArgsConstructor@Datapublic class WeatherResponse { @JsonProperty("code") private String code; @JsonProperty("updateTime") private String updateTime; @JsonProperty("fxLink") private String fxLink; @JsonProperty("now") private NowData now; @JsonProperty("refer") private Refer refer;}@Data@AllArgsConstructor@NoArgsConstructorpublic class NowData { // 观测时间 @JsonProperty("obsTime") private String obsTime; // 温度,单位:摄氏度 @JsonProperty("temp") private String temp; // 体感温度,单位:摄氏度 @JsonProperty("feelsLike") private String feelsLike; // 天气图标代码 @JsonProperty("icon") private String icon; // 天气描述 @JsonProperty("text") private String text; // 风向360角度 @JsonProperty("wind360") private String wind360; // 风向 @JsonProperty("windDir") private String windDir; // 风力等级 @JsonProperty("windScale") private String windScale; // 风速,单位:公里/小时 @JsonProperty("windSpeed") private String windSpeed; // 相对湿度,单位:% @JsonProperty("humidity") private String humidity; // 降水量,单位:毫米 @JsonProperty("precip") private String precip; // 大气压强,单位:百帕 @JsonProperty("pressure") private String pressure; // 能见度,单位:公里 @JsonProperty("vis") private String vis; // 云量,单位:% @JsonProperty("cloud") private String cloud; //露点温度,单位:摄氏度 @JsonProperty("dew") private String dew;}@NoArgsConstructor@AllArgsConstructor@Datapublic class LocationResponse { @JsonProperty("code") private String code; @JsonProperty("location") private List<Location> location; @JsonProperty("refer") private Refer refer;}@AllArgsConstructor@NoArgsConstructor@Datapublic class Location { @JsonProperty("name") private String name; @JsonProperty("id") private String id; @JsonProperty("lat") private String lat; @JsonProperty("lon") private String lon; @JsonProperty("adm2") private String adm2; @JsonProperty("adm1") private String adm1; @JsonProperty("country") private String country; @JsonProperty("tz") private String timezone; @JsonProperty("utcOffset") private String utcOffset; @JsonProperty("isDst") private String isDst; @JsonProperty("type") private String type; @JsonProperty("rank") private String rank; @JsonProperty("fxLink") private String fxLink;}@NoArgsConstructor@AllArgsConstructor@Datapublic class Refer { @JsonProperty("sources") private List<String> sources; @JsonProperty("license") private List<String> license;}以上实体类基于 和风天气api 设计。配置类:@Component@Data@ConfigurationProperties(prefix = "frog.weather")public class WeatherProperties { private String apiKey; private String apiHost;}以上配置类基于 和风天气api 设计。序列化与反序列化工具类:@Componentpublic class JasonUtils { private static final ObjectMapper mapper = new ObjectMapper() .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) .setSerializationInclusion(JsonInclude.Include.NON_NULL) .registerModule(new JavaTimeModule()); public static String toJson(Object obj) { try { return mapper.writeValueAsString(obj); } catch (JsonProcessingException e) { throw new RuntimeException("序列化失败", e); } } public static <T> T fromJson(String json, Class<T> clazz) { try { return mapper.readValue(json, clazz); } catch (JsonProcessingException e) { throw new RuntimeException("反序列化失败", e); } }}查询功能具体实现:@Service@Slf4jpublic class WeatherServiceImpl implements WeatherService { @Autowired private WeatherProperties weatherProperties; /** * 根据城市LocationID查询实时天气状况 */ @Override public WeatherResponse getWeather(String locationId) { HttpClient client = HttpClient.newHttpClient(); String apiUrl = weatherProperties.getApiHost() + "/v7/weather/now?location=" + locationId; try { HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(apiUrl)) .header("X-QW-Api-Key", weatherProperties.getApiKey()) .GET() .build(); HttpResponse<byte[]> response = client.send(request, HttpResponse.BodyHandlers.ofByteArray()); byte[] compressedBody = response.body(); // 解压缩响应体 String decompressedBody = GzipDecompressor.decompressToString(compressedBody); log.info("查询天气成功, locationId: {}", locationId); WeatherResponse weatherResponse = JasonUtils.fromJson(decompressedBody, WeatherResponse.class); return weatherResponse; } catch (Exception e) { log.error("查询天气失败, locationId: {}", locationId, e); return null; } } /** * 根据城市名称查询城市LocationID */ @Override public LocationResponse getLocationId(String cityName) { HttpClient client = HttpClient.newHttpClient(); String apiUrl = weatherProperties.getApiHost() + "/geo/v2/city/lookup?location=" + cityName; try { HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(apiUrl)) .header("X-QW-Api-Key", weatherProperties.getApiKey()) .GET() .build(); HttpResponse<byte[]> response = client.send(request, HttpResponse.BodyHandlers.ofByteArray()); byte[] compressedBody = response.body(); // 解压缩响应体 String decompressedBody = GzipDecompressor.decompressToString(compressedBody); log.info("查询城市LocationID成功, cityName: {}", cityName); LocationResponse locationResponse = JasonUtils.fromJson(decompressedBody, LocationResponse.class); return locationResponse; } catch (Exception e) { log.error("查询城市LocationID失败, cityName: {}", cityName, e); return null; } }}目前实现的tool查询功能局限温度查询,但根据工具需求可拓展(service返回了所有查询所得,tool内对数据重新整理可以产生丰富功能)。转载自https://www.cnblogs.com/syf0824/p/19523990
  • [技术干货] Spring AI学习:配置redis向量数据库&RAG实践
    配置redis向量数据库:配置依赖:<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-vector-store-redis</artifactId> </dependency>配置redis数据库属性: RAG实践:使用spring ai提供的环绕增强(advisor)功能实现RAG。配置依赖:<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-advisors-vector-store</artifactId></dependency>增加QuestionAnswerAdvisor(当前1.1.2版本应该是不能直接new了,QuestionAnswerAdvisor属于包级私有,用builder链式编程可以实现创建) 只要配置依赖和属性,spring ai starter会在程序运行初自动生成VectorStore的Bean,直接注入即可使用vectorStore。(我在运行时出现报错:EmbedingModel不可用,于是我手动设置了一个,如下图) 读取文件并写入向量数据库(spring ai提供了ETL接口,通过对应的Reader就能将文件转换为Document): 进行一次对话(数据文本及结果如下):redis中数据:转载自https://www.cnblogs.com/syf0824/p/19518768
  • [技术干货] Spring AI学习:使用WSL2安装Ubuntu&安装redis-8.4.0
     背景:spring ai中提供了RAG功能接口,需要搭配向量数据库,我选择了最新版redis作为外部向量数据库(redis7可以安装stack包来支持向量存储,redis8本身已集成向量存储功能)(redis8.0.0已有windows适配版本,redis8.4.0的windows适配尚未发布)开启WSL2:进入以下界面,点击红框 勾选以下两项并确定 确定后会要求重启(允许即可)重启后进入命令行,先更新wsl(我没有先更新,但是系统直接提醒我了) 更新后输入命令:wsl --set-default-version 2将wsl版本更改为2。安装Ubuntu:最新版本Ubuntu可以直接在电脑自带的微软商店中下载 安装成功后直接运行,会出现命令行 按照提示设置完用户名和密码后就能正常使用。安装redis8.4.0:我使用Ubuntu的apt下载redis先更新apt 然后按照官方文档里的命令下载即可:sudo apt-get install lsb-release curl gpgcurl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpgsudo chmod 644 /usr/share/keyrings/redis-archive-keyring.gpgecho "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/redis.listsudo apt-get updatesudo apt-get install redis可以实用“Another Redis Desktop Manager”,方便查看/管理redis中的数据转载自https://www.cnblogs.com/syf0824/p/19513914
  • [技术干货] Redis 常用命令之基础、进阶与场景化实战案例【转载
    Redis 常用命令全解析:基础、进阶与场景化实战一、基础通用命令这类命令不针对特定数据类型,适用于所有场景,主要用于管理键、查看状态等。命令作用示例KEYS pattern查找符合匹配模式的所有键(生产环境慎用,会阻塞 Redis)KEYS user* → 查找所有以user开头的键EXISTS key判断键是否存在,返回 1(存在)/0(不存在)EXISTS user:100 → 检查键user:100是否存在DEL key [key ...]删除指定键(可批量),返回删除成功的键数量DEL user:100 cart:100 → 删除user:100和cart:100EXPIRE key seconds为键设置过期时间(单位:秒)EXPIRE code:1234 60 → 键code:123460 秒后过期TTL key查看键的剩余过期时间(-1 = 永不过期,-2 = 已过期 / 不存在)TTL code:1234 → 返回剩余秒数PERSIST key移除键的过期时间,使其永久有效PERSIST code:1234 → 取消code:1234的过期时间TYPE key查看键对应值的数据类型TYPE user:100 → 返回hash/string/list等FLUSHDB清空当前数据库的所有键(慎用)FLUSHDB → 清空当前库FLUSHALL清空所有数据库的所有键(生产环境严禁使用)FLUSHALL → 清空全部库二、核心数据类型命令Redis 支持多种数据类型,下面是字符串(String)、哈希(Hash)、列表(List)、集合(Set)、有序集合(ZSet)这 5 种核心类型的常用命令。1. 字符串(String)- 最基础的键值对类型适用于存储简单值(如数字、文本),是 Redis 最常用的类型。1234567891011121314# 设置键值(覆盖原有值)SET name "张三"# 设置键值并指定过期时间(30秒)SETEX code 30 "8866"# 获取值GET name# 数值自增(+1,仅适用于数字值)INCR num# 数值自增指定步长(+5)INCRBY num 5# 数值自减(-1)DECR num# 追加字符串到原有值末尾APPEND name " - 程序员"2. 哈希(Hash)- 适用于存储对象(如用户信息、商品信息)一个 Hash 键可以存储多个字段(field)和值(value),类似 Java 的 Map。1234567891011121314151617181920# 设置Hash的单个字段HSET user:100 name "李四" age 25# 设置Hash的多个字段HMSET user:100 name "李四" age 25 gender "男"# 获取Hash的单个字段值HGET user:100 name# 获取Hash的多个字段值HMGET user:100 name age# 获取Hash的所有字段和值HGETALL user:100# 获取Hash的所有字段名HKEYS user:100# 获取Hash的所有值HVALS user:100# 判断Hash的字段是否存在HEXISTS user:100 age# 删除Hash的指定字段HDEL user:100 gender# 获取Hash的字段数量HLEN user:1003. 列表(List)- 有序、可重复的集合,适用于队列 / 栈场景底层是双向链表,支持从头部 / 尾部操作元素。12345678910111213141516# 从列表尾部添加元素RPUSH list:msg "消息1" "消息2"# 从列表头部添加元素LPUSH list:msg "消息0"# 从列表尾部移除并返回元素RPOP list:msg# 从列表头部移除并返回元素LPOP list:msg# 获取列表指定范围的元素(0=-1表示所有)LRANGE list:msg 0 -1# 获取列表长度LLEN list:msg# 根据索引获取元素LINDEX list:msg 1# 根据索引修改元素LSET list:msg 1 "修改后的消息2"4. 集合(Set)- 无序、不可重复的集合,适用于去重 / 交集场景支持集合间的交、并、差运算。12345678910111213141516# 向集合添加元素SADD set:tags "Java" "Redis" "MySQL"# 获取集合所有元素SMEMBERS set:tags# 判断元素是否在集合中SISMEMBER set:tags "Redis"# 删除集合指定元素SREM set:tags "MySQL"# 获取集合元素数量SCARD set:tags# 求两个集合的交集(共同元素)SINTER set:tags1 set:tags2# 求两个集合的并集(所有元素,去重)SUNION set:tags1 set:tags2# 求两个集合的差集(在set1中但不在set2中的元素)SDIFF set:tags1 set:tags25. 有序集合(ZSet)- 有序、不可重复的集合,适用于排名 / 计分场景每个元素关联一个分数(score),Redis 根据分数排序。123456789101112131415161718# 向有序集合添加元素(score为分数)ZADD zset:rank 95 "张三" 90 "李四" 98 "王五"# 获取有序集合所有元素(带分数,默认升序)ZRANGE zset:rank 0 -1 WITHSCORES# 获取有序集合所有元素(降序)ZREVRANGE zset:rank 0 -1 WITHSCORES# 获取指定元素的分数ZSCORE zset:rank "张三"# 增加指定元素的分数(+3)ZINCRBY zset:rank 3 "张三"# 获取元素的排名(升序,从0开始)ZRANK zset:rank "李四"# 获取元素的排名(降序)ZREVRANK zset:rank "李四"# 删除指定元素ZREM zset:rank "李四"# 获取集合元素数量ZCARD zset:rank三、开发常用命令1、进阶通用命令(开发排查 / 性能优化常用)这类命令补充了基础通用命令的不足,尤其适合生产环境的键遍历、性能监控、连接管理。命令作用示例 / 说明SCAN cursor [MATCH pattern] [COUNT count]非阻塞式遍历键(替代KEYS,生产环境首选),分批返回键,避免阻塞 RedisSCAN 0 MATCH user:* COUNT 100 → 从游标 0 开始,匹配user:*,每次返回 100 个键INFO [section]查看 Redis 运行状态信息(如内存、连接、持久化),排查性能问题INFO memory → 查看内存使用;INFO stats → 查看统计指标CLIENT LIST列出所有连接到 Redis 的客户端信息(IP、状态、耗时)排查长连接 / 慢连接问题,定位异常客户端CLIENT KILL ip:port关闭指定 IP + 端口的客户端连接CLIENT KILL 192.168.1.100:54321 → 断开该客户端连接DBSIZE统计当前数据库的键总数快速了解当前库数据量,比KEYS *高效RENAMENX oldkey newkey重命名键(仅当 newkey 不存在时才成功,避免覆盖)RENAMENX user:old user:new → 安全重命名MOVE key db将键移动到指定数据库(Redis 默认 16 个库,编号 0-15)MOVE user:100 1 → 把user:100移到 1 号库2、事务与分布式锁相关命令(高并发场景核心)Redis 事务保证命令的原子性执行,而SETNX/GETSET/Redlock是实现分布式锁的核心命令。(1) 事务命令Redis 事务通过MULTI(开启)、EXEC(执行)、DISCARD(取消)实现,命令入队后批量执行:1234567891011# 开启事务MULTI# 入队命令(此时不执行,仅记录)SET order:100 status "paid"HSET user:100 order_count 1# 执行事务(所有入队命令原子执行)EXEC# 取消事务(放弃所有入队命令)MULTISET a 1DISCARD  # 事务取消,SET a 1不会执行(2) 分布式锁核心命令(解决多服务并发竞争)12345678910111213# 1. SETNX:设置键(仅当键不存在时成功,返回1=加锁成功,0=已被占用)# 缺点:若加锁客户端宕机,锁无法释放,需配合过期时间SETNX lock:order 1EXPIRE lock:order 30  # 给锁设置30秒过期,避免死锁# 2. SET(推荐):原子性设置值+过期时间(解决SETNX+EXPIRE非原子问题)SET lock:order 1 NX EX 30  # NX=仅当键不存在时设置;EX=过期时间(秒);PX=过期时间(毫秒)# 3. GETSET:获取旧值并设置新值(解锁/锁续期用)# 解锁逻辑示例:先判断值是否是自己的锁,再删除(伪代码)if GET lock:order == "my_lock_value"  DEL lock:order# 4. DEL:释放锁(需配合业务逻辑,避免误删其他客户端的锁)DEL lock:order3、发布订阅命令(异步通知 / 消息广播场景)适用于实时通知(如订单状态变更、消息推送),Redis 作为轻量级消息中间件使用。12345678# 订阅频道(客户端1)SUBSCRIBE channel:order_notify  # 订阅order_notify频道PSUBSCRIBE channel:*            # 订阅所有以channel:开头的频道(通配符)# 发布消息(客户端2)PUBLISH channel:order_notify "订单100已支付"  # 向频道发布消息,订阅端实时接收# 取消订阅UNSUBSCRIBE channel:order_notify  # 取消指定频道订阅PUNSUBSCRIBE channel:*            # 取消通配符订阅4、持久化相关命令(数据备份 / 恢复常用)Redis 持久化分 RDB(快照)和 AOF(日志),以下命令用于手动触发持久化、查看配置:1234567# 手动触发RDB快照(阻塞Redis,生产环境建议低峰期执行)BGSAVE  # 后台异步保存(推荐);SAVE  # 同步保存(阻塞,慎用)# 查看持久化配置CONFIG GET save        # 查看RDB自动保存触发条件(如"900 1"=900秒内至少1个键修改则保存)CONFIG GET appendonly  # 查看AOF是否开启(yes/no)# 手动触发AOF重写(压缩AOF日志,减少文件体积)BGREWRITEAOF  # 后台异步重写(推荐)5、过期时间进阶命令(精准控制键有效期)补充基础过期命令的细节,适配不同时间精度需求:12345678# PEXPIRE:设置过期时间(单位:毫秒,适配高精度场景)PEXPIRE code:1234 60000  # 60秒(60000毫秒)后过期# EXPIREAT:指定过期时间戳(秒)EXPIREAT user:100 1735689600  # 键在2025-01-01 00:00:00过期# PEXPIREAT:指定过期时间戳(毫秒)PEXPIREAT user:100 1735689600000# TTL/PTTL:查看剩余过期时间(PTTL返回毫秒)PTTL code:1234  # 返回剩余毫秒数四、其他常用命令1、批量操作命令(提升读写效率)批量操作能减少网络 IO 次数(Redis 是单线程,网络往返是性能瓶颈),大幅提升效率,是高并发场景的必备命令。(1) String 类型批量操作123456# 批量设置多个键值对(原子操作)MSET name "张三" age 25 email "zhangsan@test.com"# 批量获取多个键的值(返回顺序与参数一致)MGET name age email  # 返回 ["张三", "25", "zhangsan@test.com"]# 批量设置键值+过期时间(Redis 7.0+支持,替代SETEX批量场景)MSETEX key1 60 "val1" key2 120 "val2"(2) Hash 类型批量操作(补充)1234# 批量设置Hash字段(已提过,补充批量获取优化)HMSET user:101 name "王五" phone "13800138000" address "北京"# 批量获取Hash字段(指定多个field,减少网络请求)HMGET user:101 name phone  # 返回 ["王五", "13800138000"](3) 通用批量删除(补充)123456789101112131415161718# 结合SCAN+DEL实现批量删除(生产环境替代KEYS+DEL)# 示例:删除所有以temp:开头的键(伪代码)cursor=0while true; do  # 分批获取键  result=$(redis-cli SCAN $cursor MATCH temp:* COUNT 100)  # 解析游标和键列表  cursor=$(echo $result | awk 'NR==1{print $1}')  keys=$(echo $result | awk 'NR==2{print $0}')  # 批量删除  if [ -n "$keys" ]; then    redis-cli DEL $keys  fi  # 游标为0时结束  if [ $cursor -eq 0 ]; then    break  fidone2、管道(Pipeline)与脚本(Lua)命令(高性能批量执行)适用于需要 “原子性批量执行多个命令” 的场景,比普通批量命令更灵活。(1) 管道(Pipeline)命令管道允许客户端一次性发送多个命令,Redis 批量执行后返回结果,减少网络往返次数:123# 终端执行管道命令示例(echo + redis-cli --pipe)echo -e "SET a 1\nINCR a\nGET a" | redis-cli --pipe# 输出:所有命令执行结果,最终GET a返回2(2) Lua 脚本命令(原子性执行复杂逻辑)Lua 脚本在 Redis 中原子执行,避免多命令竞态问题(如分布式锁续期、库存扣减):123456789101112131415161718# EVAL:执行Lua脚本(核心命令)# 格式:EVAL 脚本 键参数个数 键1 键2 ... 参数1 参数2 ...# 示例:原子性实现“先判断值再修改”(库存扣减,库存>=1才扣减)EVAL "local stock = tonumber(redis.call('GET', KEYS[1]))       if stock and stock >= 1 then        redis.call('DECR', KEYS[1])        return 1  # 扣减成功      else        return 0  # 库存不足      end" 1 stock:1001# SCRIPT LOAD:预加载Lua脚本(避免重复传输脚本内容)SCRIPT LOAD "return redis.call('GET', KEYS[1])"  # 返回脚本SHA1值# EVALSHA:执行预加载的脚本(更高效)EVALSHA 5332031c6b470dc5a0dd9b4bf2030dea6d65de91 1 stock:1001# SCRIPT EXISTS:检查脚本是否已加载SCRIPT EXISTS 5332031c6b470dc5a0dd9b4bf2030dea6d65de91# SCRIPT FLUSH:清空所有预加载的Lua脚本SCRIPT FLUSH3、Geo 地理定位命令(LBS 场景必备)适用于地图相关业务(如附近的人、门店、外卖配送),Redis 支持经纬度存储和距离计算:123456789101112# GEOADD:添加地理位置(经度,纬度,位置名称)GEOADD shop:nearby 116.403874 39.914885 "门店A" 116.410049 39.914575 "门店B"# GEOPOS:获取指定位置的经纬度GEOPOS shop:nearby "门店A"  # 返回 [116.403874, 39.914885]# GEODIST:计算两个位置的距离(单位:m/km/mi/ft)GEODIST shop:nearby "门店A" "门店B" km  # 返回距离(如0.58km)# GEORADIUS:根据经纬度范围查找位置(附近的门店)# 示例:查找经度116.405、纬度39.915,半径1km内的所有门店GEORADIUS shop:nearby 116.405 39.915 1 km WITHDIST WITHCOORD# GEORADIUSBYMEMBER:根据已有位置查找附近位置# 示例:查找门店A周围1km内的所有门店GEORADIUSBYMEMBER shop:nearby "门店A" 1 km WITHDIST4、Bitmaps 位图命令(高效存储布尔值)适用于海量布尔值场景(如用户签到、是否在线、权限开关),1 个键可存储 2^32 个布尔值,内存占用极低:123456789101112# SETBIT:设置指定偏移量的位值(0/1)# 示例:用户1001在第5天签到(偏移量5,值1)SETBIT sign:1001 5 1# GETBIT:获取指定偏移量的位值GETBIT sign:1001 5  # 返回1(已签到),0(未签到)# BITCOUNT:统计指定范围内值为1的位数量# 示例:统计用户1001当月(30天)的签到次数BITCOUNT sign:1001 0 29  # 返回签到天数# BITOP:位图运算(AND/OR/XOR/NOT)# 示例:统计同时签到的用户(用户1001和1002的签到交集)BITOP AND sign:common sign:1001 sign:1002BITCOUNT sign:common  # 返回共同签到天数5、HyperLogLog 基数统计命令(海量数据去重统计)适用于 “统计不重复元素个数” 场景(如 UV 统计、独立访客数),占用内存固定(约 12KB),支持海量数据:12345678# PFADD:添加元素到HyperLogLog集合PFADD uv:20260121 "user1001" "user1002" "user1003"# PFCOUNT:统计集合的基数(不重复元素个数)PFCOUNT uv:20260121  # 返回3# PFMERGE:合并多个HyperLogLog集合# 示例:合并1月21日和1月22日的UV数据PFMERGE uv:202601 uv:20260121 uv:20260122PFCOUNT uv:202601  # 返回两天的总独立访客数6、连接与认证命令(运维 / 开发调试)12345678910111213# AUTH:认证密码(Redis设置密码后,连接时需认证)AUTH yourpassword  # 认证成功返回OK,失败返回错误# SELECT:切换数据库(Redis默认16个库,0-15)SELECT 1  # 切换到1号库# PING:测试Redis连接是否正常PING  # 返回PONG表示连接正常# QUIT:关闭客户端连接QUIT# CONFIG SET/GET:动态修改/查看配置(无需重启Redis)# 示例:修改最大内存为10GBCONFIG SET maxmemory 10gb# 示例:查看AOF开关状态CONFIG GET appendonly总结1.通用命令:重点掌握EXISTS(判断键存在)、DEL(删除键)、EXPIRE(设置过期)、TYPE(查看类型),是所有操作的基础。2.核心类型命令:String:SET/GET(基础读写)、INCR/INCRBY(数值自增);Hash:HSET/HGET/HGETALL(对象读写);List:LPUSH/RPUSH/LPOP/RPOP(队列操作);Set:SADD/SMEMBERS/SINTER(去重 / 集合运算);ZSet:ZADD/ZRANGE/ZREVRANGE(排名场景)。3.生产环境中避免使用KEYS命令(会阻塞 Redis),如需遍历键,优先使用SCAN命令(分批遍历,非阻塞)。4.进阶通用命令:优先用SCAN替代KEYS,INFO排查性能问题,CLIENT LIST定位连接异常;5.分布式锁:核心用SET key value NX EX seconds(原子加锁 + 过期),避免SETNX+EXPIRE非原子问题;6.业务场景命令:发布订阅(PUBLISH/SUBSCRIBE)适用于异步通知,事务(MULTI/EXEC)保证命令原子执行,BGSAVE/BGREWRITEAOF用于持久化备份;7.过期时间:毫秒级需求用PEXPIRE/PEXPIREAT,时间戳过期用EXPIREAT,精准控制键的有效期。8.批量 / 高性能命令:MSET/MGET(批量读写)、Pipeline(减少网络 IO)、Lua 脚本(原子执行复杂逻辑)是提升 Redis 性能的核心,高并发场景必用;9.场景化命令:Geo 命令(LBS 场景:附近的人 / 门店);Bitmaps(海量布尔值:签到 / 在线状态);HyperLogLog(海量去重统计:UV / 独立访客);10.运维调试命令:AUTH/SELECT/PING(连接认证)、CONFIG SET/GET(动态配置)、SCAN+DEL(安全批量删除)是开发 / 运维排查问题的常用命令;11.选择原则:根据业务场景匹配命令 —— 性能优先选批量 / 管道,地理场景选 Geo,布尔统计选 Bitmaps,去重统计选 HyperLogLog。
  • [技术干货] Caffeine结合Redis空值缓存实现多级缓存【转载】
    一、多级缓存架构设计:为什么要 “三级联动”?传统缓存方案要么依赖单一本地缓存(无法分布式共享),要么仅用 Redis(网络 IO 开销影响性能),而三级缓存架构通过 “本地缓存 + 分布式缓存 + 数据库” 的层级设计,实现了 “速度” 与 “一致性” 的平衡:第一级:Caffeine 本地缓存基于 Java 内存的高性能缓存,读写延迟低至纳秒级,专门存储热点数据(如高频访问的商品信息、配置参数),避免重复查询 Redis 和数据库,提升核心接口响应速度。第二级:Redis 分布式缓存分布式环境下的共享缓存,解决本地缓存数据不一致问题,同时承担 “中间缓冲” 角色,减少数据库直接访问压力。第三级:数据库数据最终存储源,仅在缓存未命中时触发查询,保证数据可靠性。核心优势性能极致:本地缓存命中率超 90%,Redis 缓存命中率超 95%,99% 请求响应时间 < 10ms;高可用:故障隔离设计,某一级缓存失效不影响整体服务(如 Redis 宕机时,本地缓存可临时兜底);资源优化:减少 Redis 网络 IO 和数据库查询压力,降低集群部署成本;多层防护:从架构层面规避缓存穿透、雪崩、击穿三大经典问题。二、核心问题解决方案:三大缓存难题逐个击破1. 缓存穿透:拦截无效查询问题:恶意请求查询不存在的数据(如 ID=-1 的商品),导致缓存失效后直接穿透到数据库,引发性能问题。解决方案:空值缓存 + 布隆过滤器双重防护空值缓存:数据库查询无结果时,在 Redis 和 Caffeine 中缓存空值(设置短期过期时间,如 5 分钟),避免重复穿透;布隆过滤器:预先将数据库中存在的主键(如商品 ID、用户 ID)存入布隆过滤器,请求先经过过滤器校验,无效 ID 直接拦截,不进入缓存和数据库。2. 缓存雪崩:避免集中失效问题:大量缓存数据在同一时间过期,或 Redis 集群宕机,导致所有请求瞬间涌向数据库,引发数据库雪崩。解决方案:随机过期时间 + 优雅降级随机 TTL:缓存数据时,在基础过期时间(如 30 分钟)上增加随机值(5-10 分钟),使缓存过期时间分散,避免集中失效;优雅降级:Redis 宕机时,启用本地缓存兜底,同时触发告警机制,保障服务核心功能可用。3. 缓存击穿:保护热点数据问题:热点数据(如秒杀商品)缓存过期瞬间,大量并发请求穿透到数据库,导致数据库压力骤增。解决方案:热点数据预热 + 分布式锁数据预热:系统启动时,主动将热点数据加载到 Caffeine 和 Redis 中,避免缓存冷启动;分布式锁:缓存过期后,通过 Redis 分布式锁(如 Redisson)控制,仅允许一个线程查询数据库并更新缓存,其他线程等待缓存更新后再查询,防止并发穿透。三、实战落地:SpringBoot 整合三级缓存1. 依赖配置首先引入核心依赖(Maven 示例),包含 SpringBoot 缓存 starter、Caffeine、Redis、Redisson(分布式锁):12345678910111213141516171819202122<!-- SpringBoot缓存核心依赖 --><dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-cache</artifactId></dependency><!-- Caffeine本地缓存 --><dependency>    <groupId>com.github.ben-manes.caffeine</groupId>    <artifactId>caffeine</artifactId>    <version>3.1.8</version></dependency><!-- Redis依赖 --><dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-data-redis</artifactId></dependency><!-- Redisson分布式锁 --><dependency>    <groupId>org.redisson</groupId>    <artifactId>redisson-spring-boot-starter</artifactId>    <version>3.23.3</version></dependency>2. 核心配置文件(application.yml)配置 Caffeine 缓存参数、Redis 连接信息、分布式锁等:123456789101112131415161718192021222324252627282930spring:  # Redis配置  redis:    host: 127.0.0.1    port: 6379    password: 123456    lettuce:      pool:        max-active: 8        max-idle: 8        min-idle: 2        max-wait: 1000ms    timeout: 3000ms  # 缓存配置  cache:    type: caffeine    caffeine:      # 初始容量、最大容量、过期时间(写入后30分钟过期)      initial-capacity: 100      maximum-size: 1000      expire-after-write: 30m # 自定义缓存配置cache:  # 空值缓存过期时间(5分钟)  null-value-expire: 5m  # 热点数据预热key前缀  hot-data-prefix: "hot:"  # 分布式锁前缀  lock-prefix: "cache:lock:"3. 核心代码实现(1)缓存配置类:初始化 Caffeine 和 Redis 缓存123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051import com.github.benmanes.caffeine.cache.Caffeine;import org.springframework.cache.CacheManager;import org.springframework.cache.caffeine.CaffeineCacheManager;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.data.redis.cache.RedisCacheConfiguration;import org.springframework.data.redis.cache.RedisCacheManager;import org.springframework.data.redis.connection.RedisConnectionFactory;import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;import org.springframework.data.redis.serializer.RedisSerializationContext;import org.springframework.data.redis.serializer.StringRedisSerializer;import java.time.Duration;import java.util.HashMap;import java.util.Map; @Configurationpublic class CacheConfig {     // Caffeine缓存管理器(本地缓存)    @Bean    public CacheManager caffeineCacheManager() {        CaffeineCacheManager cacheManager = new CaffeineCacheManager();        // 配置Caffeine缓存参数:初始容量100,最大容量1000,写入后30分钟过期        cacheManager.setCaffeine(Caffeine.newBuilder()                .initialCapacity(100)                .maximumSize(1000)                .expireAfterWrite(Duration.ofMinutes(30)));        return cacheManager;    }     // Redis缓存管理器(分布式缓存)    @Bean    public RedisCacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {        // 序列化配置(避免Redis存储乱码)        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()                .entryTtl(Duration.ofMinutes(30)) // 默认过期时间30分钟                .serializeKeysWith(RedisSerializationContext.SerializationPair                        .fromSerializer(new StringRedisSerializer()))                .serializeValuesWith(RedisSerializationContext.SerializationPair                        .fromSerializer(new GenericJackson2JsonRedisSerializer()));         // 自定义不同缓存的过期时间(如空值缓存5分钟)        Map<String, RedisCacheConfiguration> cacheConfigs = new HashMap<>();        cacheConfigs.put("nullValueCache", config.entryTtl(Duration.ofMinutes(5)));         return RedisCacheManager.builder(connectionFactory)                .cacheDefaults(config)                .withInitialCacheConfigurations(cacheConfigs)                .build();    }}(2)缓存工具类:封装三级缓存查询逻辑核心逻辑:先查 Caffeine→再查 Redis→最后查数据库,同时处理空值缓存、分布式锁、缓存更新:1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192import org.redisson.api.RLock;import org.redisson.api.RedissonClient;import org.springframework.cache.Cache;import org.springframework.cache.CacheManager;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.stereotype.Component;import javax.annotation.Resource;import java.util.concurrent.TimeUnit; @Componentpublic class CacheUtil {     @Resource    private CacheManager caffeineCacheManager;    @Resource    private RedisCacheManager redisCacheManager;    @Resource    private RedisTemplate<String, Object> redisTemplate;    @Resource    private RedissonClient redissonClient;    @Resource    private BloomFilterUtil bloomFilterUtil; // 布隆过滤器工具类     // 缓存查询核心方法:key-缓存键,clazz-返回类型,dbLoader-数据库查询逻辑    public <T> T getCache(String key, Class<T> clazz, DataLoader<T> dbLoader) {        // 1. 布隆过滤器校验:无效key直接返回null        if (!bloomFilterUtil.contains(key)) {            return null;        }         // 2. 查询Caffeine本地缓存        Cache caffeineCache = caffeineCacheManager.getCache("localCache");        T localValue = caffeineCache.get(key, clazz);        if (localValue != null) {            return localValue;        }         // 3. 查询Redis分布式缓存        Cache redisCache = redisCacheManager.getCache("redisCache");        T redisValue = redisCache.get(key, clazz);        if (redisValue != null) {            // Redis命中,同步到本地缓存            caffeineCache.put(key, redisValue);            return redisValue;        }         // 4. 缓存未命中,分布式锁控制数据库查询        RLock lock = redissonClient.getLock("cache:lock:" + key);        try {            // 尝试获取锁,最多等待3秒,持有锁10秒            if (lock.tryLock(3, 10, TimeUnit.SECONDS)) {                // 再次查询Redis(防止其他线程已更新缓存)                redisValue = redisCache.get(key, clazz);                if (redisValue != null) {                    caffeineCache.put(key, redisValue);                    return redisValue;                }                 // 5. 查询数据库                T dbValue = dbLoader.load();                if (dbValue != null) {                    // 数据库有结果,更新各级缓存                    redisCache.put(key, dbValue);                    caffeineCache.put(key, dbValue);                } else {                    // 数据库无结果,缓存空值(5分钟过期)                    Cache nullValueCache = redisCacheManager.getCache("nullValueCache");                    nullValueCache.put(key, null);                    caffeineCache.put(key, null);                }                return dbValue;            } else {                // 获取锁失败,返回默认值或抛出异常                throw new RuntimeException("缓存更新繁忙,请稍后重试");            }        } catch (InterruptedException e) {            Thread.currentThread().interrupt();            return null;        } finally {            // 释放锁            if (lock.isHeldByCurrentThread()) {                lock.unlock();            }        }    }     // 数据加载函数式接口(封装数据库查询逻辑)    @FunctionalInterface    public interface DataLoader<T> {        T load();    }}(3)缓存更新与清除:保障数据一致性当数据库数据发生变更(新增、修改、删除)时,需同步清除各级缓存,避免数据不一致:123456789101112// 缓存清除方法(用于数据库更新后)public void clearCache(String key) {    // 1. 清除本地缓存    Cache caffeineCache = caffeineCacheManager.getCache("localCache");    caffeineCache.evict(key);    // 2. 清除Redis缓存    Cache redisCache = redisCacheManager.getCache("redisCache");    redisCache.evict(key);    // 3. 清除空值缓存    Cache nullValueCache = redisCacheManager.getCache("nullValueCache");    nullValueCache.evict(key);}(4)热点数据预热:系统启动时加载通过CommandLineRunner实现系统启动时预热热点数据,避免缓存冷启动:1234567891011121314151617181920212223242526import org.springframework.boot.CommandLineRunner;import org.springframework.stereotype.Component;import javax.annotation.Resource;import java.util.List; @Componentpublic class HotDataPreloader implements CommandLineRunner {     @Resource    private CacheUtil cacheUtil;    @Resource    private ProductMapper productMapper; // 数据库DAO层     @Override    public void run(String... args) throws Exception {        // 加载热点商品数据(如销量前100的商品)        List<Product> hotProducts = productMapper.selectHotProducts(100);        for (Product product : hotProducts) {            String key = "product:" + product.getId();            // 存入本地缓存和Redis            cacheUtil.caffeineCacheManager.getCache("localCache").put(key, product);            cacheUtil.redisCacheManager.getCache("redisCache").put(key, product);        }        System.out.println("热点数据预热完成,共加载" + hotProducts.size() + "条数据");    }}四、优化与监控:让缓存体系更稳定1. 配置优化建议Caffeine 参数:初始容量设为预期热点数据量的 80%,最大容量避免超过 JVM 内存的 30%(防止 OOM);Redis 优化:开启持久化(AOF+RDB),配置主从复制,避免单点故障;调整连接池参数适配并发量;过期时间:根据数据更新频率调整,高频更新数据(如库存)过期时间设为 5-10 分钟,低频数据设为 1-2 小时。2. 监控与告警缓存命中率:通过 Spring Boot Actuator 暴露缓存指标,监控 Caffeine 和 Redis 命中率(目标:均≥90%);响应时间:统计接口缓存命中 / 未命中的响应时间,超过阈值(如 50ms)触发告警;异常监控:监控 Redis 连接异常、分布式锁获取失败等情况,及时排查问题。3. 注意事项数据一致性:缓存清除需与数据库事务同步(建议用事务提交后异步清除,避免阻塞业务);内存管理:Caffeine 缓存避免存储大对象,定期清理过期数据;Redis 启用内存淘汰策略(如 LRU);敏感数据:缓存中不存储明文敏感数据(如密码、手机号),需加密后存储;降级策略:Redis 集群故障时,关闭 Redis 缓存读取,仅用本地缓存 + 数据库兜底,保障核心功能可用。五、总结SpringBoot+Caffeine+Redis + 空值缓存的三级缓存方案,通过 “本地缓存提效、分布式缓存保一致、空值缓存防穿透” 的设计,完美解决了高并发场景下的缓存核心难题。该方案不仅能将接口响应时间压缩至毫秒级,还能大幅降低数据库压力,同时具备故障隔离、优雅降级的高可用特性,适用于电商、支付、社交等各类高并发系统。实际落地时,可根据业务场景灵活调整缓存参数(如过期时间、最大容量)和预热策略,结合监控工具持续优化,让缓存体系真正成为系统的 “性能加速器”。
  • [技术干货] 12月技术干货合集分享来啦~
    1、git删除分支实现步骤【转载】cid:link_62、git branch如何delete方式【转载】cid:link_73、 input的accept属性让文件上传安全高效【转载】cid:link_04、HTML5的<input>标签的`type`属性值详解和代码示例【转载】cid:link_15、 python爬虫脚本HTTP 403 Forbidden错误怎么办?【转载】cid:link_26、Python实现将.py代码转换为带语法高亮的Word和PDF【转载】cid:link_87、Python多进程中避免死锁问题的六种策略【转载】cid:link_98、 Python实现将PDF转DOCX的超简单教程(附源码)【转载】cid:link_39、Python基于OpenPyxl实现Excel转PDF并精准控制分页【转载】cid:link_1010、Python获取Docker容器实时资源占用(CPU、内存、IO等)5种实现方式【转载】cid:link_1111、 Python flash URL访问参数配置【转载】cid:link_1212、 Python利用PyMobileDevice3控制iOS设备的完整教程【转载】cid:link_413、 Python基本语法总结大全(含java、js对比)【转载】cid:link_514、 Python自动化提取多个Word文档的文本【转载】cid:link_1315、mybatis-plus分表实现案例(附示例代码)【转载】https://bbs.huaweicloud.com/forum/thread-02126200655519113076-1-1.html
  • [技术干货] Redis 发布订阅(Pub/Sub)技术详解
    在现代分布式系统中,消息通信是实现服务解耦、异步处理和事件驱动架构的核心。Redis 不仅是一个高性能的键值存储系统,还内置了轻量级的发布/订阅(Publish/Subscribe) 模型,为开发者提供了一种简单高效的消息传递机制。一、什么是发布/订阅模式?发布/订阅(Pub/Sub)是一种消息通信模式,其核心思想是:发布者(Publisher):不直接将消息发送给特定的接收者,而是将消息“发布”到一个“频道”。订阅者(Subscriber):事先“订阅”感兴趣的频道,一旦有消息发布到该频道,就会收到通知。解耦:发布者和订阅者之间无需知道对方的存在,实现松耦合。这种模式非常适合用于广播通知、事件监听、日志处理等场景。二、Redis 发布/订阅的基本概念Redis 的 Pub/Sub 模型包含以下核心元素:频道(Channel):消息的逻辑通道,订阅者通过频道接收消息。模式(Pattern):支持通配符订阅多个频道,如 news.*。消息(Message):发布者发送的数据内容,通常为字符串。三、核心命令与使用示例1. 订阅频道:SUBSCRIBE订阅一个或多个频道。# 订阅名为 "news.sports" 的频道SUBSCRIBE news.sports执行后,客户端进入“订阅状态”,只能接收消息或执行少数管理命令(如 UNSUBSCRIBE)。返回示例:Reading messages... (press Ctrl-C to quit)1) "subscribe"2) "news.sports"3) (integer) 1表示已成功订阅第1个频道。2. 发布消息:PUBLISH向指定频道发布消息。# 向 "news.sports" 频道发布一条消息PUBLISH news.sports "C罗再进一球!"返回值: 接收到该消息的订阅者数量。3. 模式订阅:PSUBSCRIBE使用通配符订阅多个频道。# 订阅所有以 "news." 开头的频道PSUBSCRIBE news.*支持的通配符:*:匹配任意数量的字符(除.外)。?:匹配单个字符。4. 取消订阅:UNSUBSCRIBE 和 PUNSUBSCRIBE# 取消所有或指定频道的订阅UNSUBSCRIBE news.sports# 取消模式订阅PUNSUBSCRIBE news.*5. 查看订阅状态:PUBSUB用于查询当前的订阅情况。# 查看活跃频道PUBSUB CHANNELS# 查看订阅了某个模式的客户端数量PUBSUB NUMSUB news.sports# 查看模式订阅数量PUBSUB NUMPAT四、工作流程示例假设我们构建一个简单的新闻推送系统:步骤1:启动两个订阅者终端1(订阅体育新闻):redis-cliSUBSCRIBE news.sports终端2(订阅财经新闻):redis-cliSUBSCRIBE news.finance步骤2:发布消息终端3(发布者):redis-cliPUBLISH news.sports "湖人队夺得总冠军!"PUBLISH news.finance "美股三大指数集体上涨"结果:终端1 收到:"湖人队夺得总冠军!"终端2 收到:"美股三大指数集体上涨"五、使用模式订阅实现广播使用 PSUBSCRIBE 可以让一个订阅者接收多个频道的消息。# 订阅所有新闻类频道PSUBSCRIBE news.*# 发布消息PUBLISH news.tech "AI技术取得新突破"# 订阅者将收到:1) "pmessage"2) "news.*" # 匹配的模式3) "news.tech" # 实际频道4) "AI技术取得新突破" # 消息内容六、应用场景1. 实时通知系统用户登录提醒系统告警通知订单状态变更推送2. 日志聚合与监控多个服务将日志发布到特定频道,由统一的监控服务订阅处理。3. 聊天室/即时通讯每个聊天室对应一个频道,用户加入即订阅,发言即发布。4. 事件驱动架构服务A完成任务后发布“任务完成”事件,服务B订阅并触发后续处理。5. 配置热更新配置中心修改配置后,通过频道通知所有相关服务重新加载。七、优缺点分析 优点简单易用:无需额外消息中间件(如Kafka、RabbitMQ)。低延迟:基于内存,消息传递极快。解耦:发布者与订阅者完全独立。支持模式匹配:灵活的订阅机制。缺点无持久化:如果订阅者离线,会丢失消息(Redis 不保存历史消息)。无确认机制:无法保证消息被成功处理。不支持消息回溯:只能接收订阅之后的消息。不适合高吞吐场景:相比专业消息队列,功能较为基础。⚠️ 注意:如果需要消息持久化、ACK确认、重试机制等高级功能,建议使用 Redis Streams(Redis 5.0+)或专业消息队列。八、编程语言示例(Python)使用 redis-py 库实现发布订阅:import redisimport threading# 连接Redisr = redis.Redis(host='localhost', port=6379, db=0)# 订阅者线程def subscriber(): pubsub = r.pubsub() pubsub.subscribe('news.sports') print("等待消息...") for message in pubsub.listen(): if message['type'] == 'message': print(f"收到消息: {message['data'].decode('utf-8')}")# 启动订阅者thread = threading.Thread(target=subscriber)thread.start()# 发布消息r.publish('news.sports', '勇士队逆转取胜!')thread.join()现在就动手尝试一个简单的发布订阅示例,体验 Redis 的实时通信魅力吧!
  • [技术干货] Redis基础使用入门指南
    Redis(Remote Dictionary Server)是一个开源的、基于内存的键值对存储数据库,以其高性能、丰富的数据结构和灵活的应用场景而广受欢迎。它不仅可以用作缓存系统,还能作为数据库、消息队列等多种用途。本文将带你快速入门Redis的基础使用。一、Redis简介Redis由Salvatore Sanfilippo开发,采用ANSI C语言编写,支持多种操作系统(如Linux、macOS、Windows等)。其核心特点包括:基于内存存储:读写速度极快,适合高并发场景。持久化支持:支持RDB(快照)和AOF(日志)两种持久化机制,确保数据安全。丰富的数据类型:支持字符串(String)、哈希(Hash)、列表(List)、集合(Set)、有序集合(Sorted Set)等。原子性操作:所有操作都是原子的,保证线程安全。主从复制与高可用:支持主从架构、哨兵模式和集群部署。二、安装与启动1. 安装Redis(以Linux为例)# Ubuntu/Debiansudo apt updatesudo apt install redis-server# CentOS/RHELsudo yum install epel-releasesudo yum install redis2. 启动Redis服务# 启动服务sudo systemctl start redis-server# 设置开机自启sudo systemctl enable redis-server# 检查状态sudo systemctl status redis-server3. 连接Redis# 使用Redis自带的命令行客户端redis-cli连接成功后,可输入 ping 测试:127.0.0.1:6379> pingPONG三、基本数据类型与操作1. 字符串(String)最简单的键值对,值可以是字符串或数字。# 设置键值SET name "Alice"# 获取值GET name# 输出: "Alice"# 自增INCR counter # 若counter不存在,则设为0再+1INCRBY counter 5# 设置过期时间(秒)EXPIRE name 602. 哈希(Hash)适合存储对象,如用户信息。# 设置哈希字段HSET user:1001 name "Bob" age 25 email "bob@example.com"# 获取单个字段HGET user:1001 name# 输出: "Bob"# 获取所有字段HGETALL user:10013. 列表(List)有序的字符串列表,支持在头部或尾部插入。# 向列表左侧插入LPUSH tasks "task1" "task2"# 向右侧插入RPUSH tasks "task3"# 查看列表内容LRANGE tasks 0 -1# 输出: ["task2", "task1", "task3"]# 弹出左侧元素LPOP tasks4. 集合(Set)无序且唯一的字符串集合。# 添加元素SADD tags "redis" "database" "cache"# 查看所有元素SMEMBERS tags# 判断是否包含SISMEMBER tags "redis"# 输出: 1(存在)5. 有序集合(Sorted Set)带权重的集合,按分数排序。# 添加成员(分数在前)ZADD leaderboard 100 "Alice" 90 "Bob" 85 "Charlie"# 获取排名(从高到低)ZREVRANGE leaderboard 0 -1 WITHSCORES# 输出: Alice, 100, Bob, 90, Charlie, 85
  • [技术干货] 【浅谈】以 Redis Cluster 为例,它是如何在 CAP 中做权衡的?其数据分片和故障转移机制分别体现了对哪些特性的优先保障?
    核心结论:Redis Cluster 的 CAP 权衡Redis Cluster 优先保障 CP,即在网络分区(Partition)发生时,它会优先保障一致性(C) 和分区容错性(P),而牺牲可用性(A)。这意味着,当发生脑裂(网络分区)时,Redis Cluster 会确保在多数派(Master)主节点所在的分区继续提供服务,而少数派分区中的节点会拒绝写入(甚至部分读取),从而避免数据不一致。这期间,连接到少数派分区的客户端可能会收到错误,从而体验到服务不可用。1. 数据分片机制与 CAP 保障Redis Cluster 采用分片(Sharding) 的方式来管理数据,具体是通过哈希槽(Hash Slot)实现的。机制:整个集群有 16384 个哈希槽。每个键通过 CRC16 校验后,对 16384 取模,被分配到一个唯一的槽中。集群将这些槽分配给各个主节点(Master)。每个主节点负责一部分槽位。客户端可以请求任意节点,如果该键不在当前节点,节点会返回 MOVED 错误并告知正确节点,由客户端进行重定向。体现的 CAP 特性:一致性 (C):数据分片机制是保障强一致性的基础。它通过将数据分散,使得每个主节点只需要维护自己负责的那部分数据的一致性。在数据读写时,操作被限定在特定的主节点及其副本上,这为实现强一致性协议(如 Redis Cluster 的异步复制)创造了条件。分区容错性 (P):分片本身就是分区容错性的体现。数据被分布到多个节点上,单个节点或部分节点的故障不会导致整个集群数据的完全丢失。系统被设计为即使在部分节点不可用时,其他节点仍然可以继续服务。可用性 (A):分片本身提升了整体的可用性,因为一个分片的故障不影响其他分片。但是,在 CAP 的语境下,当发生网络分区时,分片机制会与故障转移机制协同工作,最终做出牺牲部分分片可用性以保障一致性的选择。小结:数据分片机制主要服务于 P(将数据分散以容忍部分故障)和 C(为局部一致性管理打下基础)。2. 故障转移机制与 CAP 保障故障转移是 Redis Cluster 在面临节点故障或网络分区时,维持 CP 特性的核心。机制:心跳与故障探测:所有节点通过 Gossip 协议互相通信。每个主节点会定期向其他主节点发送 PING 消息。如果一个主节点在超过 cluster-node-timeout 时间内未收到另一个主节点的 PONG 回复,它会将其标记为“疑似下线”(PFAIL)。如果集群中大多数主节点都认为某个主节点下线,则将其标记为“已下线”(FAIL),并广播这一消息。从节点晋升:当一个主节点被确认为 FAIL 状态后,其下的一个从节点会发起竞选。这个从节点必须能够连接到集群中的大多数主节点,才能获得投票并晋升为新的主节点。配置纪元(Configuration Epoch):这是一个单调递增的版本号,用于在出现多个冲突的决策(比如脑裂后恢复时出现两个主节点)时,判定哪个决策是最新的,从而解决冲突,保证集群最终只有一个配置胜出。体现的 CAP 特性:一致性 (C):这是最关键的部分。故障转移机制通过 “大多数(Majority)”原则 来保障一致性。一个从节点必须得到大多数主节点的同意才能成为新的主节点。这意味着,在网络分区中,只有拥有大多数主节点的那个分区(多数派分区)才能成功完成故障转移。少数派分区中的节点,因为无法连接到大多数主节点,其从节点无法成功晋升。因此,少数派分区中的原主节点(即使它还在运行)会被集群其他部分视为已下线,它自己也会因为无法完成写入确认而进入错误状态。这就防止了在少数派分区中出现“脏写”,从而避免了脑裂导致的数据不一致。分区容错性 (P):整个故障转移流程就是为了应对节点故障(这是分区的一种极端形式)而设计的。它允许集群在失去部分节点后,通过副本晋升来恢复完整的服务能力,体现了对分区情况的容错。可用性 (A):在这里,可用性被明确地牺牲了。在故障转移期间,原主节点和其从节点负责的哈希槽会有一小段时间不可用(从节点竞选和晋升的时间)。更重要的是,在网络分区发生时,少数派分区中的所有主节点都会变得不可用。因为它们无法与大多数节点取得联系,为了保证不产生不一致的数据,它们会拒绝所有的写操作和(可能配置为强一致性的)读操作。对于连接到这些节点的客户端来说,服务就是“不可用”的。小结:故障转移机制是 Redis Cluster 选择 CP 的集中体现。它通过依赖“大多数”原则,在发生分区时,宁可让少数派分区的服务停止,也绝不冒险接受可能导致数据不一致的写入。总结与对比  特性/机制数据分片 (Sharding)故障转移 (Failover)综合 CAP 权衡一致性 (C)为局部一致性管理打下基础优先保障:通过“大多数”原则防止脑裂和不一致优先保障 (CP)可用性 (A)提升整体可用性(故障隔离)主动牺牲:故障转移期间及少数派分区服务不可用在网络分区时被牺牲分区容错性 (P)核心设计目标:数据分布以容忍部分故障核心设计目标:在节点故障/分区后自动恢复必须保障 (CP)与 AP 系统(如 Cassandra)的对比:Redis Cluster (CP): 当网络断开,导致主节点和它的从节点被隔离在少数派分区时,这个主节点会停止服务。你的应用会收到错误。数据是安全的,不会出现冲突。Cassandra (AP): 在同样的情况下,被隔离的节点仍然会接受写入。当网络恢复时,Cassandra 会使用诸如“最后写入获胜”等机制来解决冲突,但这可能导致数据丢失。它优先保证服务始终可用,但可能牺牲强一致性。因此,在选择 Redis Cluster 时,你需要明确你的业务场景:是否能接受在网络抖动或节点故障时,部分数据的短暂不可用,以换取数据的强一致性? 如果能,那么 Redis Cluster 的 CP 模型是合适的。如果不能,需要追求高可用,并且可以接受最终一致性,那么可能需要考虑其他方案。
  • [问题求助] Redisson里面的锁是怎么来防止误删的?
    Redisson里面的锁是怎么来防止误删的?
  • [问题求助] Redis中的hash和Java中的HashMap有啥区别
    Redis中的hash和Java中的HashMap有啥区别
  • [问题求助] Redis的ZipList、SkipList和ListPack之间有什么区别?
    Redis的ZipList、SkipList和ListPack之间有什么区别?
  • [问题求助] Redis中的ListPack是如何解决级联更新问题的?
    Redis中的ListPack是如何解决级联更新问题的?
  • [问题求助] ZSet为什么在数据量少的时候用ZipList,而在数据量大的时候转成SkipList?
    ZSet为什么在数据量少的时候用ZipList,而在数据量大的时候转成SkipList?
  • [问题求助] Redis中的setnx和setex有啥区别?
    Redis中的setnx和setex有啥区别?