Redis中6种缓存更新策略详解

2025-05-03 18:50

本文主要是介绍Redis中6种缓存更新策略详解,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

《Redis中6种缓存更新策略详解》Redis作为一款高性能的内存数据库,已经成为缓存层的首选解决方案,然而,使用缓存时最大的挑战在于保证缓存数据与底层数据源的一致性,本文将介绍Redis中6种缓存更...

引言

Redis作为一款高性能的内存数据库,已经成为缓存层的首选解决方案。然而,使用缓存时最大的挑战在于保证缓存数据与底层数据源的一致性。缓存更新策略直接影响系统的性能、可靠性和数据一致性,选择合适的策略至关重要。

本文将介绍Redis中6种缓存更新策略。

策略一:Cache-Aside(旁路缓存)策略

工作原理

Cache-Aside是最常用的缓存模式,由应用层负责缓存和数据库的交互逻辑:

  • 读取数据:先查询缓存,命中则直接返回;未命中则查询数据库,将结果写入缓存并返回
  • 更新数据:先更新数据库,再删除缓存(或更新缓存)

代码示例

@Service
public class UserServiceCacheAside {
    
    @Autowired
    private RedisTemplate<String, User> redisTemplate;
    
    @Autowired
    private UserRepository userRepository;
    
    private static final String CACHE_KEY_PREFIX = "user:";
    private static final long CACHE_EXPIRATION = 30; // 缓存过期时间(分钟)
    
    public User getUserById(Long userId) {
        String cacheKey = CACHE_KEY_PREFIX + userId;
        
        // 1. 查询缓存
        User user = redisTemplate.opsForValue().get(cacheKey);
        
        // 2. 缓存命中,直接返回
        if (user != null) {
            return user;
        }
        
        // 3. 缓存未命中,查询数据库
        user = userRepository.findById(userId).orElse(null);
        
        // 4. 将数据库结果写入缓存(设置过期时间)
        if (user != null) {
            redisTemplate.opsForValue().set(cacheKey, user, CACHE_EXPIRATION, TimeUnit.MINUTES);
        }
        
        return user;
    }
    
    public void updateUser(User user) {
        // 1. 先更新数据库
        userRepository.save(user);
        
        // 2. 再删除缓存
        String cacheKey = CACHE_KEY_PREFIX + user.getId();
        redisTemplate.delete(cacheKey);
        
        // 或者选择更新缓存
        // redisTemplate.opsForValue().set(cacheKey, user, CACHE_EXPIRATION, TimeUnit.MINUTES);
    }
}

优缺点分析

优点

  • 实现简单,控制灵活
  • 适合读多写少的业务场景
  • 只缓存必要的数据,节省内存空间

缺点

  • 首次访问会有一定延迟(缓存未命中)
  • 存在并发问题:如果先删除缓存后更新数据库,可能导致数据不一致
  • 需要应用代码维护缓存一致性,增加了开发复杂度

适用场景

  • 读多写少的业务场景
  • 对数据一致性要求不是特别高的应用
  • 分布式系统中需要灵活控制缓存策略的场景

策略二:Read-Through(读穿透)策略

工作原理

Read-Through策略将缓存作为主要数据源的代理,由缓存层负责数据加载:

  • 应用程序只与缓存层交互
  • 当缓存未命中时,由缓存管理器负责从数据库加载数据并存入缓存
  • 应用程序无需关心缓存是否存在,缓存层自动处理加载逻辑

代码示例

首先定义缓存加载器接口:

public interface CacheLoader<K, V> {
    V load(K key);
}

实现Read-Through缓存管理器:

@Component
public class ReadThroughCacheManager<K, V> {
    
    @Autowired
    private RedisTemplate<String, V> redisTemplate;
    
    private final ConcurrentHashMap<String, CacheLoader<K, V>> loaders = new ConcurrentHashMap<>();
    
    public void registerLoader(String cachePrefix, CacheLoader<K, V> loader) {
        loaders.put(cachePrefix, loader);
    }
    
