• springboot整合redis ---- 缓存 分布锁 Redssion解决分布锁的bug(超时问题) redis常见面试题


    目录

    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)引入相关依赖

    1. <dependency>
    2. <groupId>org.springframework.bootgroupId>
    3. <artifactId>spring-boot-starter-data-redisartifactId>
    4. dependency>

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

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

    (3)注入StringRedisTemplate该类对象

    1. @Autowired
    2. private StringRedisTemplate redisTemplate;

    (4)使用StringRedisTemplate

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

    Spring内部存在一个类可以把java对象转化json字符串    ObjectMapper类2.2
    1. package com.wzh;
    2. import com.fasterxml.jackson.core.JsonProcessingException;
    3. import com.fasterxml.jackson.databind.ObjectMapper;
    4. import com.sun.scenario.effect.impl.sw.sse.SSEBlend_SRC_OUTPeer;
    5. import com.wzh.entity.User;
    6. import org.junit.jupiter.api.Test;
    7. import org.springframework.beans.factory.annotation.Autowired;
    8. import org.springframework.boot.test.context.SpringBootTest;
    9. import org.springframework.data.redis.core.HashOperations;
    10. import org.springframework.data.redis.core.StringRedisTemplate;
    11. import org.springframework.data.redis.core.ValueOperations;
    12. import redis.clients.jedis.*;
    13. import javax.lang.model.element.VariableElement;
    14. import java.time.Duration;
    15. import java.util.*;
    16. import java.util.concurrent.TimeUnit;
    17. @SpringBootTest
    18. class RedisJedisSpringbootApplicationTests02 {
    19. //里面所有的key还是value field它的类型必须都是string类型
    20. //因为key和value获取field他们使用的都是String的序列化方式
    21. @Autowired
    22. private StringRedisTemplate redisTemplate;
    23. @Test
    24. public void test01(){
    25. //对hash类型的操作
    26. HashOperations forHash = redisTemplate.opsForHash();
    27. forHash.put("k1","name","王振华");
    28. forHash.put("k1","age","15");
    29. Map map = new HashMap<>();
    30. map.put("name","张三");
    31. map.put("age","15");
    32. forHash.putAll("k2",map);
    33. Object o = forHash.get("k1","name");
    34. System.out.println(o);
    35. Set keys = forHash.keys("k1");
    36. System.out.println(keys);
    37. List values = forHash.values("k1");
    38. System.out.println(values);
    39. //获取k1对应的所有的field和value
    40. Map k1 = forHash.entries("k1");
    41. System.out.println(k1);
    42. }
    43. @Test
    44. public void test02() throws JsonProcessingException {
    45. //删除指定的key
    46. Boolean k1 = redisTemplate.delete("k1");
    47. //查看所有的key
    48. Set keys = redisTemplate.keys("*");
    49. System.out.println(keys);
    50. //是否存在指定的key
    51. Boolean k11 = redisTemplate.hasKey("k1");
    52. System.out.println(k11);
    53. //redisTemplate对每种数据类型都封装了一个新的类。而这些新的类有对应的操作方法。
    54. //操作字符串的类
    55. ValueOperations forValue = redisTemplate.opsForValue();
    56. //存储字符串类型--key value long unit 相当于redis中的setex()
    57. forValue.set("k1","6666",30, TimeUnit.SECONDS);
    58. //等价于setnx 存入成功返回true 失败则返回false
    59. Boolean absent = forValue.setIfAbsent("k2", "7777", 30, TimeUnit.SECONDS);
    60. System.out.println(absent);
    61. //追加
    62. Integer append = forValue.append("k2", "9999");
    63. System.out.println(forValue.get("k2"));
    64. //存储对象类型
    65. //Spring内部存在一个类可以把java对象转化json字符串
    66. ObjectMapper objectMapper=new ObjectMapper();
    67. User user = new User(11, "李四", 15);
    68. String s = objectMapper.writeValueAsString(user);
    69. forValue.set("k3",s);
    70. String k3 = forValue.get("k3");
    71. //把json字符串转化为对应的类对象
    72. User user1 = objectMapper.readValue(k3, User.class);
    73. System.out.println(user1);
    74. }
    75. }
    76. 2.RedisTemplate

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

      1. package com.wzh;
      2. import com.wzh.entity.User;
      3. import org.junit.jupiter.api.Test;
      4. import org.springframework.beans.factory.annotation.Autowired;
      5. import org.springframework.boot.test.context.SpringBootTest;
      6. import org.springframework.data.redis.core.HashOperations;
      7. import org.springframework.data.redis.core.RedisTemplate;
      8. import org.springframework.data.redis.core.StringRedisTemplate;
      9. import org.springframework.data.redis.core.ValueOperations;
      10. import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
      11. import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
      12. import org.springframework.data.redis.serializer.StringRedisSerializer;
      13. import java.util.HashMap;
      14. import java.util.List;
      15. import java.util.Map;
      16. import java.util.Set;
      17. import java.util.concurrent.TimeUnit;
      18. @SpringBootTest
      19. class RedisJedisSpringbootApplicationTests03 {
      20. //当你存储的value类型为对象类型使用redisTemplate
      21. //存储的value类型为字符串。StringRedisTemplate 比如验证码
      22. @Autowired
      23. public RedisTemplate redisTemplate;
      24. @Test
      25. public void test01(){
      26. //必须人为指定序列化方式
      27. redisTemplate.setKeySerializer(new StringRedisSerializer());
      28. //redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer(Object.class));
      29. //这两个序列化比较常用 Jackson2JsonRedisSerializer占用的内存小一点
      30. redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
      31. //对String类型操作类
      32. ValueOperations forValue = redisTemplate.opsForValue();
      33. //redis中key和value都变成乱码了
      34. //key和value都没有指定序列化方式,默认采用jdk的序列化方式
      35. forValue.set("k1","v1");
      36. //value默认采用jdk,类必须实现序列化接口
      37. forValue.set("k2",new User(15,"张三",22));
      38. }
      39. }
      40. 当RedisTemplate的value值是对象类型时,类需要实现Serializable接口,不然会乱码,因为默认会采用jdk序列化,上面存储String类型时没有乱码的原因是因为String类实现序列化接口了。

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

        1. package com.wzh.config;
        2. import com.fasterxml.jackson.annotation.JsonAutoDetect;
        3. import com.fasterxml.jackson.annotation.PropertyAccessor;
        4. import com.fasterxml.jackson.databind.ObjectMapper;
        5. import org.springframework.cache.CacheManager;
        6. import org.springframework.context.annotation.Bean;
        7. import org.springframework.context.annotation.Configuration;
        8. import org.springframework.data.redis.cache.RedisCacheConfiguration;
        9. import org.springframework.data.redis.cache.RedisCacheManager;
        10. import org.springframework.data.redis.connection.RedisConnectionFactory;
        11. import org.springframework.data.redis.core.RedisTemplate;
        12. import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
        13. import org.springframework.data.redis.serializer.RedisSerializationContext;
        14. import org.springframework.data.redis.serializer.RedisSerializer;
        15. import org.springframework.data.redis.serializer.StringRedisSerializer;
        16. import java.time.Duration;
        17. /**
        18. * @ProjectName: redis-jedis-springboot
        19. * @Package: com.wzh.config
        20. * @ClassName: RedisConfig
        21. * @Author: 王振华
        22. * @Description:
        23. * @Date: 2022/8/2 20:06
        24. * @Version: 1.0
        25. */
        26. @Configuration //标记该类为配置类
        27. public class RedisConfig {
        28. @Bean
        29. public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
        30. RedisTemplate template = new RedisTemplate<>();
        31. RedisSerializer redisSerializer = new StringRedisSerializer();
        32. Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        33. ObjectMapper om = new ObjectMapper();
        34. om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        35. om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        36. jackson2JsonRedisSerializer.setObjectMapper(om);
        37. template.setConnectionFactory(factory);
        38. //key序列化方式
        39. template.setKeySerializer(redisSerializer);
        40. //value序列化
        41. template.setValueSerializer(jackson2JsonRedisSerializer);
        42. //value hashmap序列化 filed value
        43. template.setHashValueSerializer(jackson2JsonRedisSerializer);
        44. template.setHashKeySerializer(redisSerializer);
        45. return template;
        46. }
        47. }

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

        2. redis的使用场景

        2.1 作为缓存

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

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

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

        安全系数低的数据

        (3)使用redis作为缓存

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

        1. <dependency>
        2. <groupId>mysqlgroupId>
        3. <artifactId>mysql-connector-javaartifactId>
        4. dependency>
        5. <dependency>
        6. <groupId>com.baomidougroupId>
        7. <artifactId>mybatis-plus-boot-starterartifactId>
        8. <version>3.5.1version>
        9. dependency>

        controller层:

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

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

        1. package com.wzh.controller;
        2. import com.wzh.entity.User;
        3. import com.wzh.service.UserService;
        4. import com.wzh.utils.CommonResult;
        5. import org.springframework.beans.factory.annotation.Autowired;
        6. import org.springframework.web.bind.annotation.*;
        7. /**
        8. * @ProjectName: redis-jedis-springboot
        9. * @Package: com.wzh.controller
        10. * @ClassName: UserController
        11. * @Author: 王振华
        12. * @Description:
        13. * @Date: 2022/8/2 20:36
        14. * @Version: 1.0
        15. */
        16. @RestController
        17. @RequestMapping("order")
        18. public class UserController {
        19. @Autowired
        20. private UserService userService;
        21. //order/getById/1
        22. //@PathVariable:获取请求映射中{}中的值
        23. @GetMapping(value = "getById/{id}")
        24. public CommonResult getById(@PathVariable("id") Integer id){
        25. return userService.findById(id);
        26. }
        27. @GetMapping("deleteById/{id}")
        28. public CommonResult deletebyId(@PathVariable("id") Integer id){
        29. return userService.deleteById(id);
        30. }
        31. @PostMapping("update")
        32. public CommonResult update(@RequestBody User user){
        33. return userService.update(user);
        34. }
        35. @PostMapping("insert")
        36. public CommonResult insert(@RequestBody User user){
        37. return userService.insert(user);
        38. }
        39. }

        service:

        1. package com.wzh.service;
        2. import com.baomidou.mybatisplus.core.mapper.BaseMapper;
        3. import com.wzh.entity.User;
        4. import com.wzh.utils.CommonResult;
        5. /**
        6. * @ProjectName: redis-jedis-springboot
        7. * @Package: com.wzh.service
        8. * @ClassName: UserService
        9. * @Author: 王振华
        10. * @Description:
        11. * @Date: 2022/8/2 20:41
        12. * @Version: 1.0
        13. */
        14. public interface UserService{
        15. CommonResult findById(Integer id);
        16. CommonResult deleteById(Integer id);
        17. CommonResult insert(User user);
        18. CommonResult update(User user);
        19. }

        serviceImpl:

        1. /*
        2. package com.wzh.service.impl;
        3. */
        4. import com.wzh.entity.User;
        5. import com.wzh.mapper.UserMapper;
        6. import com.wzh.service.UserService;
        7. import com.wzh.utils.CommonResult;
        8. import org.springframework.beans.factory.annotation.Autowired;
        9. import org.springframework.data.redis.core.RedisTemplate;
        10. import org.springframework.data.redis.core.ValueOperations;
        11. import org.springframework.stereotype.Service;
        12. /**
        13. * @ProjectName: redis-jedis-springboot
        14. * @Package: com.wzh.service.impl
        15. * @ClassName: UserServiceImpl
        16. * @Author: 王振华
        17. * @Description:
        18. * @Date: 2022/8/2 20:41
        19. * @Version: 1.0
        20. *//*
        21. */
        22. @Service
        23. public class UserServiceImpl implements UserService {
        24. @Autowired
        25. private UserMapper userMapper;
        26. @Autowired
        27. private RedisTemplate redisTemplate;
        28. //业务代码
        29. @Override
        30. public CommonResult findById(Integer id){
        31. ValueOperations forValue = redisTemplate.opsForValue();
        32. //查询缓存
        33. Object o = forValue.get("user::" + id);
        34. //缓存命中
        35. if(o!=null){
        36. return (CommonResult) o;
        37. }
        38. //未命中 查询数据库
        39. User user = userMapper.selectById(id);
        40. if(user!=null){
        41. //存入缓存
        42. forValue.set("user::"+id,new CommonResult(2000,"查询成功",user),30,TimeUnit.MINUTES);
        43. }else{
        44. return new CommonResult(5000,"查询失败",null);
        45. }
        46. return new CommonResult(2000,"查询成功",user);
        47. }
        48. @Override
        49. public CommonResult deleteById(Integer id){
        50. redisTemplate.delete("user::"+id);
        51. int i = userMapper.deleteById(id);
        52. return new CommonResult(2000,"删除成功",i);
        53. }
        54. //@Transactional
        55. @Override
        56. public CommonResult update(User user){
        57. //ValueOperations forValue = redisTemplate.opsForValue();
        58. //forValue.set("user::"+user.getId(),user,2, TimeUnit.HOURS);
        59. redisTemplate.delete("user::"+user.getId());
        60. int i = userMapper.updateById(user);
        61. return new CommonResult(2000,"修改成功",i);
        62. }
        63. @Override
        64. public CommonResult insert(User user){
        65. int insert = userMapper.insert(user);
        66. return new CommonResult(2000,"添加成功",insert);
        67. }
        68. }
        69. /*

        mapper:

        1. package com.wzh.mapper;
        2. import com.baomidou.mybatisplus.core.mapper.BaseMapper;
        3. import com.wzh.entity.User;
        4. import org.apache.ibatis.annotations.Mapper;
        5. /**
        6. * @ProjectName: redis-jedis-springboot
        7. * @Package: com.wzh.mapper
        8. * @ClassName: UserMapper
        9. * @Author: 王振华
        10. * @Description:
        11. * @Date: 2022/8/2 20:44
        12. * @Version: 1.0
        13. */
        14. @Mapper
        15. public interface UserMapper extends BaseMapper {
        16. }

        replication.properties:

        1. #redis的配置
        2. spring.redis.host=192.168.19.151
        3. spring.redis.port=7777
        4. spring.redis.jedis.pool.max-active=20
        5. spring.redis.jedis.pool.max-wait=20000
        6. spring.redis.jedis.pool.max-idle=10
        7. spring.redis.jedis.pool.min-idle=5
        8. server.port=8080
        9. #mysql配置
        10. spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
        11. spring.datasource.url=jdbc:mysql://localhost:3306/mydb?serverTimezone=Asia/Shanghai
        12. spring.datasource.username=root
        13. spring.datasource.password=123456
        14. #日志
        15. mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

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

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

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

        1. package com.wzh.config;
        2. import com.fasterxml.jackson.annotation.JsonAutoDetect;
        3. import com.fasterxml.jackson.annotation.PropertyAccessor;
        4. import com.fasterxml.jackson.databind.ObjectMapper;
        5. import org.springframework.cache.CacheManager;
        6. import org.springframework.context.annotation.Bean;
        7. import org.springframework.context.annotation.Configuration;
        8. import org.springframework.data.redis.cache.RedisCacheConfiguration;
        9. import org.springframework.data.redis.cache.RedisCacheManager;
        10. import org.springframework.data.redis.connection.RedisConnectionFactory;
        11. import org.springframework.data.redis.core.RedisTemplate;
        12. import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
        13. import org.springframework.data.redis.serializer.RedisSerializationContext;
        14. import org.springframework.data.redis.serializer.RedisSerializer;
        15. import org.springframework.data.redis.serializer.StringRedisSerializer;
        16. import java.time.Duration;
        17. /**
        18. * @ProjectName: redis-jedis-springboot
        19. * @Package: com.wzh.config
        20. * @ClassName: RedisConfig
        21. * @Author: 王振华
        22. * @Description:
        23. * @Date: 2022/8/2 20:06
        24. * @Version: 1.0
        25. */
        26. @Configuration //标记该类为配置类
        27. public class RedisConfig {
        28. @Bean
        29. public CacheManager cacheManager(RedisConnectionFactory factory) {
        30. RedisSerializer redisSerializer = new StringRedisSerializer();
        31. Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        32. //解决查询缓存转换异常的问题
        33. ObjectMapper om = new ObjectMapper();
        34. om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        35. om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        36. jackson2JsonRedisSerializer.setObjectMapper(om);
        37. // 配置序列化(解决乱码的问题),过期时间600秒
        38. RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
        39. .entryTtl(Duration.ofSeconds(600)) //缓存过期10分钟 ---- 业务需求。
        40. .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))//设置key的序列化方式
        41. .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)) //设置value的序列化
        42. .disableCachingNullValues();
        43. RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
        44. .cacheDefaults(config)
        45. .build();
        46. return cacheManager;
        47. }
        48. }

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

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

        (3)使用注解  

        1. package com.wzh.service.impl;
        2. import com.wzh.entity.User;
        3. import com.wzh.mapper.UserMapper;
        4. import com.wzh.service.UserService;
        5. import com.wzh.utils.CommonResult;
        6. import org.springframework.beans.factory.annotation.Autowired;
        7. import org.springframework.cache.annotation.CacheEvict;
        8. import org.springframework.cache.annotation.CachePut;
        9. import org.springframework.cache.annotation.Cacheable;
        10. import org.springframework.data.redis.core.RedisTemplate;
        11. import org.springframework.data.redis.core.ValueOperations;
        12. import org.springframework.stereotype.Service;
        13. import org.springframework.transaction.annotation.Transactional;
        14. import java.util.concurrent.TimeUnit;
        15. /**
        16. * @ProjectName: redis-jedis-springboot
        17. * @Package: com.wzh.service.impl
        18. * @ClassName: UserServiceImpl
        19. * @Author: 王振华
        20. * @Description:
        21. * @Date: 2022/8/2 20:41
        22. * @Version: 1.0
        23. */
        24. @Service
        25. public class UserServiceImpl implements UserService {
        26. @Autowired
        27. private UserMapper userMapper;
        28. @Autowired
        29. private RedisTemplate redisTemplate;
        30. //业务代码
        31. //使用查询注解:cacheNames表示缓存的名称 key:唯一标志---user::key
        32. //先从缓存中查看key为(cacheNames::key)是否存在,如果存在则不会执行方法体,如果不存在则执行方法体并把方法的返回值存入缓存中
        33. @Override
        34. @Cacheable(cacheNames = {"user"},key = "#id")
        35. public CommonResult findById(Integer id){
        36. User user = userMapper.selectById(id);
        37. return new CommonResult(2000,"查询成功",user);
        38. }
        39. @Override
        40. //先删除缓存在执行方法体。
        41. @CacheEvict(cacheNames = {"user"},key = "#id")
        42. public CommonResult deleteById(Integer id){
        43. int i = userMapper.deleteById(id);
        44. return new CommonResult(2000,"删除成功",i);
        45. }
        46. //@Transactional
        47. @Override
        48. //这个注释可以确保方法被执行,同时方法的返回值也被记录到缓存中,实现缓存与数据库的同步更新。
        49. @CachePut(cacheNames = {"user"},key = "#user.id")
        50. public CommonResult update(User user){
        51. int i = userMapper.updateById(user);
        52. if(i!=0) {
        53. return new CommonResult(2000, "成功", user);
        54. }else {
        55. return new CommonResult(5000, "失败", i);
        56. }
        57. }
        58. @Override
        59. public CommonResult insert(User user){
        60. int insert = userMapper.insert(user);
        61. return new CommonResult(2000,"添加成功",insert);
        62. }
        63. }

        2.2.分布式锁

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

        controller:

        1. package com.ykq.distrinctlock.controller;
        2. import com.ykq.distrinctlock.service.ProductStockService;
        3. import org.springframework.beans.factory.annotation.Autowired;
        4. import org.springframework.http.HttpRequest;
        5. import org.springframework.web.bind.annotation.PathVariable;
        6. import org.springframework.web.bind.annotation.RequestMapping;
        7. import org.springframework.web.bind.annotation.RestController;
        8. import javax.servlet.http.HttpServletRequest;
        9. @RestController
        10. @RequestMapping("productStock")
        11. public class ProductStockController {
        12. @Autowired
        13. private ProductStockService productStockService;
        14. //减库存
        15. @RequestMapping("decreaseStock/{productId}")
        16. public String decreaseStock(@PathVariable("productId") Integer productId){
        17. return productStockService.decreaseStock(productId);
        18. }
        19. }

        service:

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

        serviceImpl:

        1. package com.ykq.distrinctlock.service.impl;
        2. import com.ykq.distrinctlock.dao.ProductStockDao;
        3. import com.ykq.distrinctlock.service.ProductStockService;
        4. import org.springframework.beans.factory.annotation.Autowired;
        5. import org.springframework.stereotype.Service;
        6. @Service
        7. public class ProductStockServiceImpl2 implements ProductStockService {
        8. @Autowired
        9. private ProductStockDao productStockDao;
        10. @Override
        11. public String decreaseStock(Integer productId) {
        12. //查看该商品的库存数量
        13. Integer stock = productStockDao.findStockByProductId(productId);
        14. if (stock > 0) {
        15. //修改库存每次-1
        16. productStockDao.updateStockByProductId(productId);
        17. System.out.println("扣减成功!剩余库存数:" + (stock - 1));
        18. return "success";
        19. } else {
        20. System.out.println("扣减失败!库存不足!");
        21. return "fail";
        22. }
        23. }
        24. }

        dao:

        1. package com.ykq.distrinctlock.dao;
        2. import org.apache.ibatis.annotations.Mapper;
        3. @Mapper
        4. public interface ProductStockDao {
        5. public Integer findStockByProductId(Integer id);
        6. public void updateStockByProductId(Integer id);
        7. }

        ProductStockMapper.xml:

        1. mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
        2. <mapper namespace="com.ykq.distrinctlock.dao.ProductStockDao">
        3. <select id="findStockByProductId" resultType="integer">
        4. select num from tbl_stock where productId=#{productId}
        5. select>
        6. <update id="updateStockByProductId">
        7. update tbl_stock set num=num-1 where productId=#{productId}
        8. update>
        9. mapper>

        application.properties:

        1. server.port=8001
        2. spring.datasource.username=root
        3. spring.datasource.password=123456
        4. spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
        5. spring.datasource.url=jdbc:mysql://localhost:3306/distrinct_lock?serverTimezone=Asia/Shanghai
        6. mybatis.mapper-locations=classpath:/mapper/*.xml
        7. spring.redis.host=192.168.19.151
        8. spring.redis.port=7777

        pom.xml:

        1. <dependencies>
        2. <dependency>
        3. <groupId>org.redissongroupId>
        4. <artifactId>redissonartifactId>
        5. <version>3.13.4version>
        6. dependency>
        7. <dependency>
        8. <groupId>org.springframework.bootgroupId>
        9. <artifactId>spring-boot-starter-data-redisartifactId>
        10. dependency>
        11. <dependency>
        12. <groupId>org.springframework.bootgroupId>
        13. <artifactId>spring-boot-starter-webartifactId>
        14. dependency>
        15. <dependency>
        16. <groupId>org.mybatis.spring.bootgroupId>
        17. <artifactId>mybatis-spring-boot-starterartifactId>
        18. <version>2.1.3version>
        19. dependency>
        20. <dependency>
        21. <groupId>org.springframework.bootgroupId>
        22. <artifactId>spring-boot-devtoolsartifactId>
        23. <scope>runtimescope>
        24. <optional>trueoptional>
        25. dependency>
        26. <dependency>
        27. <groupId>mysqlgroupId>
        28. <artifactId>mysql-connector-javaartifactId>
        29. <scope>runtimescope>
        30. dependency>
        31. <dependency>
        32. <groupId>org.springframework.bootgroupId>
        33. <artifactId>spring-boot-configuration-processorartifactId>
        34. <optional>trueoptional>
        35. dependency>
        36. <dependency>
        37. <groupId>org.projectlombokgroupId>
        38. <artifactId>lombokartifactId>
        39. <optional>trueoptional>
        40. dependency>
        41. <dependency>
        42. <groupId>org.springframework.bootgroupId>
        43. <artifactId>spring-boot-starter-testartifactId>
        44. <scope>testscope>
        45. <exclusions>
        46. <exclusion>
        47. <groupId>org.junit.vintagegroupId>
        48. <artifactId>junit-vintage-engineartifactId>
        49. exclusion>
        50. exclusions>
        51. dependency>
        52. dependencies>

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

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

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

        1. package com.ykq.distrinctlock.service.impl;
        2. import com.ykq.distrinctlock.dao.ProductStockDao;
        3. import com.ykq.distrinctlock.service.ProductStockService;
        4. import org.springframework.beans.factory.annotation.Autowired;
        5. import org.springframework.stereotype.Service;
        6. @Service
        7. public class ProductStockServiceImpl2 implements ProductStockService {
        8. @Autowired
        9. private ProductStockDao productStockDao;
        10. @Override
        11. public String decreaseStock(Integer productId) {
        12. synchronized (this) {
        13. //查看该商品的库存数量
        14. Integer stock = productStockDao.findStockByProductId(productId);
        15. if (stock > 0) {
        16. //修改库存每次-1
        17. productStockDao.updateStockByProductId(productId);
        18. System.out.println("扣减成功!剩余库存数:" + (stock - 1));
        19. return "success";
        20. } else {
        21. System.out.println("扣减失败!库存不足!");
        22. return "fail";
        23. }
        24. }
        25. }
        26. }

        2.2.1.idea搭建集群项目

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

        这里我们用idea开集群项目

        (1)创建另外一个tomcat

         

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

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

         (3)开启两个项目

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

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

        1. package com.ykq.distrinctlock.service.impl;
        2. import com.ykq.distrinctlock.dao.ProductStockDao;
        3. import com.ykq.distrinctlock.service.ProductStockService;
        4. import org.springframework.beans.factory.annotation.Autowired;
        5. import org.springframework.data.redis.core.StringRedisTemplate;
        6. import org.springframework.data.redis.core.ValueOperations;
        7. import org.springframework.stereotype.Service;
        8. @Service
        9. public class ProductStockServiceImpl_redis implements ProductStockService {
        10. @Autowired
        11. private ProductStockDao productStockDao;
        12. @Autowired
        13. private StringRedisTemplate redisTemplate;
        14. @Override
        15. public String decreaseStock(Integer productId) {
        16. ValueOperations forValue = redisTemplate.opsForValue();
        17. //必须保证一开始没有该key 也就是说flag必须为true
        18. Boolean flag = forValue.setIfAbsent("aaa::" + productId, "~~~~~~~~~~~~~~~~~~~~~~");
        19. if(flag) {
        20. try {
        21. //查看该商品的库存数量
        22. Integer stock = productStockDao.findStockByProductId(productId);
        23. if (stock > 0) {
        24. //修改库存每次-1
        25. productStockDao.updateStockByProductId(productId);
        26. System.out.println("扣减成功!剩余库存数:" + (stock - 1));
        27. return "success";
        28. } else {
        29. System.out.println("扣减失败!库存不足!");
        30. return "fail";
        31. }
        32. }finally {
        33. redisTemplate.delete("aaa::" + productId);
        34. }
        35. }
        36. return "服务器正忙,请稍后在试......";
        37. }
        38. }

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

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

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

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

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

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

        使用:

        (1)引入redisson依赖

        1. <dependency>
        2. <groupId>org.redissongroupId>
        3. <artifactId>redissonartifactId>
        4. <version>3.13.4version>
        5. dependency>

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

        1. @Bean
        2. public Redisson redisson(){
        3. Config config =new Config();
        4. config.useSingleServer().
        5. setAddress("redis://localhost:6379").
        6. //redis默认有16个数据库
        7. setDatabase(0);
        8. return (Redisson) Redisson.create(config);
        9. }

         

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

        测试:

        1. package com.ykq.distrinctlock.service.impl;
        2. import com.ykq.distrinctlock.dao.ProductStockDao;
        3. import com.ykq.distrinctlock.service.ProductStockService;
        4. import org.redisson.Redisson;
        5. import org.redisson.api.RLock;
        6. import org.springframework.beans.factory.annotation.Autowired;
        7. import org.springframework.data.redis.core.StringRedisTemplate;
        8. import org.springframework.data.redis.core.ValueOperations;
        9. import org.springframework.stereotype.Service;
        10. import java.util.concurrent.TimeUnit;
        11. @Service
        12. public class ProductStockServiceImpl_redisson implements ProductStockService {
        13. @Autowired
        14. private ProductStockDao productStockDao;
        15. @Autowired
        16. private Redisson redisson;
        17. @Override
        18. public String decreaseStock(Integer productId) {
        19. //获取锁对象
        20. RLock lock = redisson.getLock("aaa::" + productId);
        21. try {
        22. //加锁
        23. lock.lock(30,TimeUnit.SECONDS);
        24. //查看该商品的库存数量
        25. Integer stock = productStockDao.findStockByProductId(productId);
        26. if (stock > 0) {
        27. //修改库存每次-1
        28. productStockDao.updateStockByProductId(productId);
        29. System.out.println("扣减成功!剩余库存数:" + (stock - 1));
        30. return "success";
        31. } else {
        32. System.out.println("扣减失败!库存不足!");
        33. return "fail";
        34. }
        35. }finally{
        36. //释放锁
        37. lock.unlock();
        38. }
        39. }
        40. }

        4. redis中常见的面试题

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

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

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

        建议三种都使用:

        Controller层 判断数据是否有意义

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

        1. @Override
        2. public CommonResult findById(Integer id){
        3. //创建布隆过滤器对象
        4. BloomFilter bloomFilter = BloomFilter.create(Funnels.integerFunnel(), 50,0.05);
        5. //存放1-50的id号
        6. for (int i = 1; i <= 50; i++) {
        7. bloomFilter.put(i);
        8. }
        9. ValueOperations forValue = redisTemplate.opsForValue();
        10. //查询缓存
        11. Object o = forValue.get("user::" + id);
        12. //缓存命中
        13. if(o!=null){
        14. // 判断对象是否属于该类型
        15. if(!(o instanceof NullObject)) {
        16. return (CommonResult) o;
        17. }
        18. return null;
        19. }
        20. //未命中
        21. if(bloomFilter.mightContain(id)){ //查看布隆过滤器是否存在
        22. User user = userMapper.selectById(id);
        23. if(user!=null){
        24. //存入缓存
        25. forValue.set("user::"+id,new CommonResult(2000,"查询成功",user),30, TimeUnit.MINUTES);
        26. return new CommonResult(2000,"查询成功",user);
        27. }else{
        28. //在redis中存入一个空对象,小于5分钟 避免缓存穿透
        29. forValue.set("user::"+id,new NullObject(),5, TimeUnit.MINUTES);
        30. return new CommonResult(5000,"查询失败",null);
        31. }
        32. }
        33. return null;
        34. }

        //伪代码
        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)业界比较常用的做法。

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

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

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

        4.4. Redis 淘汰策略有哪些?

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

        在redis.conf中配置

      41. 相关阅读:
        grafana9配置邮箱告警
        【人工智能-神经网络】Numpy 实现单层感知机对情感文本多分类
        Hive【Hive(三)查询语句】
        【DevOps】Docker 容器及其常用命令
        操作系统的概念、四个特征以及os的发展和分类
        JS中的栈和堆
        GhostNet实战:使用GhostNet实现图像分类任务(一)
        Linux学习笔记之设备驱动篇(5)_字符设备_理论篇2
        WordPress网站更改后台登录地址保姆级图文教程
        SpringBoot入门教程:浅聊POJO简单对象(VO、DTO、Entity)
      42. 原文地址:https://blog.csdn.net/weixin_68509156/article/details/126132854