Redis缓存穿透、击穿、雪崩解决方案:分布式锁实现与多级缓存架构设计实战

蓝色幻想1
蓝色幻想1 2025-12-21T19:24:02+08:00
0 0 0

引言

在现代分布式系统中,Redis作为高性能的内存数据库,已成为缓存层的核心组件。然而,在实际应用过程中,缓存系统往往面临三大核心问题:缓存穿透、缓存击穿和缓存雪崩。这些问题不仅会影响系统的性能,还可能导致服务不可用,严重影响用户体验。

本文将深入分析这三种缓存问题的本质原因,详细介绍基于布隆过滤器、分布式锁和多级缓存架构的解决方案,并提供详细的代码实现和最佳实践指导,帮助开发者构建更加稳定可靠的缓存系统。

缓存三大核心问题解析

缓存穿透(Cache Penetration)

缓存穿透是指查询一个根本不存在的数据,由于缓存中没有该数据,会直接访问数据库。如果数据库中也不存在该数据,就会导致请求直接穿透到数据库层,造成大量无效请求。

典型场景:

  • 用户频繁查询一个不存在的ID
  • 恶意攻击者通过大量不存在的key进行攻击
  • 系统刚启动时,缓存为空,大量请求直接打到数据库

缓存击穿(Cache Breakdown)

缓存击穿是指某个热点数据在缓存中过期失效,此时大量并发请求同时访问该数据,导致这些请求全部穿透到数据库层。

典型场景:

  • 热点商品信息缓存过期
  • 系统启动后某个key被大量并发访问
  • 某些重要的配置信息缓存失效

缓存雪崩(Cache Avalanche)

缓存雪崩是指在某一时刻大量缓存数据同时失效,导致所有请求都直接打到数据库层,造成数据库压力剧增,甚至可能导致数据库宕机。

典型场景:

  • 大量缓存数据设置相同的过期时间
  • 系统大规模重启或维护
  • 缓存服务整体故障

布隆过滤器解决方案

布隆过滤器原理

布隆过滤器是一种概率型数据结构,通过多个哈希函数将元素映射到位数组中。它能够快速判断一个元素是否存在于集合中,但存在一定的误判率。

核心特点:

  • 空间效率高
  • 查询速度快
  • 存在假阳性(可能误判不存在的元素为存在)
  • 不支持删除操作

Redis布隆过滤器实现

@Component
public class BloomFilterService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    private static final String BLOOM_FILTER_KEY = "user_bloom_filter";
    private static final long FILTER_CAPACITY = 1000000L;
    private static final double ERROR_RATE = 0.01;
    
    /**
     * 初始化布隆过滤器
     */
    public void initBloomFilter() {
        String command = String.format(
            "BF.RESERVE %s %f %d", 
            BLOOM_FILTER_KEY, 
            ERROR_RATE, 
            FILTER_CAPACITY
        );
        
        redisTemplate.execute((RedisCallback<Object>) connection -> {
            return connection.execute("BF.RESERVE".getBytes(), 
                BLOOM_FILTER_KEY.getBytes(), 
                String.valueOf(ERROR_RATE).getBytes(), 
                String.valueOf(FILTER_CAPACITY).getBytes());
        });
    }
    
    /**
     * 添加元素到布隆过滤器
     */
    public void addElement(String element) {
        redisTemplate.execute((RedisCallback<Object>) connection -> {
            return connection.execute("BF.ADD".getBytes(), 
                BLOOM_FILTER_KEY.getBytes(), 
                element.getBytes());
        });
    }
    
    /**
     * 检查元素是否存在
     */
    public boolean contains(String element) {
        return redisTemplate.execute((RedisCallback<Boolean>) connection -> {
            Object result = connection.execute("BF.EXISTS".getBytes(), 
                BLOOM_FILTER_KEY.getBytes(), 
                element.getBytes());
            return (Boolean) result;
        });
    }
}

布隆过滤器在缓存穿透防护中的应用

