Redis实际使用场景

1.热点数据的缓存

1.1缓存的作用

把经常需要访问的数据储存到redis中,以后再查询该数据时,优先从redis中查询,如果redis没有中没有,则才会查询数据。并把查询到的结果放到redis中以便下次能从redis中获取,提高查询效率,减少数据库的压力

1.2什么样的数据适合放入缓存

-查询频率高的数据

-修改频率低的数据

-数据安全性低要求不高的数据

1.3如何使用redis作为缓存

案例:创建springboot项目使用2.3.2.RELEASE版本

1.3.1依赖

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.1</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

1.3.2配置文件

#mysql
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/qy168?serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=root

#redis
spring.redis.host=192.168.61.223
spring.redis.port=6379

#mybatis-plus
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

1.3.3实体类

@Data
@TableName("tbl_dept")
public class Dept {
    @TableId(type = IdType.AUTO)
    private Integer id;
    @TableField(value = "d_name")
    private String dname;

    private String loc;
}

1.3.4dao层

public interface DeptDao extends BaseMapper<Dept> {
}

1.3.5server层

public interface DeptService {

    public Dept findById(Integer id);
    
}

@Service
public class DeptServiceImpl implements DeptService {
    @Autowired
    private DeptDao deptDao;

    @Override
    public Dept findById(Integer id) {
        Dept dept = deptDao.selectById(id);
        return dept;
    }


}

1.3.6controller层

@RestController
@RequestMapping("/dept")
public class DeptController {
    @Autowired
    private DeptService deptService;

    @GetMapping("/getById/{id}")
    public Dept getById(@PathVariable Integer id ){
        Dept dept = deptService.findById(id);
        return dept;
    }
}

测试:可以看到每次查询都会查询数据库,下面使用redis缓存来解决这个问题

修改业务层添加redis缓存

@Service
public class DeptServiceImpl implements DeptService {
    @Autowired
    private DeptDao deptDao;
    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    public Dept findById(Integer id) {
        //先查询缓存中有没有该数据
        ValueOperations forValue = redisTemplate.opsForValue();
        Object o = forValue.get("dept::" + id);
        if (o!=null){
            //如果有则直接返回
            return (Dept) o;
        }
        //如果没有再查询数据库
        Dept dept = deptDao.selectById(id);
        if (dept!=null){
            //不为空则添加到缓存
            forValue.set("dept::"+id,dept);
        }
        return dept;
    }
}

运行测试发现只有第一次访问查询了数据库后来的都使用了redis中的缓存数据

修改和删除时也应该修改缓存中的数据,添加则不需要

@Service
public class DeptServiceImpl implements DeptService {
    @Autowired
    private DeptDao deptDao;
    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    public Dept findById(Integer id) {
        //先查询缓存中有没有该数据
        ValueOperations forValue = redisTemplate.opsForValue();
        Object o = forValue.get("dept::" + id);
        if (o!=null){
            //如果有则直接返回
            return (Dept) o;
        }
        //如果没有再查询数据库
        Dept dept = deptDao.selectById(id);
        if (dept!=null){
            //不为空则添加到缓存
            forValue.set("dept::"+id,dept);
        }
        return dept;
    }

    @Override
    public Dept insert(Dept dept) {
        int insert = deptDao.insert(dept);
        return dept;
    }

    @Override
    public int update(Dept dept) {
        int i = deptDao.updateById(dept);
        if (i>0){
            //如果修改成功则清除对应的缓存或者修改对应的缓存,这里采用了删除
            redisTemplate.delete("dept::"+dept.getId());
        }
        return i;
    }

    @Override
    public int deleteById(Integer id) {
        int i = deptDao.deleteById(id);
        if (i>0){
            //如果删除成功则清除对应的缓存
            redisTemplate.delete("dept::"+id);
        }
        return i;
    }
}

思考:我们再使用redis作为缓存时,每次都需要自己添加缓存代码[非业务代码]。未来维护时还需要维护redis非业务代码。

解决:可是使用AOP解决

spring框架也会想到使用AOP解决业务代码和缓存的非业务代码的重合。--使用缓存的注解

1.3.7如何使用spring缓存注解的方式实现redis缓存功能

必须让redistemplate支持缓存

@Configuration
public class RedisConfig {
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        //解决查询缓存转换异常的问题
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // 配置序列化(解决乱码的问题),过期时间600秒
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(600)) //缓存过期10分钟 ---- 业务需求。
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))//设置key的序列化方式
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)) //设置value的序列化
                .disableCachingNullValues();
        RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();
        return cacheManager;
    }
}

使用spring提供的缓存注解