    public V get(String cachePrefix, K key, long expiration, TimeUnit timeUnit) {
        String cacheKey = cachePrefix + key;
        
        // 1. 查询缓存
        V value = redisTemplate.opsForValue().get(cacheKey);
        
        // 2. 缓存命中,直接返回
        if (value != null) {
            return value;
        }
        
        // 3. 缓存未命中,通过加载器获取数据
        CacheLoader<K, V> loader = loaders.get(cachePrefix);
        if (loader == null) {
            throw new IllegalStateException("No cache loader registered for prefix: " + cachePrefix);
        }
        
        // 使用加载器从数据源加载数据
        value = loader.load(key);
        
        // 4. 将加载的数据存入缓存
        if (value != null) {
            redisTemplate.opsForValue().set(cacheKey, value, expiration, timeUnit);
        }
        
        return value;
    }
}

使用示例:

@Service
public class UserServiceReadThrough {
    
    private static final String CACHE_PREFIX = "user:";
    private static final long CACHE_EXPIRATION = 30;
    
    @Autowired
    private ReadThroughCacheManager<Long, User> cacheManager;
    
    @Autowired
    private UserRepositopythonry userRepository;
    
    @PostConstruct
    public void init() {
        // 注册用户数据加载www.chinasem.cn器
        cacheManager.registerLoader(CACHE_PREFIX, this::loadUserFromDb);
    }
    
    private User loadUserFromDb(Long userId) {
        return userRepository.findById(userId).orElse(null);
    }
    
    public User getUserById(Long userId) {
        // 直接通过缓存管理器获取数据,缓存逻辑由管理器处理
        return cacheManager.get(CACHE_PREFIX, userId, CACHE_EXPIRATION, TimeUnit.MINUTES);
    }
}

优缺点分析

优点

  • 封装性好,应用代码无需关心缓存逻辑
  • 集中处理缓存加载,减少冗余代码
  • 适合只读或读多写少的数据

缺点

  • 缓存未命中时引发数据库请求,可能导致数据库负载增加
  • 无法直接处理写操作,需要与其他策略结合使用
  • 需要额外维护一个缓存管理层

适用场景

  • 读操作频繁的业务系统
  • 需要集中管理缓存加载逻辑的应用
  • 复杂的缓存预热和加载场景

策略三:Write-Through(写穿透)策略

工作原理

Write-Through策略由缓存层同步更新底层数据源:

  • 应用程序更新数据时先写入缓存
  • 然后由缓存层负责同步写入数据库
  • 只有当数据成功写入数据库后才视为更新成功

代码示例

首先定义写入接口:

public interface CacheWriter<K, V> {
    void write(K key, V value);
}

实现Write-Through缓存管理器:

@Component
public class WriteThroughCacheManager<K, V> {
    
    @Autowired
    private RedisTemplate<String, V> redisTemplate;
    
    private final ConcurrentHashMap<String, CacheWriter<K, V>> writers = new ConcurrentHashMap<>();
    
    public void registerWriter(String cachePrefix, CacheWriter<K, V> writer) {
        writers.put(cachePrefix, writer);
    }
    
    public void put(String cachePrefix, K key, V value, long expiration, TimeUnit timeUnit) {
        String cacheKey = cachePrefix + key;
        
        // 1. 获取对应的缓存写入器
        CacheWriter<K, V> writer = writers.get(cachePrefix);
        if (writer == null) {
            throw new IllegalStateException("No cache writer registered for prefix: " + cachePrefix);
        }
        
        // 2. 同步写入数据库
        writer.write(key, value);
        
        // 3. 更新缓存
        redisTemplate.opsForValue().set(cacheKey, value, expiration, timeUnit);
    }
}

使用示例:

@Service
public class UserServiceWriteThrough {
    
    private static final String CACHE_PREFIX = "user:";
    private static final long CACHE_EXPIRATION = 30;
    
    @Autowired
    private WriteThroughCacheManager<Long, User> cacheManager;
    
    @Autowired
    private UserRepository userRepository;
    
