使用Redis解决页面并发问题 分布式锁

当并发量提高的时候数据库就支撑不了很高的并发,这时候我们就可以引入redis来做一个数据库的缓存,来减小数据库的压力,当数据库第一次被查询之后,就把数据库查出来的结果用来存到redis当中
redis简介->入门

这样下一个请求来的时候就去redis里面了,就减轻了数据库的压力

缓存使用的简单的策略
redis的整合步骤
	1. 将redis整合到项目中(redis+spring)
				a.首先肯定是引入依赖
<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.1.0</version>
</dependency>
			b.写一个Redis的工具类->用来将redis的池初始化到spring容器中
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;
    }
}

	c.写一个Spring整合redis的配置类
			将redis的连接池创建到spring的容器中
@Configuration
public class RedisConfig {
    //读取配置文件中的redis的ip地址
    @Value("${spring.redis.host:disabled}")
    private String host = “192.168.222.20”;
    @Value("${spring.redis.port:0}")
    private int port = “6179”;
    @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);  //初始化连接池
        //@Bean注解就会把这个 return的这个对象注入到容器当中
        return redisUtil;
    }
}

	3. 设计一个数据存储策略
								->企业中的存储策略     数据对象名:数据对象ID:对象属性
								eg:User:123:Info    ->这个代表的就是存储的123这个用户的信息(info)

当然也可以直接使用RedisTemplate或者是StringRedisTemplate来直接使用

缓存list

     ShCatalog shCatalog = null;
        List<ShCatalog> list = new ArrayList<>();
        String key = "catalog:parendId-" + id + ":";
        if (redisTemplate.hasKey(key)) {
            List<T> resule = (List<T>) redisTemplate.opsForList().range(key, 0, -1);
            System.out.println("查了redis");
            list = (List<ShCatalog>) resule;
        } else {
            System.out.println("查了数据库");
            list = shCatalogRepository.findByParentId(id);
            redisTemplate.opsForList().leftPush(key, JSON.toJSONString(list));
        }
        QueryResult<ShCatalog> queryResult = new QueryResult<>();
        queryResult.setList(list);
        queryResult.setTotal(list.size());
        return new QueryResponseResult(CommonCode.SUCCESS, queryResult);

缓存对象

       ShCatalog shCatalog = null;
        String key = "catalog:" + id + ":";
        if (redisTemplate.hasKey(key)) {
            String catalogInfoJson = redisTemplate.opsForValue().get(key);
            shCatalog = JSON.parseObject(catalogInfoJson, ShCatalog.class);
        } else {
            shCatalog = shCatalogRepository.getOne(id);
            redisTemplate.opsForValue().set(key, JSON.toJSONString(shCatalog));
        }
        QueryResult<ShCatalog> queryResult = new QueryResult<>();
        List<ShCatalog> list = new ArrayList<>();
        list.add(shCatalog);
        queryResult.setList(null);
        queryResult.setTotal(0);
        return new QueryResponseResult(CommonCode.SUCCESS, queryResult);

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

缓存穿透

1.缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但数据库也是无此记录,并且处于容错考虑,我们没有将这次查询的null写入缓存.
这将导致这个不存在的数据每次请求都要到数据库去查询,失去了缓存的意义,在流量大的时候,可能DB就挂掉了,要是有人利用不存在的key频繁的攻击我们的应用,这就是漏洞
2
.解决:**空结果进行缓存,但他的过期时间会很短,最长不超过五分钟

            list = shCatalogRepository.findByParentId(id);
            if (list.size() == 0) {
//                表示数据库中也没有这个数据,但是为了防止缓存穿透 所以我们要把空存到redis中
                redisTemplate.opsForList().leftPush(key, "");
//                设置过期时间为三分钟
                redisTemplate.expire(key, 3, TimeUnit.MILLISECONDS);
            } else {
//                有数据
                redisTemplate.opsForList().leftPush(key, JSON.toJSONString(list));
            }

缓存击穿

对于一些设置了过期时间的key,如果这些key可能会在某些时间点被高并发的访问,是一种非常热点的数据,这个时候,需要考虑一个问题:如果这个key在大量请求同时进来的时候正好失效,那么所有对这个key的数据查询都落在DB,我们称为缓存击穿 简要来说就是某一个热点key在高并发访问的情况下突然失效,导致大量的并发打进数据库
------>和缓存雪崩之间的区别
1.击穿是一个热点key失效
2.雪崩是很多key集体失效
解决方法:
1.通过锁,限制请求数量,
使用redis分布式锁的机制解决DB的压力问题
第一种分布式锁:redis自带的一个分布式锁,set ex nx
setnx 就是 set key value 不同的是 只有当redis中没有 这个key的时候才能够set成功,如果有了的话就会set失败.这样的话再同一时间就只有一个才能够去set成功,然后就只让这个set成功的线程去访问数据库,访问完成之后再删除这个,其他线程才能再去set成功

//设置nx  分布式锁   ->  防止缓存击穿
            Boolean isSuccess = redisTemplate.getConnectionFactory().getConnection().setNX(keyFbs.getBytes(), "1".getBytes());
//         设置分布式锁的过期时间   10秒过期
            redisTemplate.expire(keyFbs, 10, TimeUnit.SECONDS);
