redis使用—缓存在高并发下的问题及分布式锁

目录

一、redis整合过程

1 引入pom依赖信息(本工程所有redis统一放入service-util)

2 写一个reids的工具类(用来将redis的池初始化到spring容器中)

3 写一个spring整合redis的配置类

4 注意:每个以用工程引入service-util后,单独配置自己的redis的配置文件

5、使用:通过缓存查询的代码测试

6、在高并发环境下还有如下三个问题。

二、redis常见问题

1 cont get a connection from the pool:一般是redis的配置有问题

2  缓存在高并发和安全压力下的一些问题

(1)缓存穿透

(2)缓存雪崩

(3)缓存击穿

(4)分布式锁

面试题:

附Service层完整代码:


一、redis整合过程

考虑到该页面是被用户高频访问的,所以性能必须进行尽可能的优化。

一般一个系统最大的性能瓶颈,就是数据库的io操作。从数据库入手也是调优性价比最高的切入点。

一般分为两个层面,一是提高数据库sql本身的性能,二是尽量避免直接查询数据库。

提高数据库本身的性能首先是优化sql,包括:使用索引,减少不必要的大表关联次数,控制查询字段的行数和列数。另外当数据量巨大是可以考虑分库分表,以减轻单点压力。

重点要讲的是另外一个层面:尽量避免直接查询数据库。

解决办法就是:缓存

缓存可以理解是数据库的一道保护伞,任何请求只要能在缓存中命中,都不会直接访问数据库。而缓存的处理性能是数据库10-100倍。

结构图:

1 引入pom依赖信息(本工程所有redis统一放入service-util)

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>

分别按照之前的方式放到parent模块和service-util的pom文件中。

2 写一个reids的工具类(用来将redis的池初始化到spring容器中)

由于redis作为缓存数据库,要被多个项目使用,所以要制作一个通用的工具类,方便工程中的各个模块使用。而主要使用redis的模块,都是后台服务的模块,xxx-service工程。所以咱们把redis的工具类放到service-util模块中,这样所有的后台服务模块都可以使用redis。

RedisUtil就是被注入的工具类以供其他模块调用。

public class RedisUtil {
    private  JedisPool jedisPool;
    public void initPool(String host,int port ,int database){
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        poolConfig.setMaxTotal(200);
        poolConfig.setMaxIdle(30);
        poolConfig.setBlockWhenExhausted(true);
        poolConfig.setMaxWaitMillis(10*1000);
        poolConfig.setTestOnBorrow(true);
        jedisPool=new JedisPool(poolConfig,host,port,20*1000);
    }
    public Jedis getJedis(){
        Jedis jedis = jedisPool.getResource();
        return jedis;
    }
}

3 写一个spring整合redis的配置类

将redis的链接池创建到spring的容器中。RedisConfig负责在spring容器启动时自动注入

@Configuration
public class RedisConfig {

    //读取配置文件中的redis的ip地址
    @Value("${spring.redis.host:disabled}")
    private String host;

    @Value("${spring.redis.port:0}")
    private int port;

    @Value("${spring.redis.database:0}")
    private int database;

    @Bean
    public RedisUtil getRedisUtil(){
        if(host.equals("disabled")){
            return null;
        }
        RedisUtil redisUtil=new RedisUtil();
        redisUtil.initPool(host,port,database);
        return redisUtil;
    }

}

4 注意:每个以用工程引入service-util后,单独配置自己的redis的配置文件

Service-util的配置文件没有作用

同时,任何模块想要调用redis都必须在application.properties配置,否则不会进行注入。

spring.redis.host=192.168.xxx.xxx
spring.redis.port=6379
spring.redis.database=0

注意:在SpringBoot项目中启动类所在的位置至少要和RedisUtil、RedisConfig 所在的包平级;否则无法扫描到!!

    

5、使用:通过缓存查询的代码测试

结果:

6、在高并发环境下还有如下三个问题。

  1. 如果redis宕机了,或者链接不上,怎么办?
  2. 如果redis缓存在高峰期到期失效,在这个时刻请求会向雪崩一样,直接访问数据库如何处理?
  3.  如果用户不停地查询一条不存在的数据,缓存没有,数据库也没有,那么会出现什么情况,如何处理?

二、redis常见问题

1 cont get a connection from the pool:一般是redis的配置有问题