@Service
public class DeptServiceImpl implements DeptService {
    @Autowired
    private DeptDao deptDao;
    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    @Cacheable(cacheNames = "dept",key = "#id")//查询
    public Dept findById(Integer id) {
        Dept dept = deptDao.selectById(id);
        return dept;
    }

    @Override
    public Dept insert(Dept dept) {
        int insert = deptDao.insert(dept);
        return dept;
    }

    @Override
    @CachePut(cacheNames = "dept",key = "#dept.id")//修改
    public int update(Dept dept) {
        int i = deptDao.updateById(dept);
        return i;
    }

    @Override
    @CacheEvict(cacheNames = "dept",key = "#id")//删除
    public int deleteById(Integer id) {
        int i = deptDao.deleteById(id);
        return i;
    }
}

要使用缓存注解必须在主启动类上添加@EnableCaching 用来开启缓存注解

@RestController
@RequestMapping("/dept")
public class DeptController {
    @Autowired
    private DeptService deptService;

    @GetMapping("/getById/{id}")
    public Dept getById(@PathVariable Integer id ){
        Dept dept = deptService.findById(id);
        return dept;
    }
    @GetMapping("/delete/{id}")
    public int delete(@PathVariable Integer id){
        return deptService.deleteById(id);
    }
    @GetMapping("/insert")
    public Dept insert(Dept dept){
        return deptService.insert(dept);
    }

    @GetMapping("/update")
    public int update(Dept dept){
        return deptService.update(dept);
    }
}

测试:效果和上面一样

2.分布式锁

1.为什么使用锁

可以确保多个线程之间共享资源的互斥访问,从而避免出现数据竞争和线程安全问题。接下来通过案例来说明为什么使用锁

dao层

@Mapper
public interface StockDao {
    @Select("select num from tbl_stock where productid=#{productid}")
    int findById(Integer productid);

    @Update("update tbl_stock set num=num-1 where productid=#{productid} ")
    void update(Integer productid);
}

业务层

@Service
public class StockService02 {

    @Autowired
    private StockDao stockDao;
    
    public String decrement(Integer productid) {
            int num = stockDao.findById(productid);
            if (num > 0) {
                stockDao.update(productid);
                System.out.println("商品编号为:" + productid + "的商品库存剩余:" + (num - 1) + "个");
                return "商品编号为:" + productid + "的商品库存剩余:" + (num - 1) + "个";
            } else {
                System.out.println("商品编号为:" + productid + "的商品库存不足。");
                return "商品编号为:" + productid + "的商品库存不足。";
            }
    }
}

测试:修改商品数量为10,使用测压工具0.1秒执行200次任务,模拟高并发发现商品出现了负数:

2.单机如何使用锁

可以使用lock或synchronized

 接下来我们使用自动锁来解决这一问题:

@Service
public class StockService02 {

    @Autowired
    private StockDao stockDao;

    public String decrement(Integer productid) {
        //自动锁
        synchronized (this) {
            int num = stockDao.findById(productid);
            if (num > 0) {
                stockDao.update(productid);
                System.out.println("商品编号为:" + productid + "的商品库存剩余:" + (num - 1) + "个");
                return "商品编号为:" + productid + "的商品库存剩余:" + (num - 1) + "个";
            } else {
                System.out.println("商品编号为:" + productid + "的商品库存不足。");
                return "商品编号为:" + productid + "的商品库存不足。";
            }
        }
    }
}

测试发现这次商品并没有出现负数:

但随着我们项目的集群搭建,由代理服务器分配到具体的项目上,而项目与项目之间的锁互不影响,所以还是会导致超卖的现象

模拟多台效果:使用nginx代理服务模拟集群,启动两台项目

nginx.conf:

    upstream qy168{
	server localhost:8088;
	server localhost:8089;	
    }
    server {
	listen       81;
	server_name  localhost;
	location \ {
            proxy_pass   http://qy168;
        }
    }

测试修改库存数量为10,使用测压工具访问81端口0.1秒访问200次

发现还是会导致有超卖的现象,因为两台项目之间的锁互不影响,都拿到了自己的锁,从而导致-1

接下来我们使用redis来完成分布式锁

3.怎么使用分布式锁

修改业务层

@Service
public class StockService02 {

    @Autowired
    private StockDao stockDao;