@Service
public class UserService {
    
    @Autowired
    private UserDao userDao;
    
    @Autowired
    private BloomFilterService bloomFilterService;
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    private static final String USER_CACHE_KEY = "user:";
    private static final int CACHE_EXPIRE_TIME = 3600; // 1小时
    
    /**
     * 获取用户信息 - 布隆过滤器防护
     */
    public User getUserById(Long userId) {
        // 第一步:布隆过滤器检查
        if (!bloomFilterService.contains(String.valueOf(userId))) {
            return null;
        }
        
        // 第二步:缓存查询
        String cacheKey = USER_CACHE_KEY + userId;
        User user = (User) redisTemplate.opsForValue().get(cacheKey);
        
        if (user != null) {
            return user;
        }
        
        // 第三步:数据库查询
        user = userDao.findById(userId);
        
        if (user != null) {
            // 缓存到Redis,设置过期时间
            redisTemplate.opsForValue().set(cacheKey, user, CACHE_EXPIRE_TIME, TimeUnit.SECONDS);
            // 同时添加到布隆过滤器中
            bloomFilterService.addElement(String.valueOf(userId));
        } else {
            // 数据库不存在的key也缓存,避免重复查询
            redisTemplate.opsForValue().set(cacheKey, new User(), 
                300, TimeUnit.SECONDS); // 缓存5分钟
        }
        
        return user;
    }
}

分布式锁实现

分布式锁核心原理

分布式锁的核心思想是利用Redis的原子性操作来实现互斥访问。常用的实现方式包括:

  1. SETNX + EX命令组合:设置key并设置过期时间
  2. Redlock算法:基于多个Redis实例的更安全实现
  3. Lua脚本原子化执行:确保操作的原子性

基于Redis的分布式锁实现

@Component
public class RedisDistributedLock {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    private static final String LOCK_PREFIX = "lock:";
    private static final int DEFAULT_LOCK_TIMEOUT = 30000; // 30秒
    
    /**
     * 获取分布式锁
     */
    public boolean tryLock(String lockKey, String requestId, long expireTime) {
        String key = LOCK_PREFIX + lockKey;
        
        // 使用SETNX和EX组合,确保原子性
        String script = "if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then " +
                       "redis.call('pexpire', KEYS[1], ARGV[2]) return 1 else return 0 end";
        
        try {
            Long result = (Long) redisTemplate.execute(
                new DefaultRedisScript<>(script, Long.class),
                Collections.singletonList(key),
                requestId,
                String.valueOf(expireTime)
            );
            
            return result != null && result == 1;
        } catch (Exception e) {
            log.error("获取分布式锁异常", e);
            return false;
        }
    }
    
    /**
     * 释放分布式锁
     */
    public boolean releaseLock(String lockKey, String requestId) {
        String key = LOCK_PREFIX + lockKey;
        
        // 使用Lua脚本确保原子性释放锁
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                       "return redis.call('del', KEYS[1]) else return 0 end";
        
        try {
            Long result = (Long) redisTemplate.execute(
                new DefaultRedisScript<>(script, Long.class),
                Collections.singletonList(key),
                requestId
            );
            
            return result != null && result == 1;
        } catch (Exception e) {
            log.error("释放分布式锁异常", e);
            return false;
        }
    }
    
    /**
     * 带重试机制的获取锁
     */
    public boolean tryLockWithRetry(String lockKey, String requestId, 
                                  long expireTime, int retryTimes, long retryInterval) {
        for (int i = 0; i < retryTimes; i++) {
            if (tryLock(lockKey, requestId, expireTime)) {
                return true;
            }
            
            try {
                Thread.sleep(retryInterval);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return false;
            }
        }
        
        return false;
    }
}

缓存击穿防护中的分布式锁应用

@Service
public class ProductService {
    
    @Autowired
    private ProductDao productDao;
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private RedisDistributedLock distributedLock;
    
