高并发redis缓存问题- 穿透,雪崩,击穿

缓存穿透

       概念:指查询一个一定不存在的数据,由于缓存是不命中的,将去查询数据库,但是数据库也无此记录,我们没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都要去数据库查询,失去缓存的意义!

       风险:利用不存在的数据进行攻击,数据库瞬间压力增大,最终导致崩溃。

       解决:null结果缓存,并加如短暂过期时间。

缓存雪崩:(开发中一般不会出现)

       概念:是指在我们设置缓存时,key值采用相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬间压力过重雪崩。

       解决:原有的失效时间基础上增加一个随机,比如1-5分组随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

缓存击穿

       概念:对于一些设置了过期时间的key,如果这些key可能会在某些时间点被高并发地访问,是一种非常“热点”的数据。如果这个key在大量请求同时进来的时候刚好失效,那么所有对这个key的数据查询都转义到DB上,DB就会压力过大,我们称为击穿。

       解决:加锁,大量数据并发只让一个去查询,其他人等待,直到以后释放锁,其他人才能获取到锁,先查缓存,就会有数据,就不用去查询DB。

说明:缓存穿透,缓存雪崩只做简单解释,解决方案已经在上述中说明,相对简单本篇文章只对缓存击穿提供对应的解决方案和代码使用,根据适用场景提供了三种方案

方案1.可以使用本地锁,这种只对单体项目或者是对数据要求不太高的负载项目,即单体项目一把锁(只设置一次redis),负载项目每个负载工程一把锁(负载下的每个工程一把锁,n个工程n把锁,设置n次redis)


  org.springframework.boot
  spring-boot-starter-data-redis
  2.1.7.RELEASE
spring.redis.host=127.0.0.1
spring.redis.port=6379

控制层(controller):

/**
 * 查询商品
 * @return {@link String}
 * @throws
 * @author 李庆伟
 * @date 2021/1/5 16:59
 */
@PostMapping("getCommoditysDemoOne")
public String getCommoditysDemoOne(){
    String val = commoditysService.getCommoditysDemoOne();
    if(StringUtils.isNotEmpty(val)){
        return "ok";
    }
    return "error";
}

接口(service):

/**
 * 查询商品
 * @return {@link String}
 * @throws
 * @author 李庆伟
 * @date 2021/1/5 16:59
 */
String getCommoditysDemoOne();

实现(impl):

@Autowired
StringRedisTemplate stringRedisTemplate;
/**
 * 查询商品  锁中的this是当前实例的意思
 * @return {@link String}
 * @throws
 * @author 李庆伟
 * @date 2021/1/5 16:59
 */
public String  getCommoditysDemoOne() {
    String val = stringRedisTemplate.opsForValue().get("commoditys");
    if(StringUtils.isEmpty(val)){
        synchronized (this){
            val = stringRedisTemplate.opsForValue().get("commoditys");
            if(StringUtils.isNotEmpty(val)){
                return val;
            }
            //模拟商品数据开始
            List> list = new ArrayList>();
            for(int i = 0; i<200 ; i++){
                Map map = new HashMap();
                map.put("id", UUID.randomUUID().toString().replace("-",""));
                map.put("commoditysCode","代号_"+UUID.randomUUID().toString().replace("-",""));
                list.add(map);
            }
            //模拟商品数据完成
            System.out.println(Thread.currentThread().getId()+"\t  进来了");
            stringRedisTemplate.opsForValue().set("commoditys",JSON.toJSONString(list));
        }
        return stringRedisTemplate.opsForValue().get("commoditys");
    }else {
        log.info("完成查询   "+Thread.currentThread().getId());
        return val;
    }
}

方案2.可以使用分布式锁,它能解决单体项目或者分布式项目,适用于高数据要求负载项目,即负载项目所有负载工程抢占一把锁(只设置一次redis)[这种分布式锁不推荐使用]


  org.springframework.boot
  spring-boot-starter-data-redis
  2.1.7.RELEASE

控制层(controller):

/**
 * 查询商品
 * @return {@link String}
 * @throws
 * @author 李庆伟
 * @date 2021/1/5 16:59
 */
@PostMapping("getCommoditysDemoTwo")
public String getCommoditysDemoTwo(){
    String val = commoditysService.getCommoditysDemoTwo();
    if(StringUtils.isNotEmpty(val)){
        return "ok";
    }
    return "error";
}

接口(service):

/**
 * 查询商品
 * @return {@link String}
 * @throws
 * @author 李庆伟
 * @date 2021/1/5 16:59
 */
String getCommoditysDemoTwo();

实现(impl):

@Autowired
StringRedisTemplate stringRedisTemplate;
/**
 * 查询商品
 * @return {@link String}
 * @throws
 * @author 李庆伟
 * @date 2021/1/5 16:59
 */