    @Autowired
    private RedisTemplate redisTemplate;
    public String decrement(Integer productid) {

        ValueOperations forValue = redisTemplate.opsForValue();
        //setIfAbsent如果指定的key不存在存入,存在则不存入
        //如果返回true表示获取锁成功,返回的是false表示获取锁失败
        Boolean flag = forValue.setIfAbsent("product::" + productid, "xxxx", 30, TimeUnit.SECONDS);
        if (flag){
            try {
                int num = stockDao.findById(productid);
                if (num > 0) {
                    stockDao.update(productid);
                    System.out.println("商品编号为:" + productid + "的商品库存剩余:" + (num - 1) + "个");
                    return "商品编号为:" + productid + "的商品库存剩余:" + (num - 1) + "个";
                } else {
                    System.out.println("商品编号为:" + productid + "的商品库存不足。");
                    return "商品编号为:" + productid + "的商品库存不足。";
                }
            }finally {
                //执行完释放锁资源
                redisTemplate.delete("product::" + productid);
            }


        }else {
            try {
                //如果没获取到锁睡眠100毫秒重新执行
                Thread.sleep(100);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }

        return decrement(productid);
    }
}

测试发现问题解决:

但这里还是有缺陷,当我们的程序执行时间超过redis锁的时间时,会出现bug。

如何解决该问题:使用第三方插件redisson【底层是基于redis完成的--提供了一个看门狗机制】

4.如何使用redisson

依赖:

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

创建一个RedissonClient对象--交于spring容器管理 :

@Configuration
public class MyRedissonConfiguration {
    @Bean //返回的对象交于spring容器来管理
    public RedissonClient redissonClient(){
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.61.223:6379");
        RedissonClient redissonClient = Redisson.create(config);
        return redissonClient;
    }
}

使用RedissonClient对象获取锁

@Service
public class StockService02 {

    @Autowired
    private StockDao stockDao;

    @Autowired
    private RedissonClient redissonClient;

    public String decrement(Integer productid) {
        //获取指定锁对象
        RLock lock = redissonClient.getLock("product::" + productid);
        //加锁
        lock.lock(30,TimeUnit.SECONDS);
        try {
            int num = stockDao.findById(productid);
            if (num > 0) {
                stockDao.update(productid);
                System.out.println("商品编号为:" + productid + "的商品库存剩余:" + (num - 1) + "个");
                return "商品编号为:" + productid + "的商品库存剩余:" + (num - 1) + "个";
            } else {
                System.out.println("商品编号为:" + productid + "的商品库存不足。");
                return "商品编号为:" + productid + "的商品库存不足。";
            }
        }finally {
            //释放锁
            lock.unlock();
        }

    }
}

3.redis常见的面试题

1.redis工作中的使用场景。

-可以作为热点数据的缓存

-可以解决分布式锁

-作为限时任务的操作

-商品的排行榜

2.redis支持的数据类

-redis支持很多数据类型,而我们在工作中主要使用的是String、Hash、List、Set、SortedSet

3.redis持久化方式

-RDB:快照存储,每一段时间对redis内存中的数据进行快照存储

-AOF:日志追加,当执行写操作时会通过write函数记录到日志文件中

4.redis缓存穿透是什么?以及如何解决缓存穿透

-什么是缓存穿透:数据库中没有该数据,缓存中也没有该数据,这时有人恶意访问这种数据。

-解决方案:

1、在控制层对一些不合法的数据进行校验。

2、使用布隆过滤器。把数据库中存在的id记录到一个大的bitmap数组中,当查询一个不存在的id是就会被该过滤器过滤掉。

3、可以把查询到的空数据也存入到缓存中。但是这个对象的储存时间不能太长,一般不超过5分钟

5.redis缓存雪崩?以及如何解决缓存雪崩?

1.什么是缓存雪崩:缓存雪崩就是缓存中出现大量数据过期的现象,而就在这时有大量的请求访问这些数据。压力顶到数据库。从而造成数据库压力过大。

-项目刚上线时可能会遇到些问题。解决办法:先把数据存放到缓存中

-缓存中的数据在某个时间段内出现大量过期,解决办法:可以设置散列的过期时间,也就是让这些缓存分批过期

-redis宕机时,解决办法搭建redis集群

6.如何保证缓存数据和数据库数据一致

1.合理设置缓存的过期时间。2.在添加修改删除时同步修改缓存数据。

7.redis内存淘汰策略--修改redis.conf配置文件可以改变淘汰策略

1.volatile-lru:从已设置过期时间的数据集中挑选最近使用次数最少的数据淘汰

2.volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰

3.volatile-random:从已设置过期时间的数据集中随机挑选数据淘汰

4.allkeys-lru:从数据集中挑选最近使用次数最少的数据淘汰

5.allkeys-random:从数据集中随机选择数据淘汰

6.no-enviction:禁止驱逐数据

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值