//            如果设置成功 表示拿到了lock  可以进行数据库访问  在10s的过期时间内可以访问数据库
            if (isSuccess) {
                list = shCatalogRepository.findByParentId(id);
//                在访问完成DB之后应该释放分布式锁
                redisTemplate.delete(keyFbs);
            } else {
//                如果设置失败   代表没有权利访问数据库    自旋->该线程在睡眠几秒后再重新访问本方法
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
//                这个方法findCatalogByIdList  就是现在运行的方法的名字  就是重新调用一次这个方法
                return findCatalogByIdList(id);
            }
					第二种分布式锁:redisson框架,一个redis的带有juc的lock的功能实现(既有jedis的功能,又有juc的功能)

问题一:
如果在redis中的锁已经过期,然后锁过期的那个请求又执行完毕,回来删锁(删的是其他请求的锁)怎么办?
问题描述:就是一个请求加了锁去访问数据库了,然后锁都过期了还没访问完,这个时候另一个线程加了锁去访问数据库了.前面那个线程又访问完了回来删锁,删除了第二个线程的锁,这个时候怎么办
解决办法: 设置锁的时候设置了一个key和一个value ,虽然她们key是一样的但是我们设置锁的时候value设置为当前线程的token(token不一样)->直接获取一个UUID就行了<-将来删除锁的时候,根据value来判断是不是自己的加的那把锁

   byte[] suoVal = redisTemplate.getConnectionFactory().getConnection().get(keyFbs.getBytes());
                String tempsuo = suoVal.toString();
                //如果是自己的锁才有权利删除
                if (StringUtils.isNotEmpty(tempsuo) && tempsuo.equals(suoValue)) {
                    //                在访问完成DB之后应该释放分布式锁
                    redisTemplate.delete(keyFbs);
                }

问题二:
如果碰巧在查询redis锁还没删除的时候,正在网络传输时,锁过期了怎么办?
问题解释:
就在你上面代码判断是自己的锁的时候锁过期了怎么办? ->临界点
那就查询到的一瞬间删除 ,如果没有查询到就不删
用Lua

缓存雪崩

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

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

上文中代码片段的完整代码

    public QueryResponseResult findCatalogByIdList(String id) {
        ShCatalog shCatalog = null;
        List<ShCatalog> list = new ArrayList<>();
        String key = "catalog:parendId-" + id + ":";
        String keyFbs = "catalog" + id;
        String suoValue = UUID.randomUUID() + "";
        if (redisTemplate.hasKey(key)) {
            List<T> resule = (List<T>) redisTemplate.opsForList().range(key, 0, -1);
            System.out.println("查了redis");
            list = (List<ShCatalog>) resule;
        } else {
            //            查询数据库的这个分支是应该受到保护的 尽量少去查询数据库
            System.out.println("查了数据库");
            //设置nx  分布式锁   ->  防止缓存击穿
            Boolean isSuccess = redisTemplate.getConnectionFactory().getConnection().setNX(keyFbs.getBytes(), suoValue.getBytes());
            //         设置分布式锁的过期时间   10秒过期
            redisTemplate.expire(keyFbs, 10, TimeUnit.SECONDS);
            //            如果设置成功 表示拿到了lock  可以进行数据库访问  在10s的过期时间内可以访问数据库
            if (isSuccess) {
                list = shCatalogRepository.findByParentId(id);
                byte[] suoVal = redisTemplate.getConnectionFactory().getConnection().get(keyFbs.getBytes());
                String tempsuo = suoVal.toString();
                //如果是自己的锁才有权利删除
                if (StringUtils.isNotEmpty(tempsuo) && tempsuo.equals(suoValue)) {
                    //                在访问完成DB之后应该释放分布式锁
                    redisTemplate.delete(keyFbs);
                }
            } else {
                //                如果设置失败   代表没有权利访问数据库    自旋->该线程在睡眠几秒后再重新访问本方法
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //                这个方法findCatalogByIdList  就是现在运行的方法的名字  就是重新调用一次这个方法
                return findCatalogByIdList(id);
            }
            //            防止缓存穿透
            if (list.size() == 0) {
                //                表示数据库中也没有这个数据,但是为了防止缓存穿透 所以我们要把空存到redis中
                redisTemplate.opsForList().leftPush(key, "");
                //                设置过期时间为三分钟
                redisTemplate.expire(key, 3, TimeUnit.MILLISECONDS);
            } else {
                //                有数据
                redisTemplate.opsForList().leftPush(key, JSON.toJSONString(list));
            }
        }
        QueryResult<ShCatalog> queryResult = new QueryResult<>();
        queryResult.setList(list);
        queryResult.setTotal(list.size());
        return new QueryResponseResult(CommonCode.SUCCESS, queryResult);
    }

setnx这个在redistemplate当中也有

实现分布式锁 使用redisson

首先引入依赖

      <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.12.1</version>
        </dependency>

在启动类中注入redisson

 @Bean
    public Redisson redisson(){
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(0);
        return (Redisson)Redisson.create();

    }

使用

    public void lockDemo() {
        String id = "lockTest";
        RLock redissonLock = redisson.getLock(id);//得到一个锁对象
        redissonLock.lock(10, TimeUnit.SECONDS);//加锁
        int value = Integer.valueOf(redisTemplate.opsForValue().get("have"));
        if (value > 0) {
            value--;
            redisTemplate.opsForValue().set("have", value + "");
            System.out.println("买到了货物---现在还剩" + value);
        } else {
            System.out.println("卖完了 没有买到");
        }
        redissonLock.unlock();//解锁
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值