    @PostConstruct
    public void init() {
        // 注册用户数据写入器
        cacheManager.registerWriter(CACHE_PREFIX, this::saveUserToDb);
    }
    
    private void saveUserToDb(Long userId, User user) {
        userRepository.save(user);
    }
    
    public void updateUser(User user) {
        // 通过缓存管理器更新数据,会同步更新数据库和缓存
        cacheManager.put(CACHE_PREFIX, user.getId(), user, CACHE_EXPIRATION, TimeUnit.MINUTES);
    }
}

优缺点分析

优点

  • 保证数据库与缓存的强一致性
  • 将缓存更新逻辑封装在缓存层,简化应用代码
  • 读取缓存时命中率高,无需回源到数据库

缺点

  • 实时写入数据库增加了写操作延迟
  • 增加系统复杂度,需要处理事务一致性
  • 对数据库写入压力大的场景可能成为性能瓶颈

适用场景

  • 对数据一致性要求高的系统
  • 写操作不是性能瓶颈的应用
  • 需要保证缓存与数据库实时同步的场景

策略四:Write-Behind(写回)策略

工作原理

Write-Behind策略将写操作异步化处理:

  • 应用程序更新数据时只更新缓存
  • 缓存维护一个写入队列,将更新异步批量写入数据库
  • 通过批量操作减轻数据库压力

代码示例

实现异步写入队列和处理器:

@Component
public class WriteBehindCacheManager<K, V> {
    
    @Autowired
    private RedisTemplate<String, V> redisTemplate;
    
    private final blockingQueue<CacheUpdate<K, V>> updateQueue = new LinkedBlockingQueue<>();
    private final ConcurrentHashMap<String, CacheWriter<K, V>> writers = new ConcurrentHashMap<>();
    
    public void registerWriter(String cachePrefix, CacheWriter<K, V> writer) {
        writers.put(cachePrefix, writer);
    }
    
    @PostConstruct
    public void init() {
        // 启动异步写入线程
        Thread writerThread = new Thread(this::processWriteBehindQueue);
        writerThread.setDaemon(true);
        writerThread.start();
    }
    
    public void put(String cachePrefix, K key, V value, long expiration, TimeUnit timeUnit) {
        String cacheKey = cachePrefix + key;
        
        // 1. 更新缓存
        redisTemplate.opsForValue().set(cacheKey, value, expiration, timeUnit);
        
        // 2. 将更新放入队列,等待异步写入数据库
        updateQueue.offer(new CacheUpdate<>(cachePrefix, key, value));
    }
    
    private void processWriteBehindQueue() {
        List<CacheUpdate<K, V>> BATch = new ArrayList<>(100);
        
        while (true) {
            try {
                // 获取队列中的更新,最多等待100ms
                CacheUpdate<K, V> update = updateQueue.poll(100, TimeUnit.MILLISECONDS);
                
                if (update != null) {
                    batch.add(update);
                }
                
                // 继续收集队列中可用的更新,最多收集100个或等待200ms
                updateQueue.drainTo(batch, 100 - batch.size());
                
                if (!batch.isEmpty()) {
                    // 按缓存前缀分组批量处理
                    Map<String, List<CacheUpdate<K, V>>> groupedUpdates = batch.stream()
                            .collect(Collectors.groupingBy(CacheUpdate::getCachePrefix));
                    
                    for (Map.Entry<String, List<CacheUpdate<K, V>>> entry : groupedUpdates.entrySet()) {
                        String cachePrefix = entry.getKey();
                        List<CacheUpdate<K, V>> updates = entry.getValue();
                        
                        CacheWriter<K, V> writer = writers.get(cachePrefix);
                        if (writer != null) {
                            // 批量写入数据库
                            for (CacheUpdate<K, V> u : updates) {
                                try {
                                    writer.write(u.getKey(), u.getValue());
                                } catch (Exception e) {
                                    // 处理异常,可以重试或记录日志
                                    log.error("Failed to write-behind for key {}: {}", u.getKey(), e.getMessage());
                                }
                            }
                        }
                    }
                    
                    batch.clear();
                }
                
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            } catch (Exception e) {
                log.error("Error in write-behind process", e);
            }
        }
    }
    
