SpringBoot整合Redis处理页面高并发及缓存问题

10 篇文章 0 订阅
7 篇文章 0 订阅

使用Redis作为缓存数据库高并发处理步骤图:
在这里插入图片描述

整合redis到工程中

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

<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>

二、编写配置类和工具类
分别按照之前的方式放到parent模块和service-util的pom文件中。
然后在service-util中创建两个类RedisConfig和RedisUtil
RedisConfig负责在spring容器启动时自动注入,而RedisUtil就是被注入的工具类以供其他模块调用。

 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;
    }

}
 RedisConfig
@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;
    }

}

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

spring.redis.host=redis.server.com
spring.redis.port=6379
spring.redis.database=0

现在可以在manage-service中的getSkuInfo()方法测试一下

try {
    Jedis jedis = redisUtil.getJedis();
    jedis.get("test","text_value" );
}catch (JedisConnectionException e){
    e.printStackTrace();
}

四、使用redis进行业务开发
开始开发先说明redis key的命名规范,由于Redis不像数据库表那样有结构,其所有的数据全靠key进行索引,所以redis数据的可读性,全依靠key。
企业中最常用的方式就是:
object: id: field
比如:sku:1314:info
user:1092:password

@Service
public class SkuServiceImpl extends ServiceImpl<SkuInfoMapper, PmsSkuInfo> implements SkuService
{
    @Autowired
    SkuInfoMapper skuInfoMapper;
    @Autowired
    SkuAttrValueMapper skuAttrValueMapper;
    @Autowired
    SkuSaleAttrValueMapper skuSaleAttrValueMapper;
    @Autowired
    SkuImageMapper skuImageMapper;