public String getCommoditysDemoTwo() {
    String val = stringRedisTemplate.opsForValue().get("hello");
    if(StringUtils.isEmpty(val)){
        //1.抢占分布式锁 去redis占, setIfAbsent true表示占用redis锁成功
        String uuid = UUID.randomUUID().toString().replace("-","");
        //Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock",uuid);//没有设置超时时间
        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock",uuid,300,TimeUnit.SECONDS);//
        if(lock){
            log.info("线程抢占锁成功,进来了。。。。。。");
            //2.给锁设置过期时间
            stringRedisTemplate.expire("lock",20, TimeUnit.SECONDS);
            //占锁成功,业务数据放入redis
            try {
                stringRedisTemplate.opsForValue().set("hello","world");//业务数据
            } finally {
                // 问题思考A:a.如果我们在执行业务代码的时候直接抛异常了,还没走下面的删除锁操作,会出现什么问题
                //            b.如果我们在执行完代码,还没有走下面的删除锁操作,会出现什么问题
                // A问题解决:给锁设置一个自动过期时间,即使中途出现了问题,过期时间到后还是会自动删除锁
                //            给锁设置过期时间方法:stringRedisTemplate.expire("lock",30, TimeUnit.SECONDS);
                // 问题思考B:a.如果我们在刚拿到锁的时候,还没来的及设置锁的过期时间就断电了,有怎么办
                //
                // B问题解决:抢占锁和设置锁过期时间必须是同步完成,具有原子性
                //            Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock",uuid,300,TimeUnit.SECONDS);
                //        注:有时候setIfAbsent只能设置key value,不能设置超时时间,那是因为可以设置超时时间的必须要求
                //            pring-boot-starter-data-redis为2.1版本以上
                //删除锁(不合理的删除锁)
                //String lockVal = stringRedisTemplate.opsForValue().get("lock");//不合理的删除锁
                //if(lockVal != null && uuid.equals(lockVal)){//不合理的删除锁
                // stringRedisTemplate.delete("lock");//不合理的删除锁
                //}//不合理的删除锁
                // 问题思考C:a.删除锁问题,如果我们设置锁超时时间为20秒,结果我们的业务代码处理超过了20秒,这个时候锁已过期,
                //            其他线程又会抢占锁,我们在删除锁的时候就会删除的不是自己的锁
                //            Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock",uuid,300,TimeUnit.SECONDS);
                //        注:有时候setIfAbsent只能设置key value,不能设置超时时间,那是因为可以设置超时时间的必须要求
                //            pring-boot-starter-data-redis为2.1版本以上
                //            b.删除锁问题,假如我们设置锁超时时间为20秒,结果我们的业务代码处理超过了18秒,我们查询锁释放存在用了2秒,
                //            返回结果刚好锁是自己的锁,我们这个时候在去删除锁,我们在删除锁的过程中,锁已经失效了,其他的线程有生产了新的锁,
                //            这样我们还是会把别人的锁删除掉
                // C问题解决: a.我们在删除锁的时候先去查询一下,看是不是自己创建的锁,如果是就删除,如果不是就不删除
                //            b.所以要处理a b问题,必须使 获取对比值和删除锁为原子操作
                //删除锁(正确的删除锁方法)
                String str = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
                stringRedisTemplate.execute(new DefaultRedisScript(str,Long.class),Arrays.asList("lock"),uuid);
            }
            return stringRedisTemplate.opsForValue().get("hello");
        } else {
            log.info("线程抢占锁失败,。。。。。。");

            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //占锁失败 循环等待
            return getCommoditysDemoTwo();
        }
    }else {
        log.info("完成查询   "+Thread.currentThread().getId());
        return val;
    }
}

方案3.可以使用分布式锁,它能解决单体项目或者分布式项目,适用于高数据要求负载项目,即负载项目所有负载工程抢占一把锁(只设置一次redis)[推荐使用]


  org.springframework.boot
  spring-boot-starter-data-redis
  2.1.7.RELEASE

  org.redisson
  redisson
  3.14.1

配置类:

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author 李庆伟
 * @date 2021/1/11 11:06
 */
@Configuration
public class RedissonConfig {

    /*
     * 本机地址可以直接写下面两行代码
     * 默认连接地址 127.0.0.1:6379
     * RedissonClient redisson = Redisson.create();
     * return redisson;
     */
    @Bean
    public RedissonClient redissonClient(){
        //是不是本机地址都可以这样配置(非集群版配置)
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        RedissonClient redisson = Redisson.create(config);
        return redisson;
    }
}

控制层(controller):

/**
 * @return {@link String}
 * @throws
 * @author 李庆伟
 * @date 2021/1/5 16:59
 */
@GetMapping("getCommoditysDemoThr")
public String getCommoditysDemoThr(){
    String val = commoditysService.getCommoditysDemoThr();
    if(StringUtils.isNotEmpty(val)){
        return "ok";
    }
    return "error";
}