2  缓存在高并发和安全压力下的一些问题

(1)缓存穿透

缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,并且处于容错考虑,我们没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。

解决: 为了防止缓存穿透将,null或者空字符串值设置给redis进行缓存,但它的过期时间会很短,最长不超过五分钟

(2)缓存雪崩

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

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

(3)缓存击穿

对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:如果这个key在大量请求同时进来前正好失效,那么所有对这个key的数据查询都落到db,我们称为缓存击穿。

缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

解决:分布式锁

缓存击穿和缓存雪崩的区别:

A 击穿是一个热点key失效

B 雪崩是缓存中的很多key失效,导致数据库负载过重宕机

C 穿透是利用不存在的key去攻击mysql数据库

(4)分布式锁

击穿:在正常的访问情况下,如果缓存失效,如何保护mysql,重启缓存的过程

使用redis数据库的分布式锁,解决mysql的访问压力问题

第一种分布式锁:redis自带一个分布式锁,set px nx

核心代码

加锁

  String token = UUID.randomUUID().toString();

  String lock = jedis.set(key, token, "NX", "EX",20);


第二种分布式锁:redisson框架,一个redis的带有juc的lock功能的客户端的实现(既有jedis的功能,又有juc的锁功能)

https://github.com/redisson/redisson/wiki/1.-Overview

面试题:

问题1 如果在redis中的锁已经过期了,然后锁过期的那个请求又执行完毕,回来删锁,删除了其他线程的锁怎么办?

答:在设置分布式锁的时候利用token来唯一标识,再删除之前先判断一下,判断token值是不是自己的token

问题2 如果碰巧在查询redis锁还没删除的时候,正在网络传输,锁过期了,怎么办?(意思就是,在get的时候锁还没过期,在进入判断准备删除的时候,锁过期了,怎么办)

答:可与用lua脚本,在查询到key的同时删除该key,防止高并发下的意外的发生

String script ="if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

jedis.eval(script, Collections.singletonList("lock"),Collections.singletonList(token));

--------------------------------------------------------------------------------------------------------------------------------------------------

附Service层完整代码:

@Override
    public PmsSkuInfo getSkuById(String skuId ) {
       PmsSkuInfo pmsSkuInfo = new PmsSkuInfo();
       //链接缓存
        Jedis jedis = redisUtil.getJedis();
        //查询缓存
        String skuKey = "sku:"+skuId+":info";
        String skuJson = jedis.get(skuKey);
        if (StringUtils.isNotBlank(skuJson)){
            pmsSkuInfo = JSON.parseObject(skuJson,PmsSkuInfo.class);
        }else {
            //如果缓存中没有
            //设置分布式锁
            String token = UUID.randomUUID().toString();
            String OK = jedis.set("sku:"+skuId+":lock", token, "NX", "PX", 10*1000);//拿到锁的线程有10s过期时间
            if(StringUtils.isNoneBlank(OK)  && OK.equals("OK")){
                //设置成功,有权在10s的过期时间内访问数据库
                pmsSkuInfo = getSkuByIdFromDb(skuId);//原查询数据库的方法
                //将结果存入redis
                if (pmsSkuInfo !=null) {
                    jedis.set(skuKey, JSON.toJSONString(pmsSkuInfo));
                }else {
                    //数据库中不存在该sku
                    //为了防止缓存穿透,将null值设置给redis或者空字符串并设置过期时间
                    jedis.setex(skuKey, 60*3,JSON.toJSONString(""));
                }
                //在访问mysql后,将mysql的分布式锁释放
                String lockToken = jedis.get("sku:" + skuId + ":lock");
                if (StringUtils.isNoneBlank(lockToken) && lockToken.equals(token)) {
                   //可与用lua脚本,在查询到key的同时删除该key,防止高并发下的意外的发生
                    jedis.del("sku:" + skuId + ":lock");//用token确认删除的是自己的锁
                }
            }else{
                //设置失败,自旋(该线程在睡眠几秒后,重新尝试访问本方法)
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return getSkuByIdFromDb(skuId);//不加return表示多了一条线程
            }
        }
        //关闭jedis
        jedis.close();
        return pmsSkuInfo;
    }

 

发布了96 篇原创文章 · 获赞 16 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/qq_38151401/article/details/103906570