目录

1.StringRedisTemplate

2.RedisTemplate

2. redis的使用场景

2.1 作为缓存

2.2.分布式锁

2.2.1.idea搭建集群项目

3.redis的解决分布式锁的bug

4. redis中常见的面试题

4.1. 什么是缓存穿透?怎么解决?

4.2. 什么是缓存击穿?如何解决?

4.3. 什么是缓存雪崩?如何解决?

4.4. Redis 淘汰策略有哪些?


springboot对redis的操作封装了两个模板工具类, StringRedisTemplate和RedisTemplate类,StringRedisTemplate是RedisTemplate的子类,StringRedisTemplate它只能存储字符串类型,无法存储对象类型。要想用StringRedisTemplate存储对象必须把对象转为json字符串。

1.StringRedisTemplate

(1)引入相关依赖

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

(2)配置文件   前提记得开redis服务

#redis的配置
spring.redis.host=192.168.19.151
spring.redis.port=7777
spring.redis.jedis.pool.max-active=20
spring.redis.jedis.pool.max-wait=20000
spring.redis.jedis.pool.max-idle=10
spring.redis.jedis.pool.min-idle=5

(3)注入StringRedisTemplate该类对象

 @Autowired
 private StringRedisTemplate redisTemplate;

(4)使用StringRedisTemplate

该类把对每种数据类型的操作,单独封了相应的内部类。

Spring内部存在一个类可以把java对象转化json字符串    ObjectMapper类2.2
package com.wzh;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sun.scenario.effect.impl.sw.sse.SSEBlend_SRC_OUTPeer;
import com.wzh.entity.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import redis.clients.jedis.*;

import javax.lang.model.element.VariableElement;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.TimeUnit;

@SpringBootTest
class RedisJedisSpringbootApplicationTests02 {
    //里面所有的key还是value  field它的类型必须都是string类型
    //因为key和value获取field他们使用的都是String的序列化方式
    @Autowired
    private StringRedisTemplate redisTemplate;

    @Test
    public void test01(){
        //对hash类型的操作
        HashOperations<String, Object, Object> forHash = redisTemplate.opsForHash();
        forHash.put("k1","name","王振华");
        forHash.put("k1","age","15");
        Map<String,String> map = new HashMap<>();
        map.put("name","张三");
        map.put("age","15");
        forHash.putAll("k2",map);

        Object o = forHash.get("k1","name");
        System.out.println(o);

        Set<Object> keys = forHash.keys("k1");
        System.out.println(keys);
        List<Object> values = forHash.values("k1");
        System.out.println(values);

        //获取k1对应的所有的field和value
        Map<Object, Object> k1 = forHash.entries("k1");
        System.out.println(k1);

    }

    @Test
    public void test02() throws JsonProcessingException {
        //删除指定的key
        Boolean k1 = redisTemplate.delete("k1");
        //查看所有的key
        Set<String> keys = redisTemplate.keys("*");
        System.out.println(keys);
        //是否存在指定的key
        Boolean k11 = redisTemplate.hasKey("k1");
        System.out.println(k11);

        //redisTemplate对每种数据类型都封装了一个新的类。而这些新的类有对应的操作方法。
        //操作字符串的类
        ValueOperations<String, String> forValue = redisTemplate.opsForValue();
        //存储字符串类型--key value long unit    相当于redis中的setex()
        forValue.set("k1","6666",30, TimeUnit.SECONDS);

        //等价于setnx   存入成功返回true  失败则返回false
        Boolean absent = forValue.setIfAbsent("k2", "7777", 30, TimeUnit.SECONDS);
        System.out.println(absent);

        //追加
        Integer append = forValue.append("k2", "9999");
        System.out.println(forValue.get("k2"));

        //存储对象类型
        //Spring内部存在一个类可以把java对象转化json字符串
        ObjectMapper objectMapper=new ObjectMapper();
        User user = new User(11, "李四", 15);
        String s = objectMapper.writeValueAsString(user);
        forValue.set("k3",s);

        String k3 = forValue.get("k3");
        //把json字符串转化为对应的类对象
        User user1 = objectMapper.readValue(k3, User.class);
        System.out.println(user1);
    }
}

