redis 数据库避免击穿、雪崩和过载说明和代码示例

为了避免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的击穿和雪崩问题。你可以根据实际需求进行调整和优化。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

慧香一格

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值