接口(service):

/**
 * @return {@link String}
 * @throws
 * @author 李庆伟
 * @date 2021/1/5 16:59
 */
String getCommoditysDemoThr();

实现(impl):

/**
 * @return {@link String}
 * @throws
 * @author 李庆伟
 * @date 2021/1/5 16:59
 */
public String getCommoditysDemoThr() {
    String val = stringRedisTemplate.opsForValue().get("hello");
    if(StringUtils.isEmpty(val)){
        RLock lock = redissonClient.getLock("hello-lock");//获取锁
        lock.lock();//加锁,看门狗机制,锁会自动续期 默认设置时间为30秒,续期时间每20秒开始 每次加10秒,该方法是阻塞方法
        //lock.lock(30,TimeUnit.SECONDS);//加锁,这种一旦设置了时间不会自动续期,工作中建议使用这种,加锁时间一般大于业务处理时间,该方法是阻塞方法
        //boolean lockVal = lock.tryLock(); //方法是非阻塞方法
        try {
            log.info("业务处理啦   "+Thread.currentThread().getId());
            stringRedisTemplate.opsForValue().set("hello","world");
            Thread.sleep(30000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();//解锁
        }
        return stringRedisTemplate.opsForValue().get("hello");
    } else {
        return val;
    }
}
这种方法相比于上面一种更简单方便!

 

锁说明:

重用锁:案例2和3已经应用。

读写锁(写读锁):写的时候读等待,读的时候写等待,读的时候在读不等待,写的时候阻塞

@Autowired
StringRedisTemplate stringRedisTemplate;
@Autowired
RedissonClient redissonClient;
/**
 * 写锁,写的时候读不了
 * @return {@link String}
 * @throws
 * @author 李庆伟
 * @date 2021/1/5 16:59
 */
@GetMapping("writeDemoFour")
public String writeDemoFour(){
    RReadWriteLock lock = redissonClient.getReadWriteLock("myReadWriteLock");
    RLock rLock = lock.writeLock();
    rLock.lock();
    try {
        log.info("这个是写锁。。。。。。。。。正在写中");
        Thread.sleep(30000);
        stringRedisTemplate.opsForValue().set("readDemoFour","readDemoFour");
        log.info("这个是写锁。。。。。。。。。readDemoFour");
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        rLock.unlock();
        return "readDemoFour";
    }
}
/**
 * 读锁,写完之后才能读
 * @return {@link String}
 * @throws
 * @author 李庆伟
 * @date 2021/1/11 18:01
 */
@GetMapping("readDemoFour")
public String readDemoFour(){
    RReadWriteLock lock = redissonClient.getReadWriteLock("myReadWriteLock");
    RLock rLock = lock.readLock();
    rLock.lock();
    try {
        log.info("这个是读锁。。。。。。。。。正在读中");
        String val = stringRedisTemplate.opsForValue().get("readDemoFour");
        log.info("这个是读锁。。。。。。。。。readDemoFour = "+val);
    } finally {
        rLock.unlock();
        return stringRedisTemplate.opsForValue().get("readDemoFour");
    }
}

 

公平锁:假如100个线程来抢锁,第一个抢到释放后,由第二个线程来继承锁,按照顺序来。(暂时没有遇到这方面的需求,没有举例)

 

信号量:适用于做秒杀,停车等需求

@Autowired
StringRedisTemplate stringRedisTemplate;
@Autowired
RedissonClient redissonClient;

 

/**
 * 停车位模拟-设置停车位数量
 * @return {@link String}
 * @throws
 * @author 李庆伟
 * @date 2021/1/11 18:01
 */
@GetMapping("carNoDemoFive")
public String carNoDemoFive() {
    stringRedisTemplate.opsForValue().set("carNo","5");
    return "ok";
}

/**
 * 停车位模拟-停车
 * @return {@link String}
 * @throws
 * @author 李庆伟
 * @date 2021/1/11 18:01
 */
@GetMapping("parkDemoFive")
public String parkDemoFive() throws InterruptedException {
    RSemaphore rs = redissonClient.getSemaphore("carNo");
    //rs.acquire();//这个没有返回值,这个是阻塞线程
    //return "ok";
    boolean c = rs.tryAcquire();//占位且非阻塞线程
    return c+"";
}

/**
 * 停车位模拟-车开走
 * @return {@link String}
 * @throws
 * @author 李庆伟
 * @date 2021/1/11 18:01
 */
@GetMapping("goDemoFive")
public String goDemoFive(){
    RSemaphore rs = redissonClient.getSemaphore("carNo");
    rs.release();//这个没有返回值
    return "ok";
}

 

 

 

 

 

你可能感兴趣的:(缓存,Java,高并发,redis,redisson,锁,分布式)