2.RedisTemplate

当你存储的value类型为对象类型使用redisTemplate
存储的value类型为字符串。使用StringRedisTemplate  比如验证码

package com.wzh;

import com.wzh.entity.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

@SpringBootTest
class RedisJedisSpringbootApplicationTests03 {
   //当你存储的value类型为对象类型使用redisTemplate
    //存储的value类型为字符串。StringRedisTemplate  比如验证码
    @Autowired
    public RedisTemplate redisTemplate;

    @Test
    public void test01(){
        //必须人为指定序列化方式
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        //redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<Object>(Object.class));
        //这两个序列化比较常用    Jackson2JsonRedisSerializer占用的内存小一点
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());

        //对String类型操作类
        ValueOperations forValue = redisTemplate.opsForValue();
        //redis中key和value都变成乱码了
        //key和value都没有指定序列化方式,默认采用jdk的序列化方式
        forValue.set("k1","v1");

        //value默认采用jdk,类必须实现序列化接口
        forValue.set("k2",new User(15,"张三",22));
    }
}

当RedisTemplate的value值是对象类型时,类需要实现Serializable接口,不然会乱码,因为默认会采用jdk序列化,上面存储String类型时没有乱码的原因是因为String类实现序列化接口了。

上面的RedisTemplate需要每次都指定key value以及field的序列化方式,能不能搞一个配置类,已经为RedisTemplate指定好序列化。以后再用就无需指定。

package com.wzh.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

/**
 * @ProjectName: redis-jedis-springboot
 * @Package: com.wzh.config
 * @ClassName: RedisConfig
 * @Author: 王振华
 * @Description:
 * @Date: 2022/8/2 20:06
 * @Version: 1.0
 */
@Configuration  //标记该类为配置类
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        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);
        template.setConnectionFactory(factory);
        //key序列化方式
        template.setKeySerializer(redisSerializer);
        //value序列化
        template.setValueSerializer(jackson2JsonRedisSerializer);
        //value hashmap序列化  filed value
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.setHashKeySerializer(redisSerializer);
        return template;
    }
}

 这样上面的就不需要设置序列化方式了

2. redis的使用场景

2.1 作为缓存

(1)数据存储在内存中,数据查询速度快。可以分摊数据库压力。

(2)什么样的数据适合放入缓存

查询频率比较高,修改频率比较低。

安全系数低的数据

(3)使用redis作为缓存

这里我们用到了mybatis-plus所以需要引入依赖

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

controller层:

这里有个知识点,我们可以通过   @GetMapping(value = "getById/{id}")  传参,public CommonResult getById(@PathVariable("id") Integer id)   获取获取请求映射中{}中的值。

这样的好处是安全  ,这样他们就不知道参数 1 是什么  ,如果有id = 1 聪明的人一眼就知道了,所以这种方式现在比较流行

package com.wzh.controller;

import com.wzh.entity.User;
import com.wzh.service.UserService;
import com.wzh.utils.CommonResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
 * @ProjectName: redis-jedis-springboot
 * @Package: com.wzh.controller
 * @ClassName: UserController
 * @Author: 王振华
 * @Description:
 * @Date: 2022/8/2 20:36
 * @Version: 1.0
 */
@RestController
@RequestMapping("order")
public class UserController {
    @Autowired
    private UserService userService;

    //order/getById/1
    //@PathVariable:获取请求映射中{}中的值
    @GetMapping(value = "getById/{id}")
    public CommonResult getById(@PathVariable("id") Integer id){
        return userService.findById(id);
    }

    @GetMapping("deleteById/{id}")
    public CommonResult deletebyId(@PathVariable("id") Integer id){
        return userService.deleteById(id);
    }

    @PostMapping("update")
    public CommonResult update(@RequestBody User user){
        return userService.update(user);
    }

    @PostMapping("insert")
    public CommonResult insert(@RequestBody User user){
        return userService.insert(user);
    }
}

service:

package com.wzh.service;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.wzh.entity.User;
import com.wzh.utils.CommonResult;