    private static final String PRODUCT_CACHE_KEY = "product:";
    private static final String LOCK_KEY = "product_lock:";
    private static final int CACHE_EXPIRE_TIME = 3600;
    private static final int LOCK_TIMEOUT = 5000; // 5秒
    private static final int MAX_RETRY_TIMES = 3;
    
    /**
     * 获取商品信息 - 分布式锁防护缓存击穿
     */
    public Product getProductById(Long productId) {
        String cacheKey = PRODUCT_CACHE_KEY + productId;
        String lockKey = LOCK_KEY + productId;
        String requestId = UUID.randomUUID().toString();
        
        // 第一步:从缓存获取数据
        Product product = (Product) redisTemplate.opsForValue().get(cacheKey);
        if (product != null) {
            return product;
        }
        
        // 第二步:尝试获取分布式锁
        if (distributedLock.tryLockWithRetry(lockKey, requestId, 
                                           LOCK_TIMEOUT, MAX_RETRY_TIMES, 100)) {
            try {
                // 再次检查缓存(双重检查)
                product = (Product) redisTemplate.opsForValue().get(cacheKey);
                if (product != null) {
                    return product;
                }
                
                // 第三步:数据库查询
                product = productDao.findById(productId);
                
                if (product != null) {
                    // 缓存到Redis
                    redisTemplate.opsForValue().set(cacheKey, product, 
                        CACHE_EXPIRE_TIME, TimeUnit.SECONDS);
                } else {
                    // 缓存空值,避免缓存穿透
                    redisTemplate.opsForValue().set(cacheKey, new Product(), 
                        300, TimeUnit.SECONDS);
                }
                
                return product;
            } finally {
                // 释放锁
                distributedLock.releaseLock(lockKey, requestId);
            }
        } else {
            // 获取锁失败,等待一段时间后重试
            try {
                Thread.sleep(100);
                return getProductById(productId);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return null;
            }
        }
    }
}

多级缓存架构设计

多级缓存架构原理

多级缓存是指在应用层、本地缓存和分布式缓存之间构建多层次的缓存体系,通过合理的数据分层和缓存策略,提升系统的整体性能和稳定性。

典型架构层次:

  1. 本地缓存层:应用进程内的缓存(如Caffeine)
  2. 分布式缓存层:Redis等远程缓存
  3. 数据源层:数据库、文件系统等

多级缓存实现方案

@Component
public class MultiLevelCacheService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    // 本地缓存使用Caffeine
    private final Cache<String, Object> localCache = Caffeine.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(30, TimeUnit.MINUTES)
            .build();
    
    private static final String CACHE_KEY_PREFIX = "multi_cache:";
    private static final int LOCAL_CACHE_EXPIRE_TIME = 1800; // 30分钟
    private static final int REDIS_CACHE_EXPIRE_TIME = 3600; // 1小时
    
    /**
     * 多级缓存获取数据
     */
    public Object getData(String key) {
        String cacheKey = CACHE_KEY_PREFIX + key;
        
        // 第一级:本地缓存
        Object localData = localCache.getIfPresent(cacheKey);
        if (localData != null) {
            return localData;
        }
        
        // 第二级:Redis缓存
        Object redisData = redisTemplate.opsForValue().get(cacheKey);
        if (redisData != null) {
            // 同步到本地缓存
            localCache.put(cacheKey, redisData);
            return redisData;
        }
        
        return null;
    }
    
    /**
     * 多级缓存设置数据
     */
    public void setData(String key, Object value) {
        String cacheKey = CACHE_KEY_PREFIX + key;
        
        // 设置本地缓存
        localCache.put(cacheKey, value);
        
        // 设置Redis缓存
        redisTemplate.opsForValue().set(cacheKey, value, REDIS_CACHE_EXPIRE_TIME, TimeUnit.SECONDS);
    }
    
    /**
     * 多级缓存删除数据
     */
    public void deleteData(String key) {
        String cacheKey = CACHE_KEY_PREFIX + key;
        
        // 删除本地缓存
        localCache.invalidate(cacheKey);
        
        // 删除Redis缓存
        redisTemplate.delete(cacheKey);
    }
    
    /**
     * 批量设置多级缓存
     */
    public void batchSetData(Map<String, Object> dataMap) {
        Map<String, Object> localData = new HashMap<>();
        
        for (Map.Entry<String, Object> entry : dataMap.entrySet()) {
            String key = CACHE_KEY_PREFIX + entry.getKey();
            Object value = entry.getValue();
            
            // 设置本地缓存
            localCache.put(key, value);
            localData.put(key, value);
            
            // 设置Redis缓存
            redisTemplate.opsForValue().set(key, value, REDIS_CACHE_EXPIRE_TIME, TimeUnit.SECONDS);
        }
    }
}

