• Java大牛必会|分布式缓存实现方案之Spring Cache


    场景

    小白:杨哥,今天我做分布式缓存时,看到公司用了Spring Cache,这个技术你能给我讲讲吗?

    杨哥:没问题!来,听杨哥给你说分布式缓存怎么搞。

    二. 缓存介绍

    2.1 缓存是高并发第一策略

    2.2 适合缓存数据

    2.3 缓存的重要指标

    2.3.1 缓存命中率

    从缓存中读取数据的次数:总读取次数 = 比率,命中率越高越好。

    • 命中率 = 从缓存中读取次数 / (总读取次数[从缓存中读取次数 + 从慢速设备上读取的次数]);

    • Miss率 = 没有从缓存中读取的次数 / (总读取次数[从缓存中读取次数 + 从慢速设备上读取的次数])

    2.3.2 移除策略

    参见上图!

    三. 基于Redis实现缓存

    3.1搭建环境

    3.1.1 添加核心依赖

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

    3.1.2 配置Redis

    1. @Configuration
    2. public class RedisConfig  {
    3.     @Autowired
    4.     private RedisTemplate redisTemplate;
    5.     
    6.     //序列化设置一下
    7.     @PostConstruct
    8.     public void setRedisTemplate() {
    9.         redisTemplate.setKeySerializer(new StringRedisSerializer());
    10.         redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
    11.     }
    12. }

    3.2 缓存实现

    3.2.1 实现流程

    3.2.2 核心代码

    这里我们以查询省份信息为例进行实现。

    1. @Override
    2. public DsAprovinces selectById(Integer id) {
    3.     String key = String.valueOf(id);
    4.     DsAprovinces dsAprovinces= null;
    5.     //1.从redis查
    6.     dsAprovinces = (DsAprovinces)redisTemplate.opsForValue().get(key);
    7.     //2.如果不空
    8.     if(null!= dsAprovinces) {
    9.         System.out.println("redis中查到了");
    10.         return dsAprovinces;
    11.     }
    12.     //3.查询数据库
    13.     dsAprovinces = dsAprovincesMapper.selectById(id);
    14.     //3.放入缓存
    15.     if(null!= dsAprovinces) {
    16.         System.out.println("从数据库中查,放入缓存....");
    17.         redisTemplate.opsForValue().set(key,dsAprovinces);
    18.         redisTemplate.expire(key,60, TimeUnit.SECONDS); //60秒有效期
    19.     }
    20.     return dsAprovinces;
    21. }

    3.3 数据库的增删改联动

    3.3.1 实现流程

    3.3.2 核心代码

    1. //更新:确保机制:实行双删
    2. public int update(DsAprovinces dsAprovinces) {
    3.     
    4.     redisTemplate.delete(String.valueOf(dsAprovinces.getId()));
    5.     int i = dsAprovincesMapper.updateById(dsAprovinces);
    6.     redisTemplate.delete(String.valueOf(dsAprovinces.getId()));
    7.     return  i;
    8. }

    四. 基于Spring Cache实现缓存

    4.1 Spring Cache优点

    根据上图可知,Spring Cache具有如下优点:

    • 基于注解代码清爽简洁

    • 支持各种缓存Spring Cache并非一种具体的缓存技术,而是基于各种缓存产品(如GuavaEhCache、Redis等)进行的一层封装,结合SpringBoot开箱即用的特性用起来会非常方便;

    • 可以实现复杂的逻辑

    • 可以对缓存进行回滚

    4.2 Spring Cache包结构

    4.3 常用注解

    • @Cacheable //查询

    • @CachePut //增改

    • @CacheEvict //删除

    • @Caching //组合多个注解

    • @CacheConfig //在类上添加,抽取公共配置

    4.4 代码实现

    4.4.1 配置类

    1. @Configuration
    2. @EnableCaching //开启spring缓存
    3. public class MyCacheConfig extends CachingConfigurerSupport {
    4.     //使用redis做为缓存
    5.     @Bean
    6.     public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
    7.         //1.redis缓存管理器
    8.         RedisCacheManager.RedisCacheManagerBuilder builder = RedisCacheManager.builder(connectionFactory);
    9.         //2.设置一些参数 //统一设置20s有效期
    10.         builder.cacheDefaults(RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(2000)));
    11.         builder.transactionAware();
    12.         RedisCacheManager build = builder.build();
    13.         return build;
    14.     }
    15.     //可以自定义key的生成策略
    16.     @Override
    17.     public KeyGenerator keyGenerator() {
    18.         return  new KeyGenerator() {
    19.             @Override
    20.             public Object generate(Object target, Method methodObject... objects) {
    21.                 //1.缓冲
    22.                 StringBuilder sb = new StringBuilder();
    23.                 //2.类名
    24.                 sb.append(target.getClass().getSimpleName());
    25.                 //3.方法名
    26.                 sb.append(method.getName());
    27.                 //4.参数值
    28.                 for (Object obj : objects) {
    29.                     sb.append(obj.toString());
    30.                 }
    31.                 return sb.toString();
    32.             }
    33.         };
    34.     }
    35. }

    4.4.2service层实现类

    1. @Service
    2. @Primary
    3. @CacheConfig(cacheNames = "provinces"//key键会添加provinces::
    4. public class AprovincesServiceImpl2 implements IAprovincesService {
    5.     @Autowired
    6.     private DsAprovincesMapper  dsAprovincesMapper;
    7.     @Autowired
    8.     private RedisTemplate redisTemplate;
    9.     @Override
    10.     @Cacheable(key = "#id")
    11.     //@Cacheable
    12.     public DsAprovinces selectById(Integer id) {
    13.         //3.查询数据库
    14.         DsAprovinces dsAprovinces = dsAprovincesMapper.selectById(id);
    15.         return dsAprovinces;
    16.     }
    17.     //更新:确保机制:实行双删
    18.     @CachePut(key = "#dsAprovinces.id")
    19.     public DsAprovinces update(DsAprovinces dsAprovinces) {
    20.         dsAprovincesMapper.updateById(dsAprovinces);
    21.         return dsAprovinces;
    22.     }
    23.     //添加
    24.     @CachePut(key = "#dsAprovinces.id")
    25.     public DsAprovinces save(DsAprovinces dsAprovinces) {
    26.         dsAprovincesMapper.insert(dsAprovinces);
    27.         return  dsAprovinces;
    28.     }
    29.     
    30.     //删除
    31.     @CacheEvict
    32.     public int delete(Integer id) {
    33.         int i = dsAprovincesMapper.deleteById(id);
    34.         return  i;
    35.     }
    36. }

    4.5 缺点

    任何一种技术都不是十全十美的,Spring Cache也不例外,它也有一些缺点,比如:

    • 不能保证数据的一致性保存和修改数据是,是先修改数据库,然后再进行更新缓存。如果不满意延迟效果,可以不用cache ,用双删。

    • 过期进间配置过期时间统一配置。

    五.缓存一致性问题

    我们在实现Redis缓存时,其实会存在缓存一致性的问题,比如下图所示:

    5.1 代码同步更新

    强一致性,但代码耦合高。

    5.2 代码异步更新

    有延迟,借助设计模式或者mq。

    5.3 缓存自己的TTL

    缓存有一定的延迟,可能造成缓存击穿,或者雪崩问题。

    5.4 用定时任务

    要把握好更新频率。

    六.分布式锁的实现

    6.1 实现流程

    解释说明:

    • 缓存击穿和雪崩可以用分布式解决。

    • 线程1,代码当前线程

    • 线程2,代表和线程1的同类线程。

    • 线程3,代表其它线程。

    6.2 实现代码

    6.2.1 核心代码

    1. @Service
    2. public class AprovincesServiceImpl3 implements IAprovincesService {
    3.     @Autowired
    4.     private DsAprovincesMapper  dsAprovincesMapper;
    5.     @Autowired
    6.     private RedisTemplate redisTemplate;
    7.     @Autowired
    8.     public CacheManager cacheManager;
    9.     @Override
    10.     public DsAprovinces selectById(Integer id) {
    11.         System.out.println("=======================开始");
    12.         //1.从缓存中取数据
    13.         Cache provinces1 = cacheManager.getCache("provinces");
    14.         Cache.ValueWrapper provinces = cacheManager.getCache("provinces").get(id);
    15.         //2. 如果缓存中有数据,则返回
    16.         if(null != provinces) {
    17.             System.out.println("====从缓从中拿数据=======");
    18.             return (DsAprovinces)provinces.get();
    19.         }
    20.         //3.如果缓存中没有,先加把锁
    21.         doLock(id+"");
    22.         //
    23.         DsAprovinces dsAprovinces = null;
    24.         try {
    25.             //4.0从第二个线程进来后,查一下
    26.             provinces = cacheManager.getCache("provinces").get(id);
    27.             if(null != provinces) {
    28.                 System.out.println("====从缓从中拿数据=======");
    29.                 return (DsAprovinces)provinces.get();
    30.             }
    31.             //4.查询数据库
    32.             dsAprovinces = dsAprovincesMapper.selectById(id);
    33.             //5.把数据放入缓存
    34.             if(null != dsAprovinces) {
    35.                 System.out.println("查询数据库,并放入缓存....");
    36.                 cacheManager.getCache("provinces").put(id,dsAprovinces);
    37.             }
    38.         } catch (Exception e) {
    39.             e.printStackTrace();
    40.         } finally {
    41.             releaseLock(id+"");
    42.         }
    43.         return dsAprovinces;
    44.     }
    45.     //=====================================分布式加锁和释放锁封装start=========================
    46.     //分布式存放锁,这个map是线程安全的
    47.     private ConcurrentHashMap<String,Lock> locks = new ConcurrentHashMap<>();
    48.     //依据主键进行加锁
    49.     private  void doLock(String id) {
    50.         //1.创建一个可重入锁
    51.         ReentrantLock newlock = new ReentrantLock();
    52.         //2.如 果存在id,则返回旧值,如果不存在, 放入,返回null
    53.         Lock old = locks.putIfAbsent(id,newlock);
    54.         //3.如果已存在id
    55.         if(null != old) {
    56.             old.lock();
    57.         }else {
    58.             newlock.lock();
    59.         }
    60.     }
    61.     //释放 一下
    62.     private void releaseLock(String id) {
    63.         //获取到锁
    64.         ReentrantLock lock = (ReentrantLock)locks.get(id);
    65.         //判断
    66.         if(null != lock && lock.isHeldByCurrentThread() ) {
    67.             lock.unlock();
    68.         }
    69.     }
    70.     //=====================================分布式加锁和释放锁封装end=========================
    71.     

    6.2.2 测试CountDownLatch

    1. CountDownLatch cw = new CountDownLatch(8);
    2. @GetMapping("/selectById1/{id}")
    3. public DsAprovinces selectById1(@PathVariable Integer id) {
    4.     for (int i = 0; i < 8; i++) {
    5.         new Thread(
    6.                 new Runnable() {
    7.                     @Override
    8.                     public void run() {
    9.                         try {
    10.                             cw.await();
    11.                             aprovincesService.selectById(id);
    12.                         } catch (InterruptedException e) {
    13.                             e.printStackTrace();
    14.                         } finally {
    15.                         }
    16.                     }
    17.                 }
    18.         ).start();
    19.         cw.countDown();
    20.     }
    21.     return  aprovincesService.selectById(id);
    22. }

    6.2.3测试结果

    . 小

    至此,杨哥就带大家利用Spring Cache实现了分布式缓存,我们在开发时要依据具体的业务逻辑具体地分析解决。其实在项目中并没有固定的格式,只要大家选择适合自己项目场景的方案即可。不知道你现在还有哪些问题呢?可以在评论区给我留言哦。

    *威哥Java学习交流Q群:691533824
    加群备注:CSDN推荐
      

  • 相关阅读:
    Vue跨域详解
    C++ | 高维数组、指针、返回指向数组的指针的函数
    vc可用hex字符串转char*
    MATLAB | 如何绘制三维曲线、曲面、多边形投影(三视图)?
    LeetCode 0141. 环形链表 - 三种方法解决
    近世代数之群
    Spire.Office for Java 8.10.2 同步更新Crk
    「Spring Boot 系列」09. Spring Boot集成MyBatis-Plus实现CRUD
    大一学生作品《前端框架开发技术》 期末网页制作 HTML+CSS+JavaScript 个人主页网页设计实例
    Java.lang.Class类 getMethod()方法有什么功能呢?
  • 原文地址:https://blog.csdn.net/finally_vince/article/details/127123333