/**
 * @ProjectName: redis-jedis-springboot
 * @Package: com.wzh.service
 * @ClassName: UserService
 * @Author: 王振华
 * @Description:
 * @Date: 2022/8/2 20:41
 * @Version: 1.0
 */
public interface UserService{
    CommonResult findById(Integer id);

    CommonResult deleteById(Integer id);

    CommonResult insert(User user);

    CommonResult update(User user);
}

serviceImpl:

/*
package com.wzh.service.impl;

*/

import com.wzh.entity.User;
import com.wzh.mapper.UserMapper;
import com.wzh.service.UserService;
import com.wzh.utils.CommonResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;

/**
 * @ProjectName: redis-jedis-springboot
 * @Package: com.wzh.service.impl
 * @ClassName: UserServiceImpl
 * @Author: 王振华
 * @Description:
 * @Date: 2022/8/2 20:41
 * @Version: 1.0
 *//*
*/

@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RedisTemplate redisTemplate;

    //业务代码

    @Override
    public CommonResult findById(Integer id){
        ValueOperations forValue = redisTemplate.opsForValue();
        //查询缓存
        Object o = forValue.get("user::" + id);
        //缓存命中
        if(o!=null){
            return (CommonResult) o;
        }
        //未命中 查询数据库
        User user = userMapper.selectById(id);
        if(user!=null){
            //存入缓存
            forValue.set("user::"+id,new CommonResult(2000,"查询成功",user),30,TimeUnit.MINUTES);
        }else{
            return new CommonResult(5000,"查询失败",null);
        }
        return new CommonResult(2000,"查询成功",user);

    }

    @Override
    public CommonResult deleteById(Integer id){
        redisTemplate.delete("user::"+id);
        int i = userMapper.deleteById(id);
        return new CommonResult(2000,"删除成功",i);
    }
    //@Transactional
    @Override
    public CommonResult update(User user){
        //ValueOperations forValue = redisTemplate.opsForValue();
        //forValue.set("user::"+user.getId(),user,2, TimeUnit.HOURS);
        redisTemplate.delete("user::"+user.getId());
        int i = userMapper.updateById(user);
        return new CommonResult(2000,"修改成功",i);
    }

    @Override
    public CommonResult insert(User user){
        int insert = userMapper.insert(user);
        return new CommonResult(2000,"添加成功",insert);
    }

}
/*

mapper:

package com.wzh.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.wzh.entity.User;
import org.apache.ibatis.annotations.Mapper;

/**
 * @ProjectName: redis-jedis-springboot
 * @Package: com.wzh.mapper
 * @ClassName: UserMapper
 * @Author: 王振华
 * @Description:
 * @Date: 2022/8/2 20:44
 * @Version: 1.0
 */
@Mapper
public interface UserMapper extends BaseMapper<User> {
}

replication.properties:

#redis的配置
spring.redis.host=192.168.19.151
spring.redis.port=7777
spring.redis.jedis.pool.max-active=20
spring.redis.jedis.pool.max-wait=20000
spring.redis.jedis.pool.max-idle=10
spring.redis.jedis.pool.min-idle=5

server.port=8080
#mysql配置
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/mydb?serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=123456

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

查看的缓存: 前部分代码相同@before通知,后部分代码也相同后置通知。 我们可以AOP完成缓存代码和业务代码分离。

spring框架它应该也能想到。--使用注解即可完成。解析该注解。

(1)把缓存的配置类加入

package com.wzh.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

/**
 * @ProjectName: redis-jedis-springboot
 * @Package: com.wzh.config
 * @ClassName: RedisConfig
 * @Author: 王振华
 * @Description:
 * @Date: 2022/8/2 20:06
 * @Version: 1.0
 */
@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;
    }
}

上面不是已经配置过一次了吗,为什么还需要再配置,是因为上面的都是手动创建的RedisTeplate类对象redisTemplate.opsForValue(),可是这里我们使用注解的话,都是spring容器帮我们创建的,所以需要重新配置。他的缺点就是灵活性差,缓存过期时间都是统一的,不能根据客户的需要设置,可以使用AOP自己写也能实现。

(2)主启动类开启缓存注解

(3)使用注解  

package com.wzh.service.impl;

