使用Redis作为缓存数据库高并发处理步骤图:

SpringBoot整合Redis存入list springboot整合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数据库查看缓存数据:

SpringBoot整合Redis存入list springboot整合redis缓存_redis_02

缓存问题:

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

SpringBoot整合Redis存入list springboot整合redis缓存_缓存_03

SpringBoot整合Redis存入list springboot整合redis缓存_redis_04

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

    
}

错误自旋代码:开启新进程访问,使之前的线程成为孤儿进程

SpringBoot整合Redis存入list springboot整合redis缓存_spring_05

正确自旋代码:

SpringBoot整合Redis存入list springboot整合redis缓存_spring_06

问题一:如果第一个请求在Redis中的锁由于操作数据库过程中的时间太长已经过期,然后第二个请求获取到锁之后执行数据库的过程中请求一又回来删锁,删了线程二的锁怎么办?在设置锁的时候设置值为一个唯一的UUID然后删除锁时判断是否为自己的锁。如果是自己的锁删除,不是的话说明自己的锁已经过期。

SpringBoot整合Redis存入list springboot整合redis缓存_缓存_07

SpringBoot整合Redis存入list springboot整合redis缓存_缓存_08

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

SpringBoot整合Redis存入list springboot整合redis缓存_spring_09

jedis.eval(“lua”):可以使用lua脚本,在查询key的同时删除该key,防止高并发下的意外发生

SpringBoot整合Redis存入list springboot整合redis缓存_redis_10

在判断key的时候,如果存在直接删除2、Redisson锁

Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。

SpringBoot整合Redis存入list springboot整合redis缓存_redis_11

整合:

引入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();
   }
}

SpringBoot整合Redis存入list springboot整合redis缓存_redis_12

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