    @Override
    public PmsSkuInfo getSkuById(String skuId) {
        /*PmsSkuInfo pmsSkuInfo = skuInfoMapper.selectById(skuId);
        List<PmsSkuImage> skuImageList = skuImageMapper.selectList(new QueryWrapper<PmsSkuImage>().eq("sku_id", skuId));
        pmsSkuInfo.setSkuImageList(skuImageList);*/
        PmsSkuInfo pmsSkuInfo = null;
        //链接缓存
        Jedis jedis = RedisUtil.getJedis();
        //查询缓存
        String skuKey = "sku:"+skuId+":info";
        String skuJson = jedis.get(skuKey);
        try{
            if(StringUtils.isNotBlank(skuJson)){
                pmsSkuInfo = JSON.parseObject(skuJson, PmsSkuInfo.class);
            }else{
                //如果缓存没有,查询mysql
                pmsSkuInfo = skuInfoMapper.selectById(skuId);
                List<PmsSkuImage> skuImageList = skuImageMapper.selectList(new QueryWrapper<PmsSkuImage>().eq("sku_id", skuId));
                pmsSkuInfo.setSkuImageList(skuImageList);
                //mysql查询结果存入redis
                if(pmsSkuInfo!=null){
                    jedis.set("sku:"+skuId+":info",JSON.toJSONString(pmsSkuInfo));
                }else{
                    //数据库中不存在该sku
                    //为了防止缓存穿透,null或者空字符串设置给redis
                    jedis.setex("sku:"+skuId+":info",60*3, JSON.toJSONString(""));
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            jedis.close();
        }
        return pmsSkuInfo;
    }
}

访问Redis数据库查看缓存数据:
在这里插入图片描述

缓存问题:

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

解决:
空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。

if(pmsSkuInfo!=null){
                    jedis.set("sku:"+skuId+":info",JSON.toJSONString(pmsSkuInfo));
                }else{
                    //数据库中不存在该sku
                    //为了防止缓存穿透,null或者空字符串设置给redis
                    jedis.setex("sku:"+skuId+":info",60*3, JSON.toJSONString(""));
                }

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

解决:
原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
3、缓存击穿
对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:如果这个key在大量请求同时进来前正好失效,那么所有对这个key的数据查询都落到db,我们称为缓存击穿。和缓存雪崩的区别:

  • 击穿是一个热点key失效

  • 雪崩是很多key集体失效

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

解决:
1、Reids自带的分布式锁,set ex nx
在这里插入图片描述

在这里插入图片描述此时Redis的set方法在第一次执行时会set成功,如果设置了过期时间。在过期时间之内去set一个已经存在的key时不会成功。所以我们在每次访问之前去设置对应的sku:skuId :lock 如果设置成功才去访问数据库,从而一定意义上避免了缓存击穿。


@Service
public class SkuServiceImpl extends ServiceImpl<SkuInfoMapper, PmsSkuInfo> implements SkuService
{
    @Autowired
    SkuInfoMapper skuInfoMapper;
    @Autowired
    SkuAttrValueMapper skuAttrValueMapper;
    @Autowired
    SkuSaleAttrValueMapper skuSaleAttrValueMapper;
    @Autowired
    SkuImageMapper skuImageMapper;
     @Override
    public PmsSkuInfo getSkuById(String skuId) {
        /*PmsSkuInfo pmsSkuInfo = skuInfoMapper.selectById(skuId);
        List<PmsSkuImage> skuImageList = skuImageMapper.selectList(new QueryWrapper<PmsSkuImage>().eq("sku_id", skuId));
        pmsSkuInfo.setSkuImageList(skuImageList);*/
        PmsSkuInfo pmsSkuInfo = null;
        //链接缓存
        Jedis jedis = RedisUtil.getJedis();
        //查询缓存
        String skuKey = "sku:"+skuId+":info";
        String skuJson = jedis.get(skuKey);
        try{
            if(StringUtils.isNotBlank(skuJson)){
                pmsSkuInfo = JSON.parseObject(skuJson, PmsSkuInfo.class);
            }else{
                //如果缓存没有,查询mysql

                //设置分布式锁
                // String OK = jedis.set("sku:" + skuId + ":lock",token,"nx","px",10*1000); //拿到10秒控制权限
                String token = UUID.randomUUID().toString();
                String OK = jedis.set("sku:" + skuId + ":lock",token,"nx","px",10*1000); //拿到10秒控制权限

                if(StringUtils.isNotBlank(OK)&&OK.equals("OK")){
                    //设置成功,有权在10秒的过期时间内访问数据库
                    pmsSkuInfo = skuInfoMapper.selectById(skuId);
                    List<PmsSkuImage> skuImageList = skuImageMapper.selectList(new QueryWrapper<PmsSkuImage>().eq("sku_id", skuId));
                    pmsSkuInfo.setSkuImageList(skuImageList);
                    //mysql查询结果存入redis
                    if(pmsSkuInfo!=null){
                        jedis.set("sku:"+skuId+":info",JSON.toJSONString(pmsSkuInfo));
                    }else{
                        //数据库中不存在该sku
                        //为了防止缓存穿透,null或者空字符串设置给redis
                        jedis.setex("sku:"+skuId+":info",60*3, JSON.toJSONString(""));
                    }
                    //在访问mysql之后,将分布式锁释放
                    //jedis.del("sku:" + skuId + ":lock");
                    String lockToken = jedis.get("sku:" + skuId + ":lock");
                    if(StringUtils.isNotBlank(lockToken)&&lockToken.equals(token)){
                        //用token确认删除的是自己的锁
                        //jedis.eval("lua"):可以使用lua脚本,在查询key的同时删除该key,防止高并发下的意外发生
                        jedis.del("sku:" + skuId + ":lock");
                    }


                }else{
                    //设置失败自旋(该线程睡眠几秒后,重新尝试访问)
                    try{
                        Thread.sleep(3000);
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                     /*return不会产生新的进程,这才是自旋。如果不加return,则会产生新的getSkuById()“孤儿”进程。*/
                    return getSkuById(skuId);
                }

            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            jedis.close();
        }
        return pmsSkuInfo;
    }

    
}

错误自旋代码:开启新进程访问,使之前的线程成为孤儿进程
在这里插入图片描述正确自旋代码:
在这里插入图片描述问题一:如果第一个请求在Redis中的锁由于操作数据库过程中的时间太长已经过期,然后第二个请求获取到锁之后执行数据库的过程中请求一又回来删锁,删了线程二的锁怎么办?

在设置锁的时候设置值为一个唯一的UUID然后删除锁时判断是否为自己的锁。如果是自己的锁删除,不是的话说明自己的锁已经过期。
在这里插入图片描述在这里插入图片描述

问题二:如果碰巧在查询判断Redis锁是否过期时还没有过期,而在删除操作之前的一瞬间过期,怎么办?
在这里插入图片描述

jedis.eval(“lua”):可以使用lua脚本,在查询key的同时删除该key,防止高并发下的意外发生
在这里插入图片描述在判断key的时候,如果存在直接删除

2、Redisson锁
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。
在这里插入图片描述整合:

引入pom:

<!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.10.5</version>
</dependency>

配置文件:

spring.redis.host=192.168.0.170
spring.redis.port=6379

配置类:

@Configuration
public class RedissonConfig {

    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private String port;

    @Bean
    public RedissonClient redissonClient(){
        Config config = new Config();
        config.useSingleServer().setAddress("redis://"+host+":"+port);
        RedissonClient redisson = Redisson.create(config);
        return redisson;
    }
}

项目整合:
基于Redis的Redisson分布式可重入锁RLock Java对象实现了java.util.concurrent.locks.Lock接口。同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。

RLock lock = redisson.getLock("anyLock");
// 最常见的使用方法
lock.lock();

另外Redisson还通过加锁的方法提供了leaseTime的参数来指定加锁的时间。超过这个时间后锁便自动解开了。

// 加锁以后10秒钟自动解锁
// 无需调用unlock方法手动解锁
lock.lock(10, TimeUnit.SECONDS);

// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
   try {
     ...
   } finally {
       lock.unlock();
   }
}

在这里插入图片描述


@Service
public class SkuServiceImpl extends ServiceImpl<SkuInfoMapper, PmsSkuInfo> implements SkuService
{
    @Autowired
    SkuInfoMapper skuInfoMapper;
    @Autowired
    SkuAttrValueMapper skuAttrValueMapper;
    @Autowired
    SkuSaleAttrValueMapper skuSaleAttrValueMapper;
    @Autowired
    SkuImageMapper skuImageMapper;
    @Autowired
    Redisson redisson;
    @Override
    public PmsSkuInfo getSkuById(String skuId) {
    
        //Redisson锁是包装的JUC内的锁策略,是Java代码层面的分布式锁
        RLock lock = redisson.getLock("anyLock");//声明锁
        // 加锁以后10秒钟自动解锁
        // 无需调用unlock方法手动解锁
        PmsSkuInfo pmsSkuInfo = null;
        //链接缓存
        Jedis jedis = RedisUtil.getJedis();
        //查询缓存
        String skuKey = "sku:"+skuId+":info";
        String skuJson = jedis.get(skuKey);
        try{
            if(StringUtils.isNotBlank(skuJson)){
                pmsSkuInfo = JSON.parseObject(skuJson, PmsSkuInfo.class);
            }else{
                //如果缓存没有,查询mysql
                //设置分布式锁
            // 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
            boolean res = lock.tryLock(3, 10, TimeUnit.SECONDS);
            if (res) {
                try {
                    pmsSkuInfo = skuInfoMapper.selectById(skuId);
                    List<PmsSkuImage> skuImageList = skuImageMapper.selectList(new QueryWrapper<PmsSkuImage>().eq("sku_id", skuId));
                    pmsSkuInfo.setSkuImageList(skuImageList);
                    //mysql查询结果存入redis
                    if(pmsSkuInfo!=null){
                        jedis.set("sku:"+skuId+":info",JSON.toJSONString(pmsSkuInfo));
                    }else{
                        //数据库中不存在该sku
                        //为了防止缓存穿透,null或者空字符串设置给redis
                        jedis.setex("sku:"+skuId+":info",60*3, JSON.toJSONString(""));
                    }
                }catch (Exception e){
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
                     }else{
                return getSkuById(skuId); //如果得不到锁,重新访问
            }

                 }
           }catch (Exception e){
            e.printStackTrace();
        }finally {
            jedis.close();
        }
        return pmsSkuInfo;
}

压力测试
ApacheHaus分布式压力测试

  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值