    @Data
    @AllArgsConstructor
    private static class CacheUpdate<K, V> {
        private String cachePrefix;
        private K key;
        private V value;
    }
}

使用示例:

@Service
public class UserServiceWriteBehind {
    
    private static final String CACHE_PREFIX = "user:";
    private static final long CACHE_EXPIRATION = 30;
    
    @Autowired
    private WriteBehindCacheManager<Long, User> cacheManager;
    
    @Autowired
    private UserRepository userRepository;
    
    @PostConstruct
    public void init() {
        // 注册用户数据写入器
        cacheManager.registerWriter(CACHE_PREFIX, this::saveUserToDb);
    }
    
    private void saveUserToDb(Long userId, User user) {
        userRepository.save(user);
    }
    
    public void updateUser(User user) {
        // 更新仅写入缓存,异步写入数据库
        cacheManager.put(CACHE_PREFIX, user.getId(), user, CACHE_EXPIRATION, TimeUnit.MINUTES);
    }
}

优缺点分析

优点

  • 显著提高写操作性能,减少响应延迟
  • 通过批量操作减轻数据库压力
  • 平滑处理写入峰值,提高系统吞吐量

缺点

  • 存在数据一致性窗口期,不适合强一致性要求的场景
  • 系统崩溃可能导致未写入的数据丢失
  • 实现复杂,需要处理失败重试和冲突解决

适用场景

  • 高并发写入场景,如日志记录、统计数据
  • 对写操作延迟敏感但对一致性要求不高的应用
  • 数据库写入是系统瓶颈的场景

策略五:刷新过期(Refresh-Ahead)策略

工作原理

Refresh-Ahead策略预测性地在缓存过期前进行更新:

  • 缓存设置正常的过期时间
  • 当访问接近过期的缓存项时,触发异步刷新
  • 用户始终访问的是已缓存的数据,避免直接查询数据库的延迟

代码示例

@Component
public class RefreshAheadCacheManager<K, V> {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private ThreadPoolTaskExecutor refreshExecutor;
    
    private final ConcurrentHashMap<String, CacheLoader<K, V>> loaders = new ConcurrentHashMap<>();
    
    // 刷新阈值,当过期时间剩余不足阈值比例时触发刷新
    private final double refreshThreshold = 0.75; // 75%
    
    public void registerLoader(String cachePrefix, CacheLoader<K, V> loader) {
        loaders.put(cachePrefix, loader);
    }
    
    @SuppressWarnings("unchecked")
    public V get(String cachePrefix, K key, long expiration, TimeUnit timeUnit) {
        String cacheKey = cachePrefix + key;
        
        // 1. 获取缓存项和其TTL
        V value = (V) redisTemplate.opsForValue().get(cacheKey);
        Long ttl = redisTemplate.getExpire(cacheKey, TimeUnit.MILLISECONDS);
        
        if (value != null) {
            // 2. 如果缓存存在但接近过期,触发异步刷新
            if (ttl != null && ttl > 0) {
                long expirationMs = timeUnit.toMillis(expiration);
                if (ttl < expirationMs * (1 - refreshThreshold)) {
                    refreshAsync(cachePrefix, key, cacheKey, expiration, timeUnit);
                }
            }
            return value;
        }
        
        // 3. 缓存不存在,同步加载
        return loadAndCache(cachePrefix, key, cacheKey, expiration, timeUnit);
    }
    
    private void refreshAsync(String cachePrefix, K key, String cacheKey, long expiration, TimeUnit timeUnit) {
        refreshExecutor.execute(() -> {
            try {
                loadAndCache(cachePrefix, key, cacheKey, expiration, timeUnit);
            } catch (Exception e) {
                // 异步刷新失败,记录日志但不影响当前请求
                log.error("Failed to refresh cache for key {}: {}", cacheKey, e.getMessage());
            }
        });
    }
    