多级缓存与缓存更新策略

@Service
public class CacheUpdateService {
    
    @Autowired
    private MultiLevelCacheService multiLevelCacheService;
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    private static final String CACHE_UPDATE_KEY = "cache_update:";
    
    /**
     * 延迟双删策略 - 防止缓存不一致
     */
    public void updateDataWithDelayDelete(String key, Object newValue) {
        String cacheKey = CACHE_UPDATE_KEY + key;
        
        // 第一步:更新数据库
        boolean updateSuccess = updateDatabase(key, newValue);
        if (!updateSuccess) {
            return;
        }
        
        // 第二步:删除本地缓存
        multiLevelCacheService.deleteData(key);
        
        // 第三步:延迟删除Redis缓存(防止读写冲突)
        CompletableFuture.delayedExecutor(100, TimeUnit.MILLISECONDS)
            .execute(() -> {
                redisTemplate.delete(cacheKey);
            });
        
        // 第四步:重新设置新数据到缓存
        multiLevelCacheService.setData(key, newValue);
    }
    
    /**
     * 读写分离策略
     */
    public Object readDataWithWriteBack(String key) {
        // 先从本地缓存读取
        Object data = multiLevelCacheService.getData(key);
        if (data != null) {
            return data;
        }
        
        // 从Redis读取
        String redisKey = CACHE_UPDATE_KEY + key;
        data = redisTemplate.opsForValue().get(redisKey);
        if (data != null) {
            // 同步到本地缓存
            multiLevelCacheService.setData(key, data);
            return data;
        }
        
        // 从数据库读取并写入缓存
        data = readFromDatabase(key);
        if (data != null) {
            multiLevelCacheService.setData(key, data);
        }
        
        return data;
    }
    
    /**
     * 异步更新缓存
     */
    public void asyncUpdateCache(String key, Object newValue) {
        CompletableFuture.runAsync(() -> {
            try {
                // 更新数据库
                updateDatabase(key, newValue);
                
                // 更新缓存
                multiLevelCacheService.setData(key, newValue);
                
                log.info("异步更新缓存成功: {}", key);
            } catch (Exception e) {
                log.error("异步更新缓存失败: {}", key, e);
            }
        });
    }
    
    private boolean updateDatabase(String key, Object value) {
        // 实际的数据库更新逻辑
        return true;
    }
    
    private Object readFromDatabase(String key) {
        // 实际的数据库读取逻辑
        return null;
    }
}

综合解决方案实战

完整的缓存防护系统

