Redis缓存在项目中的使用升级

对于redis在项目中的使用与介绍这里就不展开描述,与进行基本的代码构造了。

这里针对redis的代码使用进行介绍

一,最简单阶段(一)--简单使用redis缓存

思想:拿数据先从缓存中拿去,如果缓存中没有再从数据库获取。

package com.luojie.test.catchs;

import com.luojie.dao.mapper2.Mapper2;
import com.luojie.util.RedisServiceUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

/**
 * 简单使用redis缓存
 */
@Component
@Slf4j
public class RedisCatch1 {

    @Autowired
    Mapper2 mapper2;

    @Autowired
    RedisServiceUtil redisUtil;
    public String test1() {
        // 先从redis中找数据,没有才去查数据库
        String value = redisUtil.get("uuid");
        if (StringUtils.isNotEmpty(value)) {
            return value;
        } else {
            String dbValue = mapper2.getuuid("uuid");
            log.info("get data form db dbValue:{}", dbValue);
            if (StringUtils.isNotEmpty(dbValue)) {
                // 缓存数据
                redisUtil.setWithExpire("uuid", dbValue, 3, TimeUnit.DAYS);
            }
            return dbValue;
        }
    }
}

缺点:

无法避免缓存穿透,缓存击穿,缓存雪崩,并发写入的问题。

简单介绍问题名词:

  • 缓存穿透: 多个线程同时请求一个缓存中没有的数据时,可能会导致多个线程同时去查询数据库,这会给数据库带来较大的负载。

  • 缓存雪崩: 当缓存中的某个热数据在同一时间失效时,大量请求会同时击中数据库,导致数据库压力骤增。

  • 缓存击穿: 当缓存中某个热数据失效的瞬间,大量请求直接击中数据库,导致数据库瞬间负载过大。

  • 并发写入: 多个线程同时执行redisUtil.set("key", dbValue);,可能会导致一些不必要的重复操作。

二,优化上一步(二)--解决redis缓存常见问题

思想:

1,空结果缓存:解决缓存穿透

2,设置(随机过期时间):解决缓存雪崩

3,加锁:解决缓存击穿与并发写入问题

package com.luojie.test.catchs;

import com.luojie.dao.mapper2.Mapper2;
import com.luojie.util.RedisServiceUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

/**
 * 简单使用redis缓存2
 */
@Component
@Slf4j
public class RedisCatch2 {

    @Autowired
    Mapper2 mapper2;

    @Autowired
    RedisServiceUtil redisUtil;

    public String test1() {
        // 先从redis中找数据,没有才去查数据库
        String value = redisUtil.get("uuid");
        if (StringUtils.isNotEmpty(value)) {
            return value;
        }
        synchronized (this) {
            // 双重检查,以防止在获取锁之后,缓存已经被其他线程更新
            value = redisUtil.get("uuid");
            if (StringUtils.isNotEmpty(value)) {
                return value;
            }
            // 缓存空结果的前提是该方法如果出错应抛出异常,而不是被catch后返回空
            String dbValue = mapper2.getuuid("uuid");
            log.info("get data form db dbValue:{}", dbValue);
            // 缓存数据
            redisUtil.setWithExpire("uuid", dbValue, 1 + (int) Math.ceil(Math.random() * 10), TimeUnit.DAYS);
            return dbValue;
        }

    }

}

缺点

锁的力度大,范围广。高并发环境下,可能重复获取锁

三,优化上一步(三)--使用最小粒度锁

思想:使用最小粒度锁。采用轮询的方式,避免锁被重复获取

package com.luojie.test.catchs;

import com.luojie.dao.mapper2.Mapper2;
import com.luojie.util.RedisServiceUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 简单使用redis缓存3
 */
@Component
@Slf4j
public class RedisCatch3 {

    @Autowired
    Mapper2 mapper2;

    @Autowired
    RedisServiceUtil redisUtil;

    Lock lock = new ReentrantLock(false);

    public String test1() throws InterruptedException {
        // 先从redis中找数据,没有才去查数据库
        String value = redisUtil.get("uuid");
        if (StringUtils.isNotEmpty(value)) {
            return value;
        }
        try {
            if (lock.tryLock(300, TimeUnit.MILLISECONDS)) {
                // 双重检查,以防止在获取锁之后,缓存已经被其他线程更新
                value = redisUtil.get("uuid");
                if (StringUtils.isNotEmpty(value)) {
                    return value;
                }
                // 缓存空结果的前提是该方法如果出错应抛出异常,而不是被catch后返回空
                String dbValue = mapper2.getuuid("uuid");
                log.info("get data form db dbValue:{}", dbValue);
                // 缓存数据
                redisUtil.setWithExpire("uuid", dbValue, 1 + (int) Math.ceil(Math.random() * 10), TimeUnit.DAYS);
                return dbValue;
            } else {
                // 如果未获取到锁,则等待一段时间再从缓存中获取数据
                Thread.sleep(100);
                return test1();
            }
        } finally {
            lock.unlock();
        }

    }

}