import com.wzh.entity.User;
import com.wzh.mapper.UserMapper;
import com.wzh.service.UserService;
import com.wzh.utils.CommonResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.concurrent.TimeUnit;

/**
 * @ProjectName: redis-jedis-springboot
 * @Package: com.wzh.service.impl
 * @ClassName: UserServiceImpl
 * @Author: 王振华
 * @Description:
 * @Date: 2022/8/2 20:41
 * @Version: 1.0
 */
@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RedisTemplate redisTemplate;

    //业务代码
    //使用查询注解:cacheNames表示缓存的名称 key:唯一标志---user::key
    //先从缓存中查看key为(cacheNames::key)是否存在,如果存在则不会执行方法体,如果不存在则执行方法体并把方法的返回值存入缓存中
    @Override
    @Cacheable(cacheNames = {"user"},key = "#id")
    public CommonResult findById(Integer id){
        User user = userMapper.selectById(id);
        return new CommonResult(2000,"查询成功",user);

    }

    @Override
    //先删除缓存在执行方法体。
    @CacheEvict(cacheNames = {"user"},key = "#id")
    public CommonResult deleteById(Integer id){
        int i = userMapper.deleteById(id);
        return new CommonResult(2000,"删除成功",i);
    }
    //@Transactional
    @Override
    //这个注释可以确保方法被执行,同时方法的返回值也被记录到缓存中,实现缓存与数据库的同步更新。
    @CachePut(cacheNames = {"user"},key = "#user.id")
    public CommonResult update(User user){
        int i = userMapper.updateById(user);
        if(i!=0) {
            return new CommonResult(2000, "成功", user);
        }else {
            return new CommonResult(5000, "失败", i);
        }
    }

    @Override
    public CommonResult insert(User user){
        int insert = userMapper.insert(user);
        return new CommonResult(2000,"添加成功",insert);
    }

}

2.2.分布式锁

我们用这个项目来示范多线程并发带来的问题:

controller:

package com.ykq.distrinctlock.controller;

import com.ykq.distrinctlock.service.ProductStockService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpRequest;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

@RestController
@RequestMapping("productStock")
public class ProductStockController {
    @Autowired
    private ProductStockService productStockService;
    //减库存
    @RequestMapping("decreaseStock/{productId}")
    public String decreaseStock(@PathVariable("productId") Integer productId){
        return productStockService.decreaseStock(productId);
    }
}

service:

package com.ykq.distrinctlock.service;
public interface ProductStockService {
    //减少库存
    public String decreaseStock( Integer productId);
}

serviceImpl:

package com.ykq.distrinctlock.service.impl;

import com.ykq.distrinctlock.dao.ProductStockDao;
import com.ykq.distrinctlock.service.ProductStockService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class ProductStockServiceImpl2 implements ProductStockService {
    @Autowired
    private ProductStockDao productStockDao;

    @Override
    public  String decreaseStock(Integer productId) {
           
                  //查看该商品的库存数量
                  Integer stock = productStockDao.findStockByProductId(productId);
                  if (stock > 0) {
                      //修改库存每次-1
                      productStockDao.updateStockByProductId(productId);
                      System.out.println("扣减成功!剩余库存数:" + (stock - 1));
                      return "success";
                  } else {
                      System.out.println("扣减失败!库存不足!");
                      return "fail";
                  }
              

    }
}

dao:

package com.ykq.distrinctlock.dao;

import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface ProductStockDao {

    public Integer findStockByProductId(Integer id);

    public void updateStockByProductId(Integer id);
}

ProductStockMapper.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ykq.distrinctlock.dao.ProductStockDao">

  <select id="findStockByProductId" resultType="integer">
     select num from tbl_stock where productId=#{productId}
  </select>

  <update id="updateStockByProductId">
      update tbl_stock set num=num-1  where productId=#{productId}
  </update>
</mapper>

application.properties:

server.port=8001
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/distrinct_lock?serverTimezone=Asia/Shanghai
mybatis.mapper-locations=classpath:/mapper/*.xml

spring.redis.host=192.168.19.151
spring.redis.port=7777





pom.xml:

<dependencies>
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.13.4</version>
        </dependency>
        <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>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.3</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </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>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

使用压测工具jmeter测试高并发下带来线程安全问题

 我们看到同一个库存被使用了n次。以及数据库中库存为负数。 线程安全问题导致。

解决方案: 使用 synchronized 或者lock锁

package com.ykq.distrinctlock.service.impl;

import com.ykq.distrinctlock.dao.ProductStockDao;
import com.ykq.distrinctlock.service.ProductStockService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class ProductStockServiceImpl2 implements ProductStockService {
    @Autowired
    private ProductStockDao productStockDao;

    @Override
    public  String decreaseStock(Integer productId) {
              synchronized (this) {
                  //查看该商品的库存数量
                  Integer stock = productStockDao.findStockByProductId(productId);
                  if (stock > 0) {
                      //修改库存每次-1
                      productStockDao.updateStockByProductId(productId);
                      System.out.println("扣减成功!剩余库存数:" + (stock - 1));
                      return "success";
                  } else {
                      System.out.println("扣减失败!库存不足!");
                      return "fail";
                  }
              }

    }
}

2.2.1.idea搭建集群项目

使用synchronized 或者lock锁 如果我们搭建了项目集群,那么该锁无效。

这里我们用idea开集群项目

(1)创建另外一个tomcat

 

 (2)配置nginx.conf并开启nginx(这里我们下载了window版的nginx)   建议不要下载到中文路径下

记得修改测压的端口号跟上边保持一致

 (3)开启两个项目

再次压测,发现又出现: 重复数字以及库存为负数。

 我们可以使用 redis作为锁  ,来获取锁和释放锁

package com.ykq.distrinctlock.service.impl;

import com.ykq.distrinctlock.dao.ProductStockDao;
import com.ykq.distrinctlock.service.ProductStockService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;

@Service
public class ProductStockServiceImpl_redis implements ProductStockService {
    @Autowired
    private ProductStockDao productStockDao;

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Override
    public  String decreaseStock(Integer productId) {
        ValueOperations<String, String> forValue = redisTemplate.opsForValue();
        //必须保证一开始没有该key 也就是说flag必须为true
        Boolean flag = forValue.setIfAbsent("aaa::" + productId, "~~~~~~~~~~~~~~~~~~~~~~");
            if(flag) {
                try {
                //查看该商品的库存数量
                Integer stock = productStockDao.findStockByProductId(productId);
                if (stock > 0) {

                        //修改库存每次-1
                        productStockDao.updateStockByProductId(productId);
                        System.out.println("扣减成功!剩余库存数:" + (stock - 1));
                        return "success";

                } else {
                    System.out.println("扣减失败!库存不足!");
                    return "fail";
                }
                }finally {
                    redisTemplate.delete("aaa::" + productId);
                }

            }


        return "服务器正忙,请稍后在试......";
    }
}

记得开启redis服务,不然无法存储

 这里我们发现没有重复和负数,我们的问题就解决了!!!! 

3.redis的解决分布式锁的bug

Redis分布式锁不能解决超时的问题,分布式锁有一个超时时间,程序的执行如果超出了锁的超时时间就会出现问题。

 可以使用:redission依赖,redission解决redis超时问题的原理

为持有锁的线程开启一个守护线程,守护线程会每隔10秒检查当前线程是否还持有锁,如果持有则延迟生存时间。

使用:

(1)引入redisson依赖

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

(2)配置redission对象并交于spring容器管理

    @Bean
    public Redisson redisson(){
        Config config =new Config();
        config.useSingleServer().
                setAddress("redis://localhost:6379").
                //redis默认有16个数据库
                setDatabase(0);
        return (Redisson) Redisson.create(config);
    }

 

这里我们因为方便,就不用linux的redis了,我们在window上下载了个redis,跟linux上的使用是一样的

测试:

package com.ykq.distrinctlock.service.impl;

import com.ykq.distrinctlock.dao.ProductStockDao;
import com.ykq.distrinctlock.service.ProductStockService;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
public class ProductStockServiceImpl_redisson implements ProductStockService {
    @Autowired
    private ProductStockDao productStockDao;

    @Autowired
    private Redisson redisson;

    @Override
    public  String decreaseStock(Integer productId) {
        //获取锁对象
        RLock lock = redisson.getLock("aaa::" + productId);
        try {
            //加锁
            lock.lock(30,TimeUnit.SECONDS);
            //lock.trylock(30,TimeUnit.SECONDS);
            //查看该商品的库存数量
            Integer stock = productStockDao.findStockByProductId(productId);
            if (stock > 0) {

                //修改库存每次-1
                productStockDao.updateStockByProductId(productId);
                System.out.println("扣减成功!剩余库存数:" + (stock - 1));
                return "success";

            } else {
                System.out.println("扣减失败!库存不足!");
                return "fail";
            }
        }finally{
            //释放锁
            lock.unlock();  
        }
    }
}

缓存双写不一致  redission读写锁 

4. redis中常见的面试题

4.1. 什么是缓存穿透?怎么解决?

1. 数据库中没有该记录,缓存中也没有该记录,这时由人恶意大量访问这样的数据。这样就会导致该请求绕过缓存,直接访问数据,从而造成数据库压力过大。

2.解决办法:
   [1]在controller加数据校验。
   [2]我们可以在redis中存入一个空对象,而且要设置过期时间不能太长。超过5分钟
   [3]我们使用布隆过滤器。底层:有一个bitmap数组,里面存储了该表的所有id.

建议三种都使用:

Controller层 判断数据是否有意义

 这里只对查找方法进行了优化

@Override
    public CommonResult findById(Integer id){
        //创建布隆过滤器对象
        BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), 50,0.05);
        //存放1-50的id号
        for (int i = 1; i <= 50; i++) {
            bloomFilter.put(i);
        }
        ValueOperations forValue = redisTemplate.opsForValue();
        //查询缓存
        Object o = forValue.get("user::" + id);

        //缓存命中
        if(o!=null){
            // 判断对象是否属于该类型
            if(!(o instanceof NullObject)) {
                return (CommonResult) o;
            }
            return null;
        }
        //未命中
        if(bloomFilter.mightContain(id)){   //查看布隆过滤器是否存在
            User user = userMapper.selectById(id);
            if(user!=null){
                //存入缓存
                forValue.set("user::"+id,new CommonResult(2000,"查询成功",user),30, TimeUnit.MINUTES);
                return new CommonResult(2000,"查询成功",user);
            }else{
                //在redis中存入一个空对象,小于5分钟   避免缓存穿透
                forValue.set("user::"+id,new NullObject(),5, TimeUnit.MINUTES);
                return new CommonResult(5000,"查询失败",null);
            }
        }
        return null;

    }

//伪代码
String get(String key) { //布隆过滤器钟存储的是数据库表钟对应的id
    String value = redis.get(key);  //先从缓存获取。  
    if (value  == null) { //缓存没有命中
        if(!bloomfilter.mightContain(key)){//查看布隆过滤器中是否存在
            return null;q
        }else{
            value = db.get(key); //查询数据库
            redis.set(key, value); 
        }    
    }
    return value;
}

4.2. 什么是缓存击穿?如何解决?

缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。

缓存击穿解决方案:
1.设置永久不过期。【这种只适合内存】
2.使用互斥锁(mutex key)业界比较常用的做法。

//伪代码
public String get(key) {
      String value = redis.get(key);
      if (value == null) { //代表缓存值过期
          //设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
      if (redis.setnx(key_mutex, 1, 3 * 60) == 1) {  //代表设置成功
               value = db.get(key);
                      redis.set(key, value, expire_secs);
                      redis.del(key_mutex);
              } else {  //这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可
                      sleep(50);
                      get(key);  //重试
              }
          } else {
              return value;      
          }
 }

4.3. 什么是缓存雪崩?如何解决?

缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是, 缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
1.什么下会发生缓存雪崩:
  [1]项目刚上线,缓存中没有任何数据
  [2]缓存出现大量过期。
  [3]redis宕机
  
2.解决办法: 
   1.上线前预先把一些热点数据放入缓存。
   2.设置过期时间为散列值
   3.搭建redis集群

4.4. Redis 淘汰策略有哪些?

 默认是从已设置过期时间的数据集中挑选最近最少使用的数据淘汰

在redis.conf中配置

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