@Component
public class ComprehensiveCacheService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private BloomFilterService bloomFilterService;
    
    @Autowired
    private RedisDistributedLock distributedLock;
    
    @Autowired
    private MultiLevelCacheService multiLevelCacheService;
    
    private static final String USER_DATA_KEY = "user_data:";
    private static final String USER_LOCK_KEY = "user_lock:";
    private static final int CACHE_EXPIRE_TIME = 3600;
    private static final int LOCK_TIMEOUT = 5000;
    private static final int MAX_RETRY_TIMES = 3;
    
    /**
     * 综合缓存防护获取用户数据
     */
    public User getUserData(Long userId) {
        String cacheKey = USER_DATA_KEY + userId;
        String lockKey = USER_LOCK_KEY + userId;
        String requestId = UUID.randomUUID().toString();
        
        // 1. 布隆过滤器检查(防止缓存穿透)
        if (!bloomFilterService.contains(String.valueOf(userId))) {
            return null;
        }
        
        // 2. 多级缓存读取
        User user = (User) multiLevelCacheService.getData(cacheKey);
        if (user != null && !isEmptyUser(user)) {
            return user;
        }
        
        // 3. 分布式锁获取数据(防止缓存击穿)
        if (distributedLock.tryLockWithRetry(lockKey, requestId, 
                                           LOCK_TIMEOUT, MAX_RETRY_TIMES, 100)) {
            try {
                // 双重检查
                user = (User) multiLevelCacheService.getData(cacheKey);
                if (user != null && !isEmptyUser(user)) {
                    return user;
                }
                
                // 4. 数据库查询
                User dbUser = findUserFromDatabase(userId);
                if (dbUser != null) {
                    // 5. 缓存设置
                    multiLevelCacheService.setData(cacheKey, dbUser);
                    bloomFilterService.addElement(String.valueOf(userId));
                    return dbUser;
                } else {
                    // 6. 空值缓存
                    User emptyUser = new User();
                    multiLevelCacheService.setData(cacheKey, emptyUser);
                    return null;
                }
            } finally {
                distributedLock.releaseLock(lockKey, requestId);
            }
        } else {
            // 获取锁失败,等待后重试
            try {
                Thread.sleep(100);
                return getUserData(userId);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return null;
            }
        }
    }
    
    /**
     * 批量获取用户数据
     */
    public List<User> batchGetUserData(List<Long> userIds) {
        List<User> results = new ArrayList<>();
        
        // 使用布隆过滤器预过滤
        List<Long> validUserIds = new ArrayList<>();
        for (Long userId : userIds) {
            if (bloomFilterService.contains(String.valueOf(userId))) {
                validUserIds.add(userId);
            }
        }
        
        // 并发获取数据
        List<CompletableFuture<User>> futures = validUserIds.stream()
            .map(this::getUserDataAsync)
            .collect(Collectors.toList());
        
        for (CompletableFuture<User> future : futures) {
            try {
                User user = future.get(5, TimeUnit.SECONDS);
                if (user != null) {
                    results.add(user);
                }
            } catch (Exception e) {
                log.error("批量获取用户数据异常", e);
            }
        }
        
        return results;
    }
    
    private CompletableFuture<User> getUserDataAsync(Long userId) {
        return CompletableFuture.supplyAsync(() -> getUserData(userId));
    }
    
    private User findUserFromDatabase(Long userId) {
        // 实际的数据库查询逻辑
        return null;
    }
    
    private boolean isEmptyUser(User user) {
        return user == null || (user.getId() == null && user.getName() == null);
    }
}

性能优化与监控

缓存命中率监控

@Component
public class CacheMonitor {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    private final MeterRegistry meterRegistry;
    private final Counter cacheHitCounter;
    private final Counter cacheMissCounter;
    private final Timer cacheTimer;
    
    public CacheMonitor(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        
        this.cacheHitCounter = Counter.builder("cache.hit")
            .description("缓存命中次数")
            .register(meterRegistry);
            
        this.cacheMissCounter = Counter.builder("cache.miss")
            .description("缓存未命中次数")
            .register(meterRegistry);
            
        this.cacheTimer = Timer.builder("cache.response.time")
            .description("缓存响应时间")
            .register(meterRegistry);
    }
    
    /**
     * 记录缓存命中
     */
    public void recordCacheHit() {
        cacheHitCounter.increment();
    }
    
    /**
     * 记录缓存未命中
     */
    public void recordCacheMiss() {
        cacheMissCounter.increment();
    }
    
