为了避免Redis的击穿和雪崩现象,可以采取以下措施:
避免击穿
使用缓存永不过期策略:对于一些几乎不变的数据,设置为永不过期。但要注意内存占用问题。
热点数据预热:在高并发场景下,提前将热点数据加载到缓存中。
布隆过滤器:在缓存和DB之前加一层布隆过滤器,快速判断数据是否存在,减少对Redis的无效查询。
避免雪崩
设置随机过期时间:不要让大量缓存在同一时刻过期,给缓存key设置一个随机的过期时间范围。
限流降级:当检测到系统压力过大时,通过限流或服务降级来保护系统。
多级缓存架构:采用本地缓存(如Guava Cache、Caffeine)+ 分布式缓存(如Redis)的方式,增加系统的容错性。
异步更新缓存:当缓存失效时,不是立即从数据库读取并写入缓存,而是异步地去更新缓存,防止大量请求直接打到数据库上。
避免过载
限流是一种有效的手段来防止系统过载,特别是在高并发场景下。常见的限流算法包括令牌桶算法、漏桶算法和计数器算法。下面我将展示如何使用令牌桶算法来实现限流。
令牌桶算法实现
令牌桶算法是一种常用的限流算法,它以固定的速率向桶中添加令牌,请求到达时需要从桶中获取令牌才能被处理。如果桶中没有足够的令牌,则请求被拒绝。
使用Redis实现令牌桶算法
Redis提供了CL.THROTTLE命令,可以方便地实现令牌桶算法。不过,Redis本身并没有内置的CL.THROTTLE命令,我们可以使用Lua脚本来实现类似的功能。
以下是一个使用Lua脚本在Redis中实现令牌桶算法的示例:
Lua脚本实现令牌桶算法
Lua
-- 令牌桶算法 Lua 脚本
local key = KEYS[1]
local max_tokens = tonumber(ARGV[1])
local refill_rate = tonumber(ARGV[2])
local current_time = tonumber(ARGV[3])
local request_cost = tonumber(ARGV[4])
-- 获取当前令牌数量和上次填充时间
local tokens, last_refill = redis.call("HMGET", key, "tokens", "last_refill")
tokens = tokens and tonumber(tokens) or max_tokens
last_refill = last_refill and tonumber(last_refill) or current_time
-- 计算自上次填充以来的时间
local elapsed = current_time - last_refill
-- 计算可以添加的令牌数量
local tokens_to_add = elapsed * refill_rate
tokens = math.min(tokens + tokens_to_add, max_tokens)
-- 更新上次填充时间
redis.call("HSET", key, "last_refill", current_time)
-- 检查是否有足够的令牌
if tokens >= request_cost then
tokens = tokens - request_cost
redis.call("HSET", key, "tokens", tokens)
return {1, tokens, 0, 0} -- 允许请求
else
local time_to_wait = (request_cost - tokens) / refill_rate
return {0, tokens, 0, time_to_wait} -- 拒绝请求
end
java 调用Lua
java
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.params.SetParams;
import java.util.Arrays;
import java.util.List;
public class RateLimiter {
private static final JedisPool jedisPool = new JedisPool(new JedisPoolConfig(), "localhost");
private static final String LUA_SCRIPT = "local key = KEYS[1]\n" +
"local max_tokens = tonumber(ARGV[1])\n" +
"local refill_rate = tonumber(ARGV[2])\n" +
"local current_time = tonumber(ARGV[3])\n" +
"local request_cost = tonumber(ARGV[4])\n" +
"\n" +
"local tokens, last_refill = redis.call(\"HMGET\", key, \"tokens\", \"last_refill\")\n" +
"tokens = tokens and tonumber(tokens) or max_tokens\n" +
"last_refill = last_refill and tonumber(last_refill) or current_time\n" +
"\n" +
"local elapsed = current_time - last_refill\n" +
"local tokens_to_add = elapsed * refill_rate\n" +
"tokens = math.min(tokens + tokens_to_add, max_tokens)\n" +
"\n" +
"redis.call(\"HSET\", key, \"last_refill\", current_time)\n" +
"\n" +
"if tokens >= request_cost then\n" +
" tokens = tokens - request_cost\n" +
" redis.call(\"HSET\", key, \"tokens\", tokens)\n" +
" return {1, tokens, 0, 0} -- 允许请求\n" +
"else\n" +
" local time_to_wait = (request_cost - tokens) / refill_rate\n" +
" return {0, tokens, 0, time_to_wait} -- 拒绝请求\n" +
"end";
public static boolean allowRequest(String userId, int maxTokens, double refillRate, int requestCost) {
try (Jedis jedis = jedisPool.getResource()) {
long currentTime = System.currentTimeMillis() / 1000; // 当前时间戳(秒)
List<String> keys = Arrays.asList("rate_limit:" + userId);
List<String> args = Arrays.asList(String.valueOf(maxTokens), String.valueOf(refillRate), String.valueOf(currentTime), String.valueOf(requestCost));
List<Object> result = jedis.eval(LUA_SCRIPT, keys, args);
int allowed = (int) result.get(0);
return allowed == 1;
}
}
public static void main(String[] args) {
String userId = "user:1000";
int maxTokens = 10; // 最大令牌数
double refillRate = 1.0; // 每秒填充的令牌数
int requestCost = 1; // 每个请求消耗的令牌数
for (int i = 0; i < 15; i++) {
boolean allowed = allowRequest(userId, maxTokens, refillRate, requestCost);
if (allowed) {
System.out.println("Request " + (i + 1) + " allowed.");
} else {
System.out.println("Request " + (i + 1) + " denied.");
}
try {
Thread.sleep(100); // 模拟请求间隔
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
解释
Lua脚本:
max_tokens:令牌桶的最大容量。
refill_rate:每秒填充的令牌数。
current_time:当前时间戳(秒)。
request_cost:每个请求消耗的令牌数。
脚本会计算自上次填充以来的时间,并根据填充速率添加令牌。
如果令牌足够,则允许请求并消耗相应令牌;否则,拒绝请求并返回需要等待的时间。
Java代码:
allowRequest方法使用Jedis连接池连接到Redis,并执行Lua脚本。
userId:用户标识符,用于区分不同的限流桶。
maxTokens:令牌桶的最大容量。
refillRate:每秒填充的令牌数。
requestCost:每个请求消耗的令牌数。
main方法模拟了15个请求,每个请求间隔100毫秒。
通过这种方式,你可以有效地实现限流,防止系统过载。你可以根据实际需求调整maxTokens、refillRate和requestCost的值。
java代码示例
1. 设置随机过期时间
为了防止缓存在同一时刻过期,可以为缓存设置一个随机的过期时间范围。
java
import redis.clients.jedis.Jedis;
import java.util.Random;
public class RedisCacheUtil {
private static final Jedis jedis = new Jedis("localhost");
private static final Random random = new Random();
public static void setWithRandomExpire(String key, String value, int baseExpireTime, int randomRange) {
int expireTime = baseExpireTime + random.nextInt(randomRange);
jedis.setex(key, expireTime, value);
}
public static String get(String key) {
return jedis.get(key);
}
public static void main(String[] args) {
String key = "user:1000";
String value = "user_info";
int baseExpireTime = 3600; // 1 hour
int randomRange = 3600; // 1 hour
setWithRandomExpire(key, value, baseExpireTime, randomRange);
System.out.println("Cached value: " + get(key));
}
}
2. 使用布隆过滤器
布隆过滤器可以用来快速判断一个元素是否在一个集合中,减少对Redis的无效查询。
java
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
public class BloomFilterUtil {
private static final BloomFilter<String> bloomFilter = BloomFilter.create(
Funnels.stringFunnel(java.nio.charset.StandardCharsets.UTF_8),
1000000, // 预期插入的元素数量
0.01 // 误判率
);
public static void add(String key) {
bloomFilter.put(key);
}
public static boolean mightContain(String key) {
return bloomFilter.mightContain(key);
}
public static void main(String[] args) {
String key = "user:1000";
add(key);
if (mightContain(key)) {
System.out.println("Key might exist in the cache.");
} else {
System.out.println("Key definitely does not exist in the cache.");
}
}
}
3. 异步更新缓存
当缓存失效时,异步地去更新缓存,防止大量请求直接打到数据库上。
java
import redis.clients.jedis.Jedis;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class AsyncCacheUpdateUtil {
private static final Jedis jedis = new Jedis("localhost");
private static final ExecutorService executorService = Executors.newFixedThreadPool(10);
public static String get(String key) {
String value = jedis.get(key);
if (value == null) {
// 异步更新缓存
executorService.submit(() -> {
String newValue = fetchDataFromDB(key);
jedis.setex(key, 3600, newValue); // 设置缓存,有效期1小时
});
// 返回默认值或者空值
return null;
}
return value;
}
private static String fetchDataFromDB(String key) {
// 模拟从数据库获取数据
return "data_from_db";
}
public static void main(String[] args) {
String key = "user:1000";
String value = get(key);
if (value != null) {
System.out.println("Cached value: " + value);
} else {
System.out.println("Value not in cache, fetching asynchronously.");
}
}
}
以上代码示例展示了如何通过设置随机过期时间、使用布隆过滤器和异步更新缓存来避免Redis的击穿和雪崩问题。你可以根据实际需求进行调整和优化。