    private V loadAndCache(String cachePrefix, K key, String cacheKey, long expiration, TimeUnit timeUnit) {
        CacheLoader<K, V> loader = loaders.get(cachePrefix);
        if (loader == null) {
            throw new IllegalStateException("No cache loader registered for prefix: " + cachePrefix);
        }
        
        // 从数据源加载
        V value = loader.load(key);
        
        // 更新缓存
        if (value != null) {
            redisTemplate.opsForValue().set(cacheKey, value, expiration, timeUnit);
        }
        
        return value;
    }
}

使用示例:

@Service
public class ProductServiceRefreshAhead {
    
    privatandroide static final String CACHE_PREFIX = "product:";
    private static final long CACHE_EXPIRATION = 60; // 1小时
    
    @Autowired
    private RefreshAheadCacheManager<String, Product> cacheManager;
    
    @Autowired
    private ProductRepository productRepository;
    
    @PostConstruct
    public void init() {
        // 注册产品数据加载器
        cacheManager.registerLoader(CACHE_PREFIX, this::loadProductFromDb);
    }
    
    private Product loadProductFromDb(String productId) {
        return productRepository.findById(productId).orElse(null);
    }
    
    public Product getProduct(String productId) {
        return cacheManager.get(CACHE_PREFIX, productId, CACHE_EXPIRATION, TimeUnit.MINUTES);
    }
}

线程池配置

@Configuration
public class ThreadPoolConfig {
    