    /**
     * 记录缓存响应时间
     */
    public void recordCacheTime(long timeMs) {
        cacheTimer.record(timeMs, TimeUnit.MILLISECONDS);
    }
    
    /**
     * 获取缓存统计信息
     */
    public Map<String, Object> getCacheStats() {
        Map<String, Object> stats = new HashMap<>();
        
        // 获取Redis基本信息
        String info = redisTemplate.execute((RedisCallback<String>) connection -> 
            connection.info().toString());
        
        stats.put("redis_info", info);
        stats.put("cache_hit_count", cacheHitCounter.count());
        stats.put("cache_miss_count", cacheMissCounter.count());
        
        return stats;
    }
}

最佳实践总结

1. 缓存设计原则

  • 合理设置过期时间:避免缓存雪崩,使用随机过期时间
  • 预热机制:系统启动时预先加载热点数据
  • 缓存穿透防护:使用布隆过滤器或空值缓存
  • 缓存击穿防护:使用分布式锁或双检锁

2. 性能优化建议

@Configuration
public class CacheConfig {
    
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        
        // 设置序列化器
        Jackson2JsonRedisSerializer<Object> serializer = 
            new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.activateDefaultTyping(LazyCollectionResolver.instance, 
            ObjectMapper.DefaultTyping.NON_FINAL);
        serializer.setObjectMapper(objectMapper);
        
        template.setDefaultSerializer(serializer);
        template.afterPropertiesSet();
        
        return template;
    }
    
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofHours(1))
            .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(
                new StringRedisSerializer()))
            .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(
                new GenericJackson2JsonRedisSerializer()));
        
        return RedisCacheManager.builder(connectionFactory)
            .withInitialCacheConfigurations(Collections.singletonMap(
                "default", config))
            .build();
    }
}

3. 监控告警机制

@Component
public class CacheHealthCheck {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Scheduled(fixedRate = 30000) // 每30秒检查一次
    public void checkCacheHealth() {
        try {
            // 检查Redis连接状态
            String pingResult = redisTemplate.ping();
            if (!"PONG".equals(pingResult)) {
                log.warn("Redis连接异常: {}", pingResult);
                // 发送告警通知
                sendAlert("Redis连接异常");
            }
            
            // 检查内存使用情况
            String info = redisTemplate.execute((RedisCallback<String>) connection -> 
                connection.info().toString());
            
            if (info.contains("used_memory_human:")) {
                // 解析内存使用率
                double memoryUsage = parseMemoryUsage(info);
                if (memoryUsage > 0.8) {
                    log.warn("Redis内存使用率过高: {}%", memoryUsage * 100);
                    sendAlert("Redis内存使用率过高");
                }
            }
        } catch (Exception e) {
            log.error("缓存健康检查异常", e);
        }
    }
    
    private double parseMemoryUsage(String info) {
        // 解析Redis信息中的内存使用率
        return 0.75; // 示例值
    }
    
    private void sendAlert(String message) {
        // 实现告警通知逻辑
        log.warn("发送告警: {}", message);
    }
}

结论

通过本文的深入分析和实践方案,我们可以看到Redis缓存系统的三大核心问题都有相应的解决方案:

  1. 缓存穿透:通过布隆过滤器实现快速判断,避免无效请求穿透到数据库层
  2. 缓存击穿:使用分布式锁确保同一时间只有一个线程访问数据库,防止热点数据失效时的并发冲击
  3. 缓存雪崩:通过合理的过期时间设置和多级缓存架构,避免大量缓存同时失效

在实际应用中,建议根据具体的业务场景选择合适的防护策略,并结合监控告警机制,确保系统的稳定性和高性能。同时,多级缓存架构的设计能够进一步提升系统的整体性能,为用户提供更好的服务体验。

通过合理的缓存设计和优化,我们不仅能够显著提升系统的响应速度,还能够有效降低数据库的访问压力,实现高并发场景下的系统稳定运行。

相关推荐
广告位招租

相似文章

    评论 (0)

    0/2000