Redis 7.0中5种新特性及实战应用
Redis 7.0引入了多项革命性的新特性,不仅在性能和可靠性方面有所提升,更在功能和使用体验上有了质的飞跃。本文将介绍Redis 7.0的五大关键新特性,可以根据实际情况利用Redis 7.0的强大功能,构建更高效、更可靠的应用系统。
特性一:Redis Functions(函数存储)
技术原理
Redis Functions是Redis 7.0引入的重量级特性,它允许开发者将Lua脚本作为命名函数存储在Redis服务器中。与传统的EVAL命令不同,Redis Functions支持创建库(Library)的概念,可以将相关功能的函数组织在一起,提供更好的可管理性。
关键优势:
- 函数持久化存储在Redis中,无需每次连接时重新加载
- 支持函数版本管理和库的概念
- 提供更好的权限控制和可观测性
- 减少网络传输开销,提高执行效率
实现示例
创建和注册函数库
# 创建一个简单的计数器函数库
FUNCTION LOAD "
#!lua name=mycounterredis.register_function('incr_by_and_get', function(keys, args)local key = keys[1]local increment = tonumber(args[1])local result = redis.call('INCRBY', key, increment)return result
end)redis.register_function('get_and_incr_by', function(keys, args)local key = keys[1]local increment = tonumber(args[1])local current = tonumber(redis.call('GET', key)) or 0redis.call('INCRBY', key, increment)return current
end)
"
调用函数
# 使用FCALL命令调用函数
FCALL incr_by_and_get 1 my_counter 5
# 返回递增后的值,比如 5FCALL get_and_incr_by 1 my_counter 3
# 返回递增前的值,比如 5(然后递增到 8)
Java客户端示例
@Service
public class RedisCounterService {private final StringRedisTemplate redisTemplate;public RedisCounterService(StringRedisTemplate redisTemplate) {this.redisTemplate = redisTemplate;}public void initializeCounterFunctions() {String functionScript = """#!lua name=mycounterredis.register_function('incr_by_and_get', function(keys, args)local key = keys[1]local increment = tonumber(args[1])local result = redis.call('INCRBY', key, increment)return resultend)redis.register_function('get_and_incr_by', function(keys, args)local key = keys[1]local increment = tonumber(args[1])local current = tonumber(redis.call('GET', key)) or 0redis.call('INCRBY', key, increment)return currentend)""";try {redisTemplate.execute((RedisCallback<Object>) connection -> {connection.serverCommands().functionLoad(functionScript);return null;});} catch (Exception e) {// 处理已存在的情况if (e.getMessage().contains("already exists")) {log.info("Counter functions already loaded");} else {throw e;}}}public Long incrementAndGet(String counterKey, int increment) {return redisTemplate.execute((RedisCallback<Long>) connection -> (Long) connection.serverCommands().functionCall("incr_by_and_get", Arrays.asList(counterKey), Arrays.asList(String.valueOf(increment))));}public Long getAndIncrement(String counterKey, int increment) {return redisTemplate.execute((RedisCallback<Long>) connection -> (Long) connection.serverCommands().functionCall("get_and_incr_by", Arrays.asList(counterKey), Arrays.asList(String.valueOf(increment))));}
}
实际应用场景
1. 原子计数器与限流器
函数库可以实现复杂的计数逻辑,如分布式限流器、访问频率控制等。
#!lua name=ratelimiter-- 令牌桶限流算法
redis.register_function('acquire_token', function(keys, args)local key = keys[1]local capacity = tonumber(args[1])local rate = tonumber(args[2])local requested = tonumber(args[3]) or 1local now = tonumber(redis.call('TIME')[1])-- 获取当前桶状态local bucket = redis.call('HMGET', key, 'last_refill', 'tokens')local last_refill = tonumber(bucket[1]) or nowlocal tokens = tonumber(bucket[2]) or capacity-- 计算令牌补充local elapsed = now - last_refilllocal new_tokens = math.min(capacity, tokens + elapsed * rate)-- 尝试获取令牌if new_tokens >= requested thennew_tokens = new_tokens - requestedredis.call('HMSET', key, 'last_refill', now, 'tokens', new_tokens)return 1 -- 成功elseredis.call('HMSET', key, 'last_refill', now, 'tokens', new_tokens)return 0 -- 失败end
end)
2. 复杂业务逻辑封装
电商场景中的下单流程,涉及库存检查、价格计算、订单创建等多个步骤:
#!lua name=ordersystem-- 下单流程
redis.register_function('create_order', function(keys, args)local product_key = keys[1]local order_key = keys[2]local product_id = args[1]local quantity = tonumber(args[2])local user_id = args[3]-- 检查库存local stock = tonumber(redis.call('HGET', product_key, 'stock'))if not stock or stock < quantity thenreturn {err = "Insufficient stock"}end-- 获取价格local price = tonumber(redis.call('HGET', product_key, 'price'))if not price thenreturn {err = "Product price not found"}end-- 创建订单local order_id = redis.call('INCR', 'order:id:counter')local total = price * quantity-- 减库存redis.call('HINCRBY', product_key, 'stock', -quantity)-- 保存订单local order_data = {id = order_id,user_id = user_id,product_id = product_id,quantity = quantity,price = price,total = total,status = "created",create_time = redis.call('TIME')[1]}redis.call('HMSET', order_key .. order_id, 'id', order_data.id,'user_id', order_data.user_id,'product_id', order_data.product_id,'quantity', order_data.quantity,'price', order_data.price,'total', order_data.total,'status', order_data.status,'create_time', order_data.create_time)-- 添加到用户订单列表redis.call('SADD', 'user:' .. user_id .. ':orders', order_id)return {order_id = order_id,total = total}
end)
3. 数据一致性保证
在需要保证多个操作原子性的场景中特别有用,如积分兑换:
#!lua name=pointsystem-- 积分兑换
redis.register_function('redeem_points', function(keys, args)local user_points_key = keys[1]local reward_key = keys[2]local user_rewards_key = keys[3]local user_id = args[1]local reward_id = args[2]local required_points = tonumber(args[3])-- 检查用户积分local current_points = tonumber(redis.call('GET', user_points_key)) or 0if current_points < required_points thenreturn {success = false, reason = "Insufficient points"}end-- 检查奖励是否有效local reward_exists = redis.call('EXISTS', reward_key)if reward_exists == 0 thenreturn {success = false, reason = "Reward not found"}end-- 扣减积分redis.call('DECRBY', user_points_key, required_points)-- 记录兑换历史local redeem_id = redis.call('INCR', 'redeem:id:counter')redis.call('HMSET', 'redeem:' .. redeem_id,'user_id', user_id,'reward_id', reward_id,'points', required_points,'time', redis.call('TIME')[1])-- 添加到用户奖励列表redis.call('SADD', user_rewards_key, reward_id)return {success = true,redeem_id = redeem_id,remaining_points = current_points - required_points}
end)
最佳实践
- 功能分组:按业务功能将相关函数组织到同一个库中,提高代码可维护性
- 版本管理:为函数库添加版本信息,便于升级和回滚
- 错误处理:在Lua函数中添加完善的错误处理逻辑
- 权限控制:结合ACL限制函数的调用权限
- 单一职责:每个函数保持功能单一,避免过于复杂的逻辑
特性二:分片发布/订阅(Sharded Pub/Sub)
技术原理
Redis 7.0引入了分片发布/订阅功能,这是对传统Pub/Sub模型的重要增强。传统的Pub/Sub在集群环境下存在效率和可扩展性问题,因为消息需要在所有节点间广播。分片Pub/Sub通过将频道分布到特定的节点,实现了更高效的消息传递。
关键优势:
- 消息只在特定节点处理,减少网络开销
- 频道数据和订阅信息只存储在特定节点,降低内存使用
- 更好的可扩展性,适合大规模Redis集群
- 避免了全局广播带来的性能问题
实现示例
Redis命令
# 订阅分片频道
SSUBSCRIBE news.sports# 向分片频道发布消息
SPUBLISH news.sports "Team A won the championship"# 退订分片频道
SUNSUBSCRIBE news.sports
Java实现
@Service
public class ShardedPubSubService {private final RedisTemplate<String, String> redisTemplate;private final Map<String, MessageListener> subscriptions = new ConcurrentHashMap<>();public ShardedPubSubService(RedisTemplate<String, String> redisTemplate) {this.redisTemplate = redisTemplate;}public void publish(String channel, String message) {redisTemplate.execute((RedisCallback<Long>) connection -> {// 使用底层连接直接执行SPUBLISH命令return connection.execute("SPUBLISH", channel.getBytes(), message.getBytes());});}public void subscribe(String channel, Consumer<String> messageHandler) {MessageListener listener = (message, pattern) -> messageHandler.accept(new String(message.getBody()));RedisMessageListenerContainer container = new RedisMessageListenerContainer();container.setConnectionFactory(redisTemplate.getConnectionFactory());container.afterPropertiesSet();// 注册分片订阅container.addMessageListener(listener, new PatternTopic("__shard__:" + channel));// 存储引用以便后续取消订阅subscriptions.put(channel, listener);}public void unsubscribe(String channel) {MessageListener listener = subscriptions.remove(channel);if (listener != null) {redisTemplate.execute((RedisCallback<Void>) connection -> {connection.execute("SUNSUBSCRIBE", channel.getBytes());return null;});}}
}
实际应用场景
1. 地理位置感知的消息推送
在基于地理位置的应用中,可以使用分片Pub/Sub向特定区域的用户推送消息:
@Service
public class LocationBasedNotificationService {private final ShardedPubSubService pubSubService;private final UserLocationService locationService;// 发送区域通知public void sendAreaNotification(String areaCode, String message) {String channel = "location.area." + areaCode;pubSubService.publish(channel, message);}// 用户订阅自己所在区域的通知public void subscribeUserToAreaNotifications(String userId) {String userArea = locationService.getUserAreaCode(userId);String channel = "location.area." + userArea;pubSubService.subscribe(channel, message -> {// 处理接收到的区域通知notifyUser(userId, message);});}
}
2. 实时聊天系统
分片Pub/Sub非常适合大规模聊天应用,减轻服务器负担:
@Service
public class ChatService {private final ShardedPubSubService pubSubService;// 发送聊天消息public void sendChatMessage(String roomId, ChatMessage message) {String channel = "chat.room." + roomId;String messageJson = objectMapper.writeValueAsString(message);pubSubService.publish(channel, messageJson);}// 用户加入聊天室public void joinChatRoom(String userId, String roomId) {String channel = "chat.room." + roomId;// 订阅聊天室消息pubSubService.subscribe(channel, messageJson -> {ChatMessage message = objectMapper.readValue(messageJson, ChatMessage.class);// 将消息发送到用户WebSocketwebSocketService.sendToUser(userId, message);});// 发送加入通知ChatMessage joinMessage = new ChatMessage("system", userId + " joined the room", System.currentTimeMillis());pubSubService.publish(channel, objectMapper.writeValueAsString(joinMessage));}
}
3. 分布式系统状态同步
使用分片Pub/Sub实现微服务间的高效状态同步:
@Service
public class SystemStateManager {private final ShardedPubSubService pubSubService;@PostConstructpublic void init() {// 订阅配置更新pubSubService.subscribe("system.config", this::handleConfigUpdate);// 订阅服务状态变更pubSubService.subscribe("system.service." + getCurrentServiceName(), this::handleServiceCommand);}// 发布配置变更public void publishConfigChange(String configKey, String configValue) {ConfigChangeEvent event = new ConfigChangeEvent(configKey, configValue, System.currentTimeMillis());pubSubService.publish("system.config", objectMapper.writeValueAsString(event));}// 发送服务指令public void sendServiceCommand(String serviceName, String command, Map<String, Object> params) {ServiceCommand cmd = new ServiceCommand(command, params, System.currentTimeMillis());pubSubService.publish("system.service." + serviceName, objectMapper.writeValueAsString(cmd));}private void handleConfigUpdate(String message) {ConfigChangeEvent event = objectMapper.readValue(message, ConfigChangeEvent.class);// 更新本地配置configManager.updateConfig(event.getKey(), event.getValue());}private void handleServiceCommand(String message) {ServiceCommand command = objectMapper.readValue(message, ServiceCommand.class);// 执行命令commandExecutor.execute(command.getCommand(), command.getParams());}
}
最佳实践
- 频道命名规范:使用层次化的命名方式(如"category.subcategory.id")
- 消息序列化:使用JSON或其他格式序列化消息,便于跨语言使用
- 错误处理:在订阅处理程序中添加异常处理逻辑
- 合理分片:根据业务特性合理设计频道分布
- 组合传统Pub/Sub:某些需要全局广播的场景可以继续使用传统Pub/Sub
特性三:多部分AOF(Multi-part AOF)
技术原理
Redis 7.0对AOF(Append Only File)持久化机制进行了重大改进,引入了多部分AOF文件结构。传统AOF是单一文件,在重写时会导致磁盘压力和性能波动。新的多部分AOF由基础文件(base files)和增量文件(incremental files)组成,提供更高效的持久化机制。
关键优势:
- 减少AOF重写期间的磁盘I/O压力
- 降低内存使用峰值
- 更快的重写过程,减少性能波动
- 可靠性提升,减少数据丢失风险
配置示例
# redis.conf 配置# 启用AOF持久化
appendonly yes# 使用新的多部分AOF格式
aof-use-rdb-preamble yes# 设置AOF目录
dir /data/redis# 文件名前缀 (Redis 7.0新增)
appendfilename "appendonly.aof"# AOF自动重写
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
工作原理详解
多部分AOF的工作流程如下:
- 初始化:Redis在启动时创建一个基础(base)AOF文件和一个增量(incr)AOF文件
- 命令记录:新的写入命令追加到增量AOF文件中
- 重写触发:当满足重写条件时,Redis创建一个新的基础文件
- 文件管理:重写完成后,历史增量文件被清理,新的增量文件开始接收命令
文件命名:
- 基础文件:appendonly_base.aof
- 增量文件:appendonly_incr.aof.{seq}
- 清单文件:appendonly.manifest.json(包含所有AOF文件的信息)
实际应用场景
1. 高写入量数据库优化
对于高频写入的Redis实例,多部分AOF可以显著减少写入峰值带来的性能波动:
# 针对高写入量的配置优化# 使用更激进的AOF fsync策略,减少数据丢失风险
appendfsync everysec# 优化内存使用
aof-rewrite-incremental-fsync yes# 增加重写阈值,减少重写频率
auto-aof-rewrite-percentage 200
auto-aof-rewrite-min-size 128mb
2. 快速恢复方案实现
利用多部分AOF特性实现更快的数据恢复策略:
@Service
public class RedisPersistenceManager {@Value("${redis.data.dir}")private String redisDataDir;// 执行AOF分析public AOFAnalysisResult analyzeAOF() {File manifestFile = new File(redisDataDir, "appendonly.manifest.json");if (!manifestFile.exists()) {return new AOFAnalysisResult(false, "Manifest file not found");}// 解析清单文件AOFManifest manifest = objectMapper.readValue(manifestFile, AOFManifest.class);// 分析AOF文件信息long totalSize = 0;long commandCount = 0;File baseFile = new File(redisDataDir, manifest.getBaseAofName());totalSize += baseFile.length();for (String incrFile : manifest.getIncrAofNames()) {File f = new File(redisDataDir, incrFile);totalSize += f.length();commandCount += countCommands(f);}return new AOFAnalysisResult(true, "Base: " + manifest.getBaseAofName() + ", Incremental: " + manifest.getIncrAofNames().size() + ", Total size: " + formatSize(totalSize) + ", Commands: " + commandCount);}// 手动触发AOF重写public void triggerAofRewrite() {redisTemplate.execute((RedisCallback<String>) connection -> {connection.serverCommands().bgrewriteaof();return null;});}
}
3. 系统升级与迁移策略
在系统升级或Redis迁移场景中,利用多部分AOF简化流程:
@Service
public class RedisMigrationService {// 准备Redis迁移public MigrationPlan prepareMigration(String sourceRedisUrl, String targetRedisUrl) {MigrationPlan plan = new MigrationPlan();// 1. 分析源Redis AOF状态AOFAnalysisResult aofAnalysis = analyzeSourceRedisAOF(sourceRedisUrl);plan.setAofAnalysis(aofAnalysis);// 2. 如果未使用多部分AOF,建议升级if (!aofAnalysis.isMultiPartAof()) {plan.addStep("Enable multi-part AOF on source Redis");}// 3. 触发AOF重写以创建干净的基础文件plan.addStep("Trigger AOF rewrite to create clean base file");// 4. 设置数据传输步骤plan.addStep("Copy base AOF file to target server");plan.addStep("Start target Redis with base file");plan.addStep("Continue copying incremental files during migration");return plan;}// 执行迁移过程中的增量同步public void syncIncrementalAOF(String sourceRedisDataDir, String targetRedisDataDir) {// 读取源Redis的清单文件AOFManifest sourceManifest = readManifest(sourceRedisDataDir);// 读取目标Redis的清单文件AOFManifest targetManifest = readManifest(targetRedisDataDir);// 找出目标缺少的增量文件List<String> filesToSync = new ArrayList<>();for (String incrFile : sourceManifest.getIncrAofNames()) {if (!targetManifest.getIncrAofNames().contains(incrFile)) {filesToSync.add(incrFile);}}// 同步缺少的文件for (String file : filesToSync) {copyFile(new File(sourceRedisDataDir, file), new File(targetRedisDataDir, file));}// 更新目标Redis的清单文件writeManifest(targetRedisDataDir, sourceManifest);}
}
最佳实践
- 磁盘规划:为AOF文件分配足够的磁盘空间,预留增长空间
- 监控AOF状态:定期检查AOF文件大小和重写频率
- 备份策略:将AOF文件纳入常规备份计划
- fsync策略选择:根据数据重要性和性能需求选择合适的fsync策略
- 与RDB结合:在某些场景下同时启用RDB快照,提供额外保护
特性四:访问控制列表(ACL)增强
技术原理
Redis 7.0对ACL(Access Control List)系统进行了重要增强,提供了更精细的权限控制机制。新增的ACL功能包括Pub/Sub频道权限控制、KEYS命令的模式匹配权限,以及针对Redis Functions的权限管理。
关键增强:
- 支持对Pub/Sub频道的读写权限控制
- 能够定义KEYS命令可查询的键模式
- 对Redis Functions的调用权限控制
- 改进的权限继承和组合模式
实现示例
ACL规则配置
# 创建只能访问特定前缀键且只读的用户
ACL SETUSER readonly ON >secret123 ~product:* +get +scan +keys +zrange +hgetall# 创建有Pub/Sub特定频道权限的用户
ACL SETUSER publisher ON >pubpassword ~notification:* +@all &channel:notifications:*# 创建可以调用特定函数的用户
ACL SETUSER func_user ON >funcpass ~* +@all %f:mycounter:incr_by_and_get# 使用键模式限制keys命令
ACL SETUSER admin ON >adminpass ~* +@all %K~user:*
Java配置示例
@Configuration
public class RedisSecurityConfig {@Beanpublic RedisConnectionFactory redisConnectionFactory() {LettuceConnectionFactory factory = new LettuceConnectionFactory();factory.setUsername("app_user");factory.setPassword("app_password");return factory;}@Bean@Profile("admin")public RedisSecurityService redisSecurityService(StringRedisTemplate redisTemplate) {return new RedisSecurityService(redisTemplate);}
}@Service
public class RedisSecurityService {private final StringRedisTemplate redisTemplate;public RedisSecurityService(StringRedisTemplate redisTemplate) {this.redisTemplate = redisTemplate;}public void createReadOnlyUser(String username, String password, List<String> keyPatterns) {StringBuilder aclCommand = new StringBuilder();aclCommand.append("ACL SETUSER ").append(username).append(" ON >").append(password);// 添加键模式for (String pattern : keyPatterns) {aclCommand.append(" ~").append(pattern);}// 添加只读命令权限aclCommand.append(" +get +mget +scan +keys +hget +hgetall +zrange +scard +smembers +lrange +info");redisTemplate.execute((RedisCallback<Object>) connection -> connection.execute(aclCommand.toString()));}public void createFunctionUser(String username, String password, String libraryName, List<String> functions) {StringBuilder aclCommand = new StringBuilder();aclCommand.append("ACL SETUSER ").append(username).append(" ON >").append(password).append(" ~*"); // 允许访问所有键// 添加基本命令权限aclCommand.append(" +@read +@write");// 添加函数调用权限for (String function : functions) {aclCommand.append(" %f:").append(libraryName).append(":").append(function);}redisTemplate.execute((RedisCallback<Object>) connection -> connection.execute(aclCommand.toString()));}public void createPubSubUser(String username, String password, List<String> channelPatterns, boolean publishOnly) {StringBuilder aclCommand = new StringBuilder();aclCommand.append("ACL SETUSER ").append(username).append(" ON >").append(password).append(" ~*"); // 通常Pub/Sub用户不需要键访问权限,但根据需要可以调整// 添加Pub/Sub权限if (publishOnly) {aclCommand.append(" +publish");} else {aclCommand.append(" +publish +subscribe +psubscribe");}// 添加频道权限for (String pattern : channelPatterns) {aclCommand.append(" &").append(pattern);}redisTemplate.execute((RedisCallback<Object>) connection -> connection.execute(aclCommand.toString()));}public List<Map<String, Object>> listUsers() {List<Object> result = (List<Object>) redisTemplate.execute((RedisCallback<Object>) connection -> connection.execute("ACL LIST"));List<Map<String, Object>> users = new ArrayList<>();if (result != null) {for (Object userInfo : result) {String[] parts = userInfo.toString().split(" ");Map<String, Object> user = new HashMap<>();user.put("username", parts[1]);// 解析权限信息List<String> flags = new ArrayList<>();List<String> commands = new ArrayList<>();List<String> keys = new ArrayList<>();List<String> channels = new ArrayList<>();List<String> functions = new ArrayList<>();for (int i = 2; i < parts.length; i++) {String part = parts[i];if (part.startsWith("+") || part.startsWith("-")) {commands.add(part);} else if (part.startsWith("~")) {keys.add(part);} else if (part.startsWith("&")) {channels.add(part);} else if (part.startsWith("%")) {functions.add(part);} else if (part.equals("on") || part.equals("off")) {flags.add(part);}}user.put("flags", flags);user.put("commands", commands);user.put("keys", keys);user.put("channels", channels);user.put("functions", functions);users.add(user);}}return users;}
}
实际应用场景
1. 多租户SaaS应用
为不同租户创建隔离的Redis访问权限:
@Service
public class TenantRedisManager {private final RedisSecurityService redisSecurityService;public void setupNewTenant(String tenantId) {// 为租户创建只能访问自己数据的用户String username = "tenant_" + tenantId;String password = generateSecurePassword();// 键模式限制List<String> keyPatterns = Arrays.asList("tenant:" + tenantId + ":*","shared:public:*");// 创建用户redisSecurityService.createReadOnlyUser(username, password, keyPatterns);// 保存凭证(安全存储)credentialManager.storeTenantRedisCredentials(tenantId, username, password);// 创建具有写入权限的用户String adminUsername = "tenant_" + tenantId + "_admin";String adminPassword = generateSecurePassword();StringBuilder aclCommand = new StringBuilder();aclCommand.append("ACL SETUSER ").append(adminUsername).append(" ON >").append(adminPassword);// 添加键模式for (String pattern : keyPatterns) {aclCommand.append(" ~").append(pattern);}// 添加完整读写权限aclCommand.append(" +@all");// 执行ACL命令redisTemplate.execute((RedisCallback<Object>) connection -> connection.execute(aclCommand.toString()));// 保存管理员凭证credentialManager.storeTenantRedisAdminCredentials(tenantId, adminUsername, adminPassword);}
}
2. 消息系统中的发布者与订阅者分离
在消息系统中创建不同角色的用户:
@Service
public class NotificationSystemManager {private final RedisSecurityService redisSecurityService;// 设置消息发布者public void setupPublisherAccount(String system, String password) {List<String> channels = Arrays.asList("notifications:" + system + ":*");redisSecurityService.createPubSubUser("publisher_" + system, password, channels, true // 只有发布权限);}// 设置订阅者public void setupSubscriberAccount(String subscriberId, String password, List<String> systems) {List<String> channels = systems.stream().map(system -> "notifications:" + system + ":*").collect(Collectors.toList());StringBuilder aclCommand = new StringBuilder();aclCommand.append("ACL SETUSER subscriber_").append(subscriberId).append(" ON >").append(password).append(" ~*") // 不需要键访问权限.append(" +subscribe +psubscribe"); // 只有订阅权限// 添加频道权限for (String channel : channels) {aclCommand.append(" &").append(channel);}redisTemplate.execute((RedisCallback<Object>) connection -> connection.execute(aclCommand.toString()));}
}
3. API网关限流功能
为API网关创建有限制的Redis Functions调用权限:
@Service
public class ApiGatewayRateLimiterService {private final RedisSecurityService redisSecurityService;private final StringRedisTemplate adminRedisTemplate;@PostConstructpublic void setupRateLimiter() {// 部署限流函数库String rateLimiterScript = """#!lua name=ratelimiterredis.register_function('check_rate_limit', function(keys, args)local key = keys[1]local limit = tonumber(args[1])local window = tonumber(args[2])local current = redis.call('INCR', key)if current == 1 thenredis.call('EXPIRE', key, window)endreturn current <= limitend)""";adminRedisTemplate.execute((RedisCallback<Object>) connection -> connection.serverCommands().functionLoad(rateLimiterScript));// 为API网关创建专用用户String gatewayUser = "api_gateway";String gatewayPassword = secureRandomPassword();List<String> functions = Arrays.asList("check_rate_limit");redisSecurityService.createFunctionUser(gatewayUser, gatewayPassword, "ratelimiter", functions);// 安全保存凭证configService.saveApiGatewayRedisCredentials(gatewayUser, gatewayPassword);}
}
最佳实践
- 最小权限原则:为每个用户仅分配必要的权限
- 密码复杂性:使用强密码,并定期轮换
- 功能分割:按照功能角色创建不同的用户,避免单一用户拥有过多权限
- 监控ACL操作:记录和审计ACL变更
- 分层权限模型:实现权限继承和组合,简化管理
- 定期审核:定期检查和清理不再使用的账户
特性五:客户端缓存增强
技术原理
Redis 7.0对客户端缓存(Client-side Caching)进行了增强,使其更加实用和高效。客户端缓存允许Redis客户端在本地缓存数据,通过服务器通知机制在数据变更时使缓存失效。Redis 7.0添加了对集群环境和哈希子字段的支持,扩展了这一功能的应用范围。
关键优势:
- 减少网络请求和Redis服务器负载
- 降低读取操作延迟
- 支持分片集群环境的客户端缓存
- 支持哈希字段级别的缓存控制
- 改进的内存效率和缓存追踪
实现原理
客户端缓存有两种模式:
- 跟踪模式:Redis服务器记录每个客户端缓存的键,当键变更时发送通知
- 广播模式:服务器广播所有键的变更,客户端根据自己的缓存内容决定是否使缓存失效
Redis 7.0中的改进包括:
- 集群环境中的缓存一致性
- 哈希字段级别的追踪
- 优化的内存使用
实现示例
使用Lettuce客户端实现
@Configuration
public class RedisClientCacheConfig {@Beanpublic ClientResources clientResources() {return ClientResources.builder().build();}@Beanpublic RedisClient redisClient(ClientResources clientResources) {return RedisClient.create(clientResources, RedisURI.create("redis://localhost:6379"));}@Beanpublic StatefulRedisConnection<String, String> connection(RedisClient redisClient) {return redisClient.connect();}@Beanpublic RedisCommands<String, String> redisCommands(StatefulRedisConnection<String, String> connection) {return connection.sync();}
}@Service
public class CachingRedisClient {private final RedisCommands<String, String> redis;private final StatefulRedisConnection<String, String> connection;private final Map<String, String> localCache = new ConcurrentHashMap<>();private final Set<String> trackedKeys = ConcurrentHashMap.newKeySet();public CachingRedisClient(RedisCommands<String, String> redis, StatefulRedisConnection<String, String> connection) {this.redis = redis;this.connection = connection;setupClientCaching();}private void setupClientCaching() {// 设置失效通知处理器connection.addListener(message -> {if (message instanceof PushMessage) {PushMessage pushMessage = (PushMessage) message;if ("invalidate".equals(pushMessage.getType())) {List<Object> invalidations = pushMessage.getContent();// 处理失效通知processInvalidations(invalidations);}}});// 启用客户端缓存,使用跟踪模式redis.clientTracking(ClientTrackingArgs.Builder.enabled().bcast().prefixes("user:", "product:").optIn());}public String get(String key) {// 先检查本地缓存String cachedValue = localCache.get(key);if (cachedValue != null) {return cachedValue;}// 本地缓存未命中,从Redis获取String value = redis.get(key);if (value != null) {// 存入本地缓存localCache.put(key, value);trackedKeys.add(key);}return value;}public void set(String key, String value) {// 更新Redisredis.set(key, value);// 更新本地缓存localCache.put(key, value);trackedKeys.add(key);}private void processInvalidations(List<Object> invalidations) {if (invalidations.size() >= 2) {String invalidationType = new String((byte[]) invalidations.get(0));if ("key".equals(invalidationType)) {// 单个键失效String key = new String((byte[]) invalidations.get(1));localCache.remove(key);trackedKeys.remove(key);} else if ("prefix".equals(invalidationType)) {// 前缀失效String prefix = new String((byte[]) invalidations.get(1));// 移除所有匹配前缀的缓存项Iterator<Map.Entry<String, String>> it = localCache.entrySet().iterator();while (it.hasNext()) {String key = it.next().getKey();if (key.startsWith(prefix)) {it.remove();trackedKeys.remove(key);}}}}}// 手动使缓存失效public void invalidateCache(String key) {localCache.remove(key);trackedKeys.remove(key);}// 获取缓存统计信息public Map<String, Object> getCacheStats() {Map<String, Object> stats = new HashMap<>();stats.put("cacheSize", localCache.size());stats.put("trackedKeysCount", trackedKeys.size());// 简单的统计信息Map<String, Integer> prefixCounts = new HashMap<>();for (String key : localCache.keySet()) {String prefix = key.split(":")[0] + ":";prefixCounts.put(prefix, prefixCounts.getOrDefault(prefix, 0) + 1);}stats.put("prefixCounts", prefixCounts);return stats;}
}
哈希字段级别缓存
@Service
public class HashFieldCachingService {private final RedisCommands<String, String> redis;private final StatefulRedisConnection<String, String> connection;private final Map<String, Map<String, String>> hashCache = new ConcurrentHashMap<>();public HashFieldCachingService(RedisCommands<String, String> redis, StatefulRedisConnection<String, String> connection) {this.redis = redis;this.connection = connection;setupClientCaching();}private void setupClientCaching() {// 设置失效通知处理器connection.addListener(message -> {if (message instanceof PushMessage) {PushMessage pushMessage = (PushMessage) message;if ("invalidate".equals(pushMessage.getType())) {List<Object> invalidations = pushMessage.getContent();// 处理失效通知processInvalidations(invalidations);}}});// 启用客户端缓存,使用跟踪模式redis.clientTracking(ClientTrackingArgs.Builder.enabled().prefixes("user:", "product:").optIn());}// 获取哈希字段public String hget(String key, String field) {// 先检查本地缓存Map<String, String> cachedHash = hashCache.get(key);if (cachedHash != null && cachedHash.containsKey(field)) {return cachedHash.get(field);}// 本地缓存未命中,从Redis获取String value = redis.hget(key, field);if (value != null) {// 存入本地缓存cachedHash = hashCache.computeIfAbsent(key, k -> new ConcurrentHashMap<>());cachedHash.put(field, value);}return value;}// 获取整个哈希public Map<String, String> hgetall(String key) {// 先检查本地缓存是否有完整哈希Map<String, String> cachedHash = hashCache.get(key);// 如果不存在或者不确定是否完整,从Redis获取Map<String, String> redisHash = redis.hgetall(key);if (!redisHash.isEmpty()) {// 更新本地缓存hashCache.put(key, new ConcurrentHashMap<>(redisHash));return redisHash;}return cachedHash != null ? cachedHash : new HashMap<>();}// 设置哈希字段public void hset(String key, String field, String value) {// 更新Redisredis.hset(key, field, value);// 更新本地缓存Map<String, String> cachedHash = hashCache.computeIfAbsent(key, k -> new ConcurrentHashMap<>());cachedHash.put(field, value);}private void processInvalidations(List<Object> invalidations) {if (invalidations.size() >= 2) {String invalidationType = new String((byte[]) invalidations.get(0));if ("key".equals(invalidationType)) {// 单个键失效String key = new String((byte[]) invalidations.get(1));hashCache.remove(key);} else if ("prefix".equals(invalidationType)) {// 前缀失效String prefix = new String((byte[]) invalidations.get(1));// 移除所有匹配前缀的缓存项hashCache.keySet().removeIf(key -> key.startsWith(prefix));}}}
}
实际应用场景
1. 用户配置文件管理
在需要频繁读取用户个人信息但写入较少的场景中:
@Service
public class UserProfileService {private final CachingRedisClient redisClient;// 获取用户资料public UserProfile getUserProfile(String userId) {String cacheKey = "user:" + userId + ":profile";// 利用客户端缓存获取数据String profileJson = redisClient.get(cacheKey);if (profileJson != null) {return objectMapper.readValue(profileJson, UserProfile.class);}return null;}// 更新用户资料public void updateUserProfile(String userId, UserProfile profile) {String cacheKey = "user:" + userId + ":profile";// 序列化为JSONString profileJson = objectMapper.writeValueAsString(profile);// 更新Redis,客户端缓存会自动更新redisClient.set(cacheKey, profileJson);// 记录审计日志logProfileUpdate(userId, profile);}// 批量获取多个用户资料public Map<String, UserProfile> getUserProfiles(List<String> userIds) {Map<String, UserProfile> results = new HashMap<>();for (String userId : userIds) {UserProfile profile = getUserProfile(userId);if (profile != null) {results.put(userId, profile);}}return results;}
}
2. 产品目录展示
电商平台中的产品信息缓存:
@Service
public class ProductCatalogService {private final HashFieldCachingService hashCache;// 获取产品基本信息public Product getProductBasicInfo(String productId) {String key = "product:" + productId;// 获取基本信息字段String name = hashCache.hget(key, "name");String price = hashCache.hget(key, "price");String category = hashCache.hget(key, "category");if (name != null && price != null) {Product product = new Product();product.setId(productId);product.setName(name);product.setPrice(Double.parseDouble(price));product.setCategory(category);return product;}return null;}// 获取产品完整信息public ProductDetails getProductDetails(String productId) {String key = "product:" + productId;// 获取完整哈希Map<String, String> productData = hashCache.hgetall(key);if (productData.isEmpty()) {return null;}// 构建产品详情对象ProductDetails details = new ProductDetails();details.setId(productId);details.setName(productData.get("name"));details.setPrice(Double.parseDouble(productData.get("price")));details.setCategory(productData.get("category"));details.setDescription(productData.get("description"));details.setBrand(productData.get("brand"));// 处理可选字段if (productData.containsKey("stock")) {details.setStock(Integer.parseInt(productData.get("stock")));}if (productData.containsKey("rating")) {details.setRating(Double.parseDouble(productData.get("rating")));}// 处理图片列表if (productData.containsKey("images")) {details.setImages(Arrays.asList(productData.get("images").split(",")));}return details;}// 更新产品价格public void updateProductPrice(String productId, double newPrice) {String key = "product:" + productId;hashCache.hset(key, "price", String.valueOf(newPrice));// 记录价格变更日志logPriceChange(productId, newPrice);}
}
3. 分布式配置管理
管理应用配置并实时同步更新:
@Service
public class DistributedConfigService {private final CachingRedisClient redisClient;private final Map<String, Map<String, ConfigValue>> configCache = new ConcurrentHashMap<>();// 获取配置值public String getConfigValue(String application, String key) {String cacheKey = "config:" + application + ":" + key;// 使用客户端缓存获取值String value = redisClient.get(cacheKey);if (value != null) {// 解析JSON值ConfigValue configValue = objectMapper.readValue(value, ConfigValue.class);return configValue.getValue();}return null;}// 更新配置值public void setConfigValue(String application, String key, String value) {String cacheKey = "config:" + application + ":" + key;// 创建带版本的配置值对象ConfigValue configValue = new ConfigValue();configValue.setValue(value);configValue.setVersion(System.currentTimeMillis());configValue.setUpdatedBy(getCurrentUser());// 序列化为JSONString valueJson = objectMapper.writeValueAsString(configValue);// 更新RedisredisClient.set(cacheKey, valueJson);// 发布配置变更事件publishConfigChangeEvent(application, key, value);}// 获取应用的所有配置public Map<String, String> getAllConfig(String application) {// 使用SCAN命令查找所有应用配置键Set<String> configKeys = scanKeys("config:" + application + ":*");Map<String, String> config = new HashMap<>();for (String fullKey : configKeys) {String key = fullKey.substring(("config:" + application + ":").length());String value = getConfigValue(application, key);if (value != null) {config.put(key, value);}}return config;}
}
最佳实践
- 选择合适的缓存模式:根据数据访问模式选择追踪或广播模式
- 控制缓存粒度:对于频繁变动的数据,考虑使用更细粒度的缓存
- 缓存大小管理:使用LRU或其他策略控制本地缓存大小
- 设置过期策略:为本地缓存设置合理的过期时间
- 优雅处理失效通知:实现健壮的失效处理逻辑
- 监控缓存效率:跟踪缓存命中率和内存使用情况
总结
Redis 7.0通过这五大核心特性:Redis Functions、分片Pub/Sub、多部分AOF、ACL增强以及客户端缓存优化,显著提升了Redis的功能性、性能和可靠性。
相关文章:
Redis 7.0中5种新特性及实战应用
Redis 7.0引入了多项革命性的新特性,不仅在性能和可靠性方面有所提升,更在功能和使用体验上有了质的飞跃。本文将介绍Redis 7.0的五大关键新特性,可以根据实际情况利用Redis 7.0的强大功能,构建更高效、更可靠的应用系统。 特性一…...
游戏如何应对AssetStudio解包工具
「游戏解包」是指将游戏文件中被压缩或加密的资源提取出来,通过解包工具对资源进行修改、查看或导出。这个过程通常涉及到将游戏客户端中的数据包进行解压,故称为“解包”。 游戏的资源文件包含代码、图片、视频、音频等重要内容。一旦被解密࿰…...
UE5 渲染思路笔记(角色)
参考示例 首先是怎么做到辉光只有部分有而整体没有的 使用的是Bloom内的阈值,控制光的溢光量 Threshold(阈值):这个参数决定了图像中哪些像素会参与泛光计算。只有那些亮度超过阈值的像素才会触发泛光效果。阈值越低,更多的像素会…...
Sublime Text快速搭建Lua语言运行环境
第一步 先去Sublime Text官网下载安装 Sublime Text - Text Editing, Done Right 第二步 下载lua编译运行程序 Lua - Joe DFs Builds 第三步 在Sublime Text中配置lua运行环境 {"cmd": ["D:/Lua/lua.exe", "$file"], "file_regex"…...
提示词的 嵌入空间优化
提示词的 嵌入空间优化 提示词的 嵌入空间优化的定义 提示词的嵌入空间优化,是指通过技术手段**调整提示词在低维向量空间(嵌入空间)**中的表示,使其更精准地捕捉语义信息、增强语义关联性,或适配特定任务需求,从而提升模型(如大语言模型)对提示词的理解与处理效果。…...
STM32--GPIO
教程 视频 博主教程 STM32系统结构图 GPIO GPIO(General Purpose Input/Output)是STM32内部的一种外设。 一个STM32芯片内存在多个GPIO外设,每个GPIO外设有16个引脚; 比如GPIOA:PA0~PA15; GPIOB:PB0~…...
npm下载插件无法更新package.json和package-lock.json文件的解决办法
经过多番查证,使用npm config ls查看相关配置等方式,最后发现全局的.npmrc文件的配置多写了globaltrue,去掉就好了 如果参数很多,不知道是哪个参数引起的,先只保留registryhttp://xxx/,试试下载࿰…...
ABAQUS三维CT重建插件CT2Model3D V2版本
插件介绍 CT2Model 3D V2.0插件采用Python 3.10研发,适配2024及以上版本的Abaqus软件,具备在Abaqus平台中基于CT断层扫描图像的三维重建功能,插件支持批量导入tif、tiff、png、jpg等格式的图像文件,推动了数字化建模技术与有限元…...
导入飞帆的网页为组件并注入数据驱动
飞帆制作的网页可以作为 Vue 2 组件导入到你自己的网页中使用。 这里我们来试一下。 并且将数据传入这个组件,驱动里面的仪表盘控件。 https://andi.cn/page/622177.html...
C语言的重要知识点☞static关键字
static译为"静态的",该关键字可以修饰以下内容: 修饰局部变量修饰全局变量修饰函数 在讲解static的具体作用前需要先知道"作用域"以及"生命周期"的概念: 作用域: 作用域是一个程序设计概念&#…...
unordered_map和unordered_set的设计
#pragma once #include"HashTable.h" namespace aqc {template<class K,class V,class HashHashFunc<K>>class unordered_map{public:struct MapKeyOfT{const K& operator()(const pair<K, V>& kv)//pair对象是const返回值也得是const{ret…...
Servlet--快速入门及HTTP概述
Servlet概述 Servlet:server applet,是用Java编写的服务器端程序,其主要功能在于交互式的浏览和修改数据,生成动态web内容,一般来说,Servlet是指实现了这个Servlet接口的类 在Java中,Servlet是用于创建动态Web内容的服务器端组件。 Servle…...
【LeetCode Hot100 | 每日刷题】二叉树的层序遍历
题目: 给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。 示例 1: 输入:root [3,9,20,null,null,15,7] 输出:[[3],[9,20],[15,7]]示例 2&a…...
编码器型与解码器型语言模型的比较
编码器型与解码器型语言模型的比较 1. 引言 自然语言处理(NLP)领域近年来取得了革命性进展,这在很大程度上归功于基于Transformer架构的语言模型。在这一技术生态中,编码器型(Encoder-only)和解码器型&am…...
Java 函数式编程
函数式编程的意义 函数式编程理念强调函数纯粹性和不可变性,这有助于写出更稳定、更易测试的代码,尤其在并发环境下减少 bug lambda 表达式 import java.util.function.Function;public class Strategize {Function<String, String> getString …...
MySQL初阶:基础增删改查(CRUD)
创建(Create) 先创建一个表 1)单独插入一条 insert into 表名 values (列名 类型)...; 插入的记录要和表一开始创建记录的类型,个数,结构一样。 如果不一样,就会报错。…...
yolo训练用的数据集的数据结构
Football Players Detection using YOLOV11 可以在roboflow上标注 Sign in to Roboflow 训练数据集只看这个data.yaml 里面是train的image地址和classnames 每个image一一对应一个label 第一个位是分类,0是classnames[0]对应的物体,现在是cuboid &…...
vue3+ts继续学习
我们再写点东西,这里面都是vue2的语法,应该都能看明白!我们写完直接去运行一下代码! 发现什么都没有发生!为什么呢?因为我们在App.vue中没有引入!哈哈哈哈!这样就好了!注…...
Oracle01-入门
零、文章目录 Oracle01-入门 1、Oracle简介 (1)数据库基础 数据库基础请参考:https://blog.csdn.net/liyou123456789/article/details/131207068 (2)Oracle是什么 ORACLE 数据库系统是美国 ORACLE 公司ÿ…...
即开即用,封装 Flask 项目为 exe 文件实操步骤
见字如面,朋友们! 嗨,这里是 AIGC 创意人_竹相左边! 正如你们所知,我正在通过 AI 自学软硬件工程师,目标是手搓一台可回收火箭玩具! 最近,我被《流浪地球 2》中马兆的那句“没有硬…...
【STM32单片机】#14 PWR电源控制
主要参考学习资料: B站江协科技 STM32入门教程-2023版 细致讲解 中文字幕 开发资料下载链接:https://pan.baidu.com/s/1h_UjuQKDX9IpP-U1Effbsw?pwddspb 单片机套装:STM32F103C8T6开发板单片机C6T6核心板 实验板最小系统板套件科协 目录 PWR…...
FastComposer论文问题与解决
在FastComposer中,跨注意力定位监督(Cross-Attention Localization Supervision) 的实现是通过以下步骤完成的,核心思想是利用分割掩码约束扩散模型中跨注意力图的分布,确保每个主体的特征仅影响图像中对应的区域。具体…...
51单片机同一个timer 作为定时器和波特率发生器么?
在51单片机中,同一个Timer(定时器)不能同时作为普通定时器和波特率发生器。这是因为这两种功能都需要对Timer的寄存器进行配置和操作,而它们的配置要求是冲突的。具体来说: 1. 普通定时器功能 配置要求:需…...
爱情的本质是什么--deepseek
爱情的本质是一个跨越生物学、心理学、哲学和社会学的复杂命题。不同学科视角下,爱情呈现出多层次的真相,但核心可以归结为: “爱情是进化塑造的生存策略、神经化学的短暂狂欢,以及人类对抗存在孤独的精神创造。” 以下从四个维…...
Leetcode 刷题记录 07 —— 链表
本系列为笔者的 Leetcode 刷题记录,顺序为 Hot 100 题官方顺序,根据标签命名,记录笔者总结的做题思路,附部分代码解释和疑问解答。 01 相交链表 /*** Definition for singly-linked list.* struct ListNode {* int val;* …...
Android View#post()源码分析
文章目录 Android View#post()源码分析概述onCreate和onResume不能获取View的宽高post可以获取View的宽高总结 Android View#post()源码分析 概述 在 Activity 中,在 onCreate() 和 onResume() 中是无法获取 View 的宽高,可以通过 View#post() 获取 Vi…...
dubbo限流
单机限流 限流过滤器 package com.doudou.filter;import org.apache.dubbo.common.URL; import org.apache.dubbo.common.constants.CommonConstants; import org.apache.dubbo.common.extension.Activate; import org.apache.dubbo.rpc.*;import java.util.concurrent.Concu…...
IBM BAW(原BPM升级版)使用教程:基本概念
本部分为“IBM BAW(原BPM升级版)使用教程系列”内容的补充。 一、IBM BAW中的流程概念 在IBM Business Automation Workflow(BAW)中,流程定义是流程设计的核心组成部分,它涵盖了流程的结构、任务、数据流…...
1. 视频基础知识
1. 图像基础概念 像素:像素是一个图片的基本单位,pix是英语单词picture,加上英语单词“元素element”,就得到了pixel,简称px。所以“像素”有“图像元素”之意。分辨率:指的是图像的大小或者尺寸。比如 19…...
docker + K3S + Jenkins + Harbor自动化部署
最近公司在研究自动化部署的一套流程,下面记录一下配置流程 需要提前准备好Jenkins Harbor Git(其他管理工具也可以) 我这里的打包编译流程是Jenkins上配置打包任务-->自动到git目录下找打包文件---->项目编译后打镜像包------>打完镜像包将镜像上传到…...
【算法专题十】哈希表
文章目录 0.哈希表简介1. 两数之和1.1 题目1.2 思路1.3 代码 2.判断是否为字符重排2.1 题目2.2 思路2.3 代码 3. leetcode.217.存在重复元素3.1 题目3.2 思路3.3 代码 4. leetcode.219.存在重复的元素Ⅱ4.1 题目4.2 思路4.3 代码 5. leetcode.49.字母异位词分组5.1 题目5.2 思路…...
鸿蒙系统被抹黑的深层解析:技术、商业与地缘政治的复杂博弈-优雅草卓伊凡
鸿蒙系统被抹黑的深层解析:技术、商业与地缘政治的复杂博弈-优雅草卓伊凡 一、技术过渡期的必然误解 1.1 兼容性设计的双刃剑效应 鸿蒙系统早期版本的兼容性策略为后续争议埋下了伏笔。2019年华为被列入实体清单后,面临着生死存亡的技术断供危机。在这…...
Nginx 安全防护与 HTTPS 安全部署
目录 Nginx 安全防护与 HTTPS 安全部署 一、引言 二、Nginx 安全防护措施 2.1 关闭不必要的服务和端口 2.2 限制访问频率 2.3 防止 SQL 注入和 XSS 攻击 2.4 隐藏 Nginx 版本信息 三、HTTPS 安全部署 3.1 HTTPS 简介 3.2 申请 SSL/TLS 证书 3.3 配置 Nginx 启用 HTTP…...
告别异步复杂性?JDK 21 虚拟线程让高并发编程重回简单
长期以来,Java 的并发编程主要围绕平台线程(Platform Threads)构建。然而,在现代应用对海量并发的巨大需求面前,传统模型面临着可伸缩性的挑战。JDK 21 引入了一项突破性的特性——虚拟线程(Virtual Thread…...
Marin说PCB之POC电路layout设计仿真案例---08
Layers –stackup: RX1_96724F_FAKRA_1仿真原理图信息如下,设计中采用了6Gbps/187Mbps的速率配置: IL的limited: RL的limited: RX1_96724F_FAKRA_1--Return Loss:结果显示,板级设计裕量不是很充足,很接近限值曲线了。 …...
【Python系列】Python 中的 HTTP 请求处理
💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…...
【BUG】mmdetection ValueError: need at least one array to concatenate
问题: 使用mmdetection框架使用COCO格式训练自定义数据集时出现如下错误: ValueError: need at least one array to concatenate 解决方法: 修改mmdet/datasets/coco.py文件,将CocoDataset类中的METAINFO修改为自己数据集的类别信…...
GLIBC:GLIBCXX not found
更多内容:XiaoJ的知识星球 目录 1. GLIBCXX not found2.解决方法:(使用预编译库)2.1 获取预编译libstdc库2.2 获取预编译libc库 注意:涉及到修改GLIBC库是个危险操作,可能会影响到系统。请谨慎操作…...
初步认识java
目录 1. java语言概述 1.1 java是什么 1.2 Java语言重要性 1.2.1 语言广泛使用程度 1.2.2 使用领域 1.3 Java语言发展简史 1.4 Java语言的特点 2. Java开发环境安装 2.1 什么是JDK 2.2 什么是JRE 2.3 什么是JVM 2.4 JDK、JRE 和 JVM的包含关系 2.5 JDK的安装和环…...
ShardingJdbc-水平分库
ShardingJdbc-水平分库 水平分库 表结构相同、记录不同、所属库不同多个库中表记录数和才是总的记录数通常根据主键ID进行分表,这里采用奇偶策略 案例 建立库 sharding_demo-1、sharding_demo-2每个库建立表 user_1、user_2 表结构相同id 为主键,big…...
模板模式 VS 建造者模式
模板模式和建造者模式是两种不同的设计模式,下面从定义、结构、应用场景等方面介绍它们的区别,并给出 Python 示例代码。 定义 模板模式:定义了一个操作中的算法骨架,将一些步骤的实现延迟到子类中。这样,子类可以在…...
模态编码器
1.CLIP的textEncoder能输入多少个单词? CLIP 模型中的 context_length 设置为 77,表示每个输入句子会被 tokenized 成最多 77 个token。这个 77 并不是直接对应到 77 个单词, 因为一个单词可能会被拆分成多个 token,特别是对于较长的或不常…...
Python-map从基础到进阶
无论你是打打算法比赛还是做项目map函数肯定都是你必学内置函数,这篇文章小白也能轻松掌握map函数,学习map,理解map,进阶用法map 描述 map() 函数会根据提供的函数对指定序列做映射。 第一个参数 function 以参数序列中的每一个…...
大数据产品销售数据分析:基于Python机器学习产品销售数据爬虫可视化分析预测系统设计与实现
文章目录 大数据产品销售数据分析:基于Python机器学习产品销售数据爬虫可视化分析预测系统设计与实现一、项目概述二、项目说明三、研究意义四、系统总体架构设计总体框架技术架构数据可视化模块设计图后台管理模块设计数据库设计 五、开发技术介绍Flask框架Python爬…...
「Mac畅玩AIGC与多模态21」开发篇17 - 多字段判断与多路径分支工作流示例
一、概述 本篇在结构化输出字段控制流程的基础上,进一步引入多字段联合判断与多路径分支控制。通过综合分析用户输入的情绪类型和紧急程度,实现三分支路径执行逻辑,开发人员将掌握复杂流程中多条件判断节点的配置技巧。 二、环境准备 macO…...
网页截图指南
截取网页截图看似是一项简单的任务,但当你真正动手去做的时候,就会发现事情远没有那么容易。我在尝试截取一篇很长的 Reddit 帖子时就深有体会。一开始我以为只要调用 browser.TakeImage() 就万事大吉,结果却陷入了浏览器视口、动态内容加载、…...
作为主动唤醒的节点,ECU上电如何请求通讯
一个ECU如果作为主动唤醒的节点,ECU上电时可以通过以下方式请求通信 如上图所示,ECU在上电后,在OS起来后,可以通过在BSWM模块中完成NvM_ReadAll和相关BSW 模块初始化以及Rte_Start后,这个时候周期性Task已经可以正常调…...
应用服务器Tomcat
启动两给tomcat apache-tomcat-9.0.60\bin——> 启动tomcat startup.bat (Windows) / startup.sh(Linux) 关闭tomcat shutdown.bat(Windows)/shutdown.sh (Linux) 复制一个Tomcat为2,先启…...
【安全】端口保护技术--端口敲门和单包授权
【安全】端口保护技术--端口敲门和单包授权 备注一、端口保护二、端口敲门三、单包授权 备注 2025/05/06 星期二 最近学习了端口保护技术总结一下 一、端口保护 为了保护联网设备的安全,一般会尽量减小暴露的攻击面,开放的端口就是最常见的攻击面&…...
金升阳科技:配套AC/DC砖类电源的高性能滤波器
金升阳推出的FC-L15HB是为我司AC砖类电源配套使用的EMC辅助器。将FC-L15HB加装在金升阳AC/DC砖类电源的前端,可以提高电源产品IEC/EN61000—4系列及CISPR32/EN55032标准的EMC性能。 01 产品优势 (1)高共差模插入损耗 ①DM&CM࿱…...