缺点:

  • 本地锁局限性: ReentrantLock 是 JVM 级别的锁,只能在单个应用实例内有效。如果应用部署在多个实例(比如微服务架构或分布式系统)中,本地锁无法保证全局唯一性。

  • 潜在的死锁风险: 在尝试获取锁和释放锁时,如果发生异常而未能正确释放锁,可能会导致死锁。

四,优化上一步(四)--使用分布式锁

思想:

在集群部署情况下保证同一时间只有一个请求能打到数据库获取数据。

package com.luojie.test.catchs;

import com.luojie.dao.mapper2.Mapper2;
import com.luojie.util.RedisServiceUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 简单使用redis缓存3
 */
@Component
@Slf4j
public class RedisCatch4 {

    @Autowired
    Mapper2 mapper2;

    @Autowired
    RedisServiceUtil redisUtil;

    public String test1() throws InterruptedException {
        // 先从redis中找数据,没有才去查数据库
        String value = redisUtil.get("uuid");
        if (StringUtils.isNotEmpty(value)) {
            return value;
        }
        try {
            if (redisUtil.setIfAbsent("lock", "doneLock")) {
                // 双重检查,以防止在获取锁之后,缓存已经被其他线程更新
                value = redisUtil.get("uuid");
                if (StringUtils.isNotEmpty(value)) {
                    return value;
                }
                // 缓存空结果的前提是该方法如果出错应抛出异常,而不是被catch后返回空
                String dbValue = mapper2.getuuid("uuid");
                log.info("get data form db dbValue:{}", dbValue);
                // 缓存数据
                redisUtil.setWithExpire("uuid", dbValue, 1 + (int) Math.ceil(Math.random() * 10), TimeUnit.DAYS);
                return dbValue;
            } else {
                // 如果未获取到锁,则等待一段时间再从缓存中获取数据
                Thread.sleep(100);
                // 自旋获取
                return test1();
            }
        } finally {
            // 删除锁
            redisUtil.delete("lock");
        }

    }

}

缺点:

若加锁成功但系统故障则会导致锁永久存在,进入死锁。

方法在未获取到锁时递归调用自身。如果长时间未获取到锁,这可能导致StackOverflowError。

五,优化上一步(五)--优化分布式锁

思想:

redis加锁增加超时时间已经独特vule设定--防止释放掉别人的锁

使用递归时增加次数循环次数判定

package com.luojie.test.catchs;

import com.luojie.dao.mapper2.Mapper2;
import com.luojie.util.RedisServiceUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * 简单使用redis缓存3
 */
@Component
@Slf4j
public class RedisCatch5 {

    @Autowired
    Mapper2 mapper2;

    @Autowired
    RedisServiceUtil redisUtil;

    private static final int MAX_RETRIES = 5;
    private static final long RETRY_DELAY_MS = 100;

    public String test1() throws InterruptedException {
        // 先从redis中找数据,没有才去查数据库
        String value = redisUtil.get("uuid");
        if (StringUtils.isNotEmpty(value)) {
            return value;
        }
        String uuid = UUID.randomUUID().toString();
        int retryCount = 0;
        boolean lockAcquired = false;
        // 循环代替迭代
        while (retryCount < MAX_RETRIES && !lockAcquired) {
            lockAcquired = redisUtil.setIfAbsentWithExpire("lock", uuid, 300, TimeUnit.SECONDS);
            if (!lockAcquired) {
                Thread.sleep(RETRY_DELAY_MS);
                ++retryCount;
            }
        }

        try {
            if (lockAcquired) {
                // 双重检查,以防止在获取锁之后,缓存已经被其他线程更新
                value = redisUtil.get("uuid");
                if (StringUtils.isNotEmpty(value)) {
                    return value;
                }
                // 缓存空结果的前提是该方法如果出错应抛出异常,而不是被catch后返回空
                String dbValue = mapper2.getuuid("uuid");
                log.info("get data form db dbValue:{}", dbValue);
                // 缓存数据
                redisUtil.setWithExpire("uuid", dbValue, 1 + (int) Math.ceil(Math.random() * 10), TimeUnit.DAYS);
                return dbValue;
            } else {
                // 如果未能获取到锁,重试从缓存获取数据
                return redisUtil.get("uuid");
            }
        } finally {
            // 获取锁值
            String lock = redisUtil.get("lock");
            if (StringUtils.isNotEmpty(lock) && lock.equals(uuid)) {
                // 删除锁
                redisUtil.delete("lock");
            }
        }
    }

}

缺点:

redis并没有跟服务进行脱钩,是强绑定的状态,当redis出现异常会导致服务也发生异常。

六,优化上一步(六) --最终 

思想:

对set,delete操作与主线程,主业务脱钩。使用多线程 + 监听器进行解耦操作

具体多线程 + 监听器的实现请参考:

SpringBoot使用Redis(事务异步add + 更新)_spring redis 异步处理-CSDN博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值