    @Bean
    public ThreadPoolTaskExecutor refreshExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(20);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("cache-refresh-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}

优缺点分析

优点

  • 用户始终访问缓存数据,避免因缓存过期导致的延迟
  • 异步刷新减轻了数据库负载峰值
  • 缓存命中率高,用户体验更好

缺点

  • 实现复杂度高,需要额外的线程池管理
  • 预测算法可能不准确,导致不必要的刷新
  • 对于很少访问的数据,刷新可能是浪费

适用场景

  • 对响应时间要求苛刻的高流量系统
  • 数据更新频率可预测的场景
  • 数据库资源有限但缓存容量充足的系统

策略六:最终一致性(Eventual Consistency)策略

工作原理

最终一致性策略基于分布式事件系统实现数据同步:

  • 数据变更时发布事件到消息队列
  • 缓存服务订阅相关事件并更新缓存
  • 即使某些操作暂时失败,最终系统也会达到一致状态

代码示例

首先定义数据变更事件:

@Data
@AllArgsConstructor
public class DataChangeEvent {
    private String entityType;
    private String entityId;
    private String operation; // CREATE, UPDATE, DELETE
    private String payload;   // jsON格式的实体数据
}

实现事件发布者:

@Component
public class DataChangePublisher {
    
    @Autowired
    private KafkaTemplate<String, DataChangeEvent> kafkaTemplate;
    
    private static final String TOPIC = "data-changes";
    
    public void publishChange(String entityType, String entityId, String operation, Object entity) {
        try {
            // 将实体序列化为JSON
            String payload = new ObjectMapper().writeValueAsString(entity);
            
            // 创建事件
            DataChangeEvent event = new DataChangeEpythonvent(entityType, entityId, operation, payload);
            
            // 发布到Kafka
            kafkaTemplate.send(TOPIC, entityId, event);
        } catch (Exception e) {
            log.error("Failed to publish data change event", e);
            throw new RuntimeException("Failed to publish event", e);
        }
    }
}

实现事件消费者更新缓存:

@Component
@Slf4j
public class CacheUpdateConsumer {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    private static final long CACHE_EXPIRATION = 30;
    
    @KafkaListener(topics = "data-changes")
    public void handleDataChangeEvent(DataChangeEvent event) {
        try {
            String cacheKey = buildCacheKey(event.getEntityType(), event.getEntityId());
            
            switch (event.getOperation()) {
                case "CREATE":
                case "UPDATE":
                    // 解析JSON数据
                    Object entity = parseEntity(event.getPayload(), event.getEntityType());
                    // 更新缓存
                    redisTemplate.opsForValue().set(
                            cacheKey, entity, CACHE_EXPIRATION, TimeUnit.MINUTES);
                    log.info("Updated cache for {}: {}", cacheKey, event.getOperation());
                    break;
                    
                case "DELETE":
                    // 删除缓存
                    redisTemplate.delete(cacheKey);
                    log.info("Deleted cache for {}", cacheKey);
                    break;
                    
                default:
                    log.warn("Unknown operation: {}", event.getOperation());
            }
        } catch (Exception e) {
            log.error("Error handling data change event: {}", e.getMessage(), e);
            // 失败处理:可以将失败事件放入死信队列等
        }
    }
    
    private String buildCacheKey(String entityType, String entityId) {
        return entityType.toLowerCase() + ":" + entityId;
    }
    
    private Object parseEntity(String payload, String entityType) throws JsonProcessingException {
        // 根据实体类型选择反序列化目标类
        Class<?> targetClass = geChina编程tClassForEntityType(entityType);
        return new ObjectMapper().readValue(payload, targetClass);
    }
    
    private Class<?> getClassForEntityType(String entityType) {
        switch (entityType) {
            case "User": return User.class;
            case "Product": return Product.class;
            // 其他实体类型
            default: throw new IllegalArgumentException("Unknown entity type: " + entityType);
        }
    }
}

使用示例:

@Service
@Transactional
public class UserServiceEventDriven {
    
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private DataChangePublisher publisher;
    
    public User createUser(User user) {
        // 1. 保存用户到数据库
        User savedUser = userRepository.save(user);
        
        // 2. 发布创建事件
        publisher.publishChange("User", savedUser.getId().toString(), "CREATE", savedUser);
        
        return savedUser;
    }
    
    public User updateUser(User user) {
        // 1. 更新用户到数据库
        User updatedUser = userRepository.save(user);
        
        // 2. 发布更新事件
        publisher.publishChange("User", updatedUser.getId().toString(), "UPDATE", updatedUser);
        
        return updatedUser;
    }
    
    public void deleteUser(Long userId) {
        // 1. 从数据库删除用户
        userRepository.deleteById(userId);
        
        // 2. 发布删除事件
        publisher.publishChange("User", userId.toString(), "DELETE", null);
    }
}

优缺点分析

优点

  • 支持分布式系统中的数据一致性
  • 削峰填谷,减轻系统负载峰值
  • 服务解耦,提高系统弹性和可扩展性

缺点

  • 一致性延迟,只能保证最终一致性
  • 实现和维护更复杂,需要消息队列基础设施
  • 可能需要处理消息重复和乱序问题

适用场景

  • 大型分布式系统
  • 可以接受短暂不一致的业务场景
  • 需要解耦数据源和缓存更新逻辑的系统

缓存更新策略选择指南

选择合适的缓存更新策略需要考虑以下因素:

1. 业务特性考量

业务特征推荐策略
读多写少Cache-Aside 或 Read-Through
写密集型Write-Behind
高一致性需求Write-Through
响应时间敏感Refresh-Ahead
分布式系统最终一致性

2. 资源限制考量

资源约束推荐策略
内存限制Cache-Aside(按需缓存)
数据库负载高Write-Behind(减轻写压力)
网络带宽受限Write-Behind 或 Refresh-Ahead

3. 开发复杂度考量

复杂度要求推荐策略
简单实现Cache-Aside
中等复杂度Read-Through 或 Write-Through
高复杂度但高性能Write-Behind 或 最终一致性

结论

缓存更新是Redis应用设计中的核心挑战,没有万能的策略适用于所有场景。根据业务需求、数据特性和系统资源,选择合适的缓存更新策略或组合多种策略才是最佳实践。

在实际应用中,可以根据不同数据的特性选择不同的缓存策略,甚至在同一个系统中组合多种策略,以达到性能和一致性的最佳平衡。

以上就是Redis中6种缓存更新策略详解的详细内容,更多关于Redis缓存更新的资料请关注编程China编程(www.chinasem.cn)其它相关文章!

这篇关于Redis中6种缓存更新策略详解的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1154471

相关文章

Docker多阶段镜像构建与缓存利用性能优化实践指南

《Docker多阶段镜像构建与缓存利用性能优化实践指南》这篇文章将从原理层面深入解析Docker多阶段构建与缓存机制,结合实际项目示例,说明如何有效利用构建缓存,组织镜像层次,最大化提升构建速度并减少... 目录一、技术背景与应用场景二、核心原理深入分析三、关键 dockerfile 解读3.1 Docke

Android实现图片浏览功能的示例详解(附带源码)

《Android实现图片浏览功能的示例详解(附带源码)》在许多应用中,都需要展示图片并支持用户进行浏览,本文主要为大家介绍了如何通过Android实现图片浏览功能,感兴趣的小伙伴可以跟随小编一起学习一... 目录一、项目背景详细介绍二、项目需求详细介绍三、相关技术详细介绍四、实现思路详细介绍五、完整实现代码

SpringBoot AspectJ切面配合自定义注解实现权限校验的示例详解

《SpringBootAspectJ切面配合自定义注解实现权限校验的示例详解》本文章介绍了如何通过创建自定义的权限校验注解,配合AspectJ切面拦截注解实现权限校验,本文结合实例代码给大家介绍的非... 目录1. 创建权限校验注解2. 创建ASPectJ切面拦截注解校验权限3. 用法示例A. 参考文章本文

Java中字符编码问题的解决方法详解

《Java中字符编码问题的解决方法详解》在日常Java开发中,字符编码问题是一个非常常见却又特别容易踩坑的地方,这篇文章就带你一步一步看清楚字符编码的来龙去脉,并结合可运行的代码,看看如何在Java项... 目录前言背景:为什么会出现编码问题常见场景分析控制台输出乱码文件读写乱码数据库存取乱码解决方案统一使

PHP轻松处理千万行数据的方法详解

《PHP轻松处理千万行数据的方法详解》说到处理大数据集,PHP通常不是第一个想到的语言,但如果你曾经需要处理数百万行数据而不让服务器崩溃或内存耗尽,你就会知道PHP用对了工具有多强大,下面小编就... 目录问题的本质php 中的数据流处理:为什么必不可少生成器:内存高效的迭代方式流量控制:避免系统过载一次性

MySQL的JDBC编程详解

《MySQL的JDBC编程详解》:本文主要介绍MySQL的JDBC编程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录前言一、前置知识1. 引入依赖2. 认识 url二、JDBC 操作流程1. JDBC 的写操作2. JDBC 的读操作总结前言本文介绍了mysq

Redis 的 SUBSCRIBE命令详解

《Redis的SUBSCRIBE命令详解》Redis的SUBSCRIBE命令用于订阅一个或多个频道,以便接收发送到这些频道的消息,本文给大家介绍Redis的SUBSCRIBE命令,感兴趣的朋友跟随... 目录基本语法工作原理示例消息格式相关命令python 示例Redis 的 SUBSCRIBE 命令用于订

使用Python批量将.ncm格式的音频文件转换为.mp3格式的实战详解

《使用Python批量将.ncm格式的音频文件转换为.mp3格式的实战详解》本文详细介绍了如何使用Python通过ncmdump工具批量将.ncm音频转换为.mp3的步骤,包括安装、配置ffmpeg环... 目录1. 前言2. 安装 ncmdump3. 实现 .ncm 转 .mp34. 执行过程5. 执行结

Python中 try / except / else / finally 异常处理方法详解

《Python中try/except/else/finally异常处理方法详解》:本文主要介绍Python中try/except/else/finally异常处理方法的相关资料,涵... 目录1. 基本结构2. 各部分的作用tryexceptelsefinally3. 执行流程总结4. 常见用法(1)多个e

SpringBoot日志级别与日志分组详解

《SpringBoot日志级别与日志分组详解》文章介绍了日志级别(ALL至OFF)及其作用,说明SpringBoot默认日志级别为INFO,可通过application.properties调整全局或... 目录日志级别1、级别内容2、调整日志级别调整默认日志级别调整指定类的日志级别项目开发过程中,利用日志