缓存与分布式锁

缓存

在这里插入图片描述
在这里插入图片描述

1、分布式本地缓存

分布式缓存-本地模式在分布式下的问题
在这里插入图片描述
这种情况下,每个服务维持一个缓存,所带来的问题:

(1)缓存不共享
在这种情况下,每个服务都有一个缓存,但是这个缓存并不共享,水平上当调度到另外一个台设备上的时候,可能它的服务中并不存在这个缓存,因此需要重新查询。

(2)缓存一致性问题
在一台设备上的缓存更新后,其他设备上的缓存可能还未更新,这样当从其他设备上获取数据的时候,得到的可能就是未给更新的数据。

2、分布式缓存

在这里插入图片描述

在这种下,一个服务的不同副本共享同一个缓存空间,缓存放置到缓存中间件中,这个缓存中间件可以是redis等,而且缓存中间件也是可以水平或纵向扩展的,如Redis可以使用redis集群。它打破了缓存容量的限制,能够做到高可用,高性能。

3、整合redis测试

       <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
  • 源码提供的两种操作方法
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {
 
   @Bean
   @ConditionalOnMissingBean(name = "redisTemplate")
   //将保存进入Redis的键值都是Object
   public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
         throws UnknownHostException {
      RedisTemplate<Object, Object> template = new RedisTemplate<>();
      template.setConnectionFactory(redisConnectionFactory);
      return template;
   }
 
   @Bean
   @ConditionalOnMissingBean
   //保存进Redis的数据,键值是(String,String)
   public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory)
         throws UnknownHostException {
      StringRedisTemplate template = new StringRedisTemplate();
      template.setConnectionFactory(redisConnectionFactory);
      return template;
   }
 
}
  • StringRedisTemplate
public class StringRedisTemplate extends RedisTemplate<String, String> {
 
   /**
    * Constructs a new <code>StringRedisTemplate</code> instance. {@link #setConnectionFactory(RedisConnectionFactory)}
    * and {@link #afterPropertiesSet()} still need to be called.
    */
   public StringRedisTemplate() {
      setKeySerializer(RedisSerializer.string());//键序列化为String
      setValueSerializer(RedisSerializer.string());//key序列化为String
      setHashKeySerializer(RedisSerializer.string());
      setHashValueSerializer(RedisSerializer.string());
   }
redis:
    host: 服务器地址
    port: 6379
  • 使用SpringBoot自动配置好的"StringRedisTemplate"来操作redis
@Autowired
    StringRedisTemplate stringRedisTemplate;

    @Test
    public void testStringRedisTemplate(){
        ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();

        ops.set("hello","word_"+UUID.randomUUID().toString());

        String hello = ops.get("hello");
        System.out.println(hello);
    }

    @Test
    public void testFindPath(){
        Long[] catelogPath = categoryService.findCatelogPath(225L);
        log.info("完整路径:{}", Arrays.asList(catelogPath));
    }

4、三级分类业务改造(加入缓存)

@Override
    public Map<String, List<Catelog2Vo>> getCatelogJson() {

        /**
         * 1、空结果缓存:解决缓存雪崩
         * 2、设置过期时间(加随机值),解决缓存雪崩
         * 3、加锁,解决缓存击穿
         */

        //1、加入缓存逻辑
        String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
        if (StringUtils.isEmpty(catalogJSON)) {
            //2、缓存中没有数据,查数据库
            Map<String, List<Catelog2Vo>> catelogJsonFromDb = getCatelogJsonFromDbWithLocalLock();

            return catelogJsonFromDb;
        }
        //转为我们指定的对象。
        Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
        });
        return result;
    }

    public Map<String, List<Catelog2Vo>> getDataFromDb(){
        //得到锁之后,应该再去缓存中确定一次,如果没有才需要继续查询
        String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
        if (!StringUtils.isEmpty(catalogJSON)) {
            //缓存不为null直接返回
            Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
            });
            return result;
        }
        List<CategoryEntity> entityList = baseMapper.selectList(null);
        // 查询所有一级分类
        List<CategoryEntity> level1 = getCategoryEntities(entityList, 0L);
        Map<String, List<Catelog2Vo>> parent_cid = level1.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
            // 拿到每一个一级分类 然后查询他们的二级分类
            List<CategoryEntity> entities = getCategoryEntities(entityList, v.getCatId());
            List<Catelog2Vo> catelog2Vos = null;
            if (entities != null) {
                catelog2Vos = entities.stream().map(l2 -> {
                    Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), l2.getName(), l2.getCatId().toString(), null);
                    // 找当前二级分类的三级分类
                    List<CategoryEntity> level3 = getCategoryEntities(entityList, l2.getCatId());
                    // 三级分类有数据的情况下
                    if (level3 != null) {
                        List<Catalog3Vo> catalog3Vos = level3.stream().map(l3 -> new Catalog3Vo(l3.getCatId().toString(), l3.getName(), l2.getCatId().toString())).collect(Collectors.toList());
                        catelog2Vo.setCatalog3List(catalog3Vos);
                    }
                    return catelog2Vo;
                }).collect(Collectors.toList());
            }
            return catelog2Vos;
        }));
        //3、查询到的数据放入缓存,转json存
        String s = JSON.toJSONString(parent_cid);
        redisTemplate.opsForValue().set("catalogJSON", s, 1, TimeUnit.DAYS);
        return parent_cid;
    }

    /**
     * 从数据库查询封装并分类数据,本地锁
     */
    public Map<String, List<Catelog2Vo>> getCatelogJsonFromDbWithLocalLock() {

        /**
         * 1、只要是一把锁,就能锁住需要这个锁的所有线程
         * synchronize(this)springboot所有的组件在容器中都是单例的
         */
        //TODO 本地锁:synchronize在分布式的情况下,想要锁住所有,必须使用分布式锁

        synchronized (this) {
            return getDataFromDb();
        }

    }

    /**
     * 分布式锁
     *
     * @return
     */
    public Map<String, List<Catelog2Vo>> getCatelogJsonFromDbWithRedisLock() {

        //1、占用分布式锁,去redis占坑;setNx
        String uuid = UUID.randomUUID().toString();
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);
        if (lock){
            //占座成功
            Map<String, List<Catelog2Vo>> dataFromDb;

            try{
                dataFromDb = getDataFromDb();
            }finally {
                //删除锁,获取值对比+对比成功删除=原子操作  lua脚本解锁
//            String lockValue = redisTemplate.opsForValue().get("lock");
//            if (uuid.equals(lockValue))
//            redisTemplate.delete("lock");
                String script = "if redis call('get',KEYS[1]) == ARGV[1] then return redis call('del',KEYS[1]) else return 0 end";
                Long lock1 = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class)
                        , Arrays.asList("lock"), uuid);
            }
            return dataFromDb;
        }else {
            //占锁失败,重试,自旋
            try {
                Thread.sleep(200);
            }catch (Exception e){

            }
            return getCatelogJsonFromDbWithRedisLock();
        }
    }


    /**
     * 第一次查询的所有 CategoryEntity 然后根据 parent_cid去这里找
     */
    private List<CategoryEntity> getCategoryEntities(List<CategoryEntity> entityList, Long parent_cid) {

        return entityList.stream().filter(item -> item.getParentCid().equals(parent_cid)).collect(Collectors.toList());
    }

5、本地锁和分布式锁

本地锁的问题

  • 多个服务还是会查询数据库
@Override
    public Map<String, List<Catelog2Vo>> getCatelogJson() {
        //给缓存中放json字符串,拿出json字符串,还要逆转为能用的对象类型【序列化与反序列化】
        /**
         * 1、空结果缓存,解决缓存穿透
         * 2、设置过期时间(随机加值);解决缓存雪崩
         * 3、加锁,解决缓存击穿
         */
        //1、加入缓存逻辑
        //JSON好处是跨语言,跨平台兼容。
        String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
        if (StringUtils.isEmpty(catalogJSON)){
            //2、缓存中没有,查询数据库
            System.out.println("缓存不命中.....将要查询数据库...");
            Map<String, List<Catelog2Vo>> catelogJsonFromDB = getCatelogJsonFromDB();
 
        }
        System.out.println("缓存命中...直接返回...");
        //转为我们指定的对象。
        Map<String,List<Catelog2Vo>> result = JSON.parseObject(catalogJSON,new TypeReference<Map<String,List<Catelog2Vo>>>(){});
        return result;
    }
 
    //从数据库查询并封装分类数据
    public Map<String, List<Catelog2Vo>> getCatelogJsonFromDB() {
        //只要同一把锁就能锁住需要这个锁的所有线程
        //1、synchronized(this):SpringBoot所有的组件在容器中都是单例的
        // TODO 本地锁:synchronized,JUC(lock)。在分布式情况下想要锁住所有,必须使用分布式锁
        //使用DCL(双端检锁机制)来完成对于数据库的访问
        synchronized (this){
            //得到锁以后,我们应该再去缓存中确定一次,如果没有才需要继续查询
            String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
            if (!StringUtils.isEmpty(catalogJSON)){
                //如果缓存不为null直接缓存
                Map<String,List<Catelog2Vo>> result = JSON.parseObject(catalogJSON,new TypeReference<Map<String,List<Catelog2Vo>>>(){});
                return result;
            }
            System.out.println("查询了数据库。。。。。");
            /**
             * 1、将数据库的多次查询变为1次
             */
            List<CategoryEntity> selectList = baseMapper.selectList(null);
            //查出所有一级分类
            List<CategoryEntity> level1Category = getParent_cid(selectList,0L);
 
            //2、封装数据
            Map<String, List<Catelog2Vo>> parent_cid = level1Category.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
                //1、每一个的一级分类,查到这个一级分类的二级分类
                List<CategoryEntity> categoryEntities = getParent_cid(selectList,v.getCatId());
                //2、封装上面的结果
                List<Catelog2Vo> catelog2Vos = null;
                if (categoryEntities != null) {
                    catelog2Vos = categoryEntities.stream().map(l2 -> {
                        Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
                        //1、找当前二级分类的三级分类封装vo
                        List<CategoryEntity> level3Catelog = getParent_cid(selectList,l2.getCatId());
                        if (level3Catelog != null){
                            List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(l3 -> {
                                //2、封装成指定格式
                                Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
                                return catelog3Vo;
                            }).collect(Collectors.toList());
                            catelog2Vo.setCatelog3List(Collections.singletonList(collect));
                        }
                        return catelog2Vo;
                    }).collect(Collectors.toList());
                }
                return catelog2Vos;
            }));
            //3、将查到的数据再放入缓存,将对象转为JSON在缓存中
            String jsonString = JSON.toJSONString(parent_cid);
            redisTemplate.opsForValue().set("catalogJSON",jsonString,1, TimeUnit.DAYS);
            return parent_cid;
        }
 
    }

通过观察日志,能够发现只有一个线程查询了数据库,其他线程都是直接从缓存中获取到数据的。所以在单体应用上实现了多线程的并发访问。

由于这里我们的“gulimall-product”就部署了一台,所以看上去一切祥和,但是在如果部署了多台,问题就出现了,主要问题就集中在我们所使用的锁上。我们锁使用的是“synchronized ”,这是一种本地锁,它只是在一台设备上有效,无法实现分布式情况下,锁住其他设备的相同操作。
在这里插入图片描述

分布式锁

阶段一
  • 删除锁,获取值对比+对比成功删除=原子操作,如果出错就会出现死锁
	public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisLock() {
        //阶段一
        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "111");
        //获取到锁,执行业务
        if (lock) {
            //加锁成功。。。执行业务
            //2、设置过期时间,必须和加锁是同步的,原子的
            Map<String, List<Catelog2Vo>> dataFromDB = getDataFromDB();
            redisTemplate.delete("lock");//删除锁
            return dataFromDB;
        }else {
            //没获取到锁,等待100ms重试
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return getCatalogJsonDBWithRedisLock();
        }
    }

在这里插入图片描述
下面使用redis来实现分布式锁,使用的是SET key value [EX seconds] [PX milliseconds] [NX|XX],
在这里插入图片描述
stringRedisTemplate.opsForValue(“lock”, “111”).setIfAbsent就是setNX操作,set之前会检查是否已经存在key"lock",已存在则返回null

阶段二
  • 问题: 1、setnx占好了位,业务代码异常或者程序在页面过程中宕机。没有执行删除锁逻辑,这就造成了死锁
  • 问题: 1、setnx设置好,正要去设置过期时间,宕机。又死锁了。 解决: 设置过期时间和占位必须是原子的。redis支持使用setnx ex命令
  • 问题: 1、删除锁直接删除??? 如果由于业务时间很长,锁自己过期了,我们直接删除,有可能把别人正在持有的锁删除了。 解决: 占锁的时候,值指定为uuid,每个人匹配是自己的锁才删除。
  • 问题: 1、如果正好判断是当前值,正要删除锁的时候,锁已经过期,别人已经设置到了新的值。那么我们删除的是别人的锁 解决: 删除锁必须保证原子性。使用redis+Lua脚本完成
  • 防止解错锁:加锁添加uuid
  • 防止程序出错导致死锁:添加过期时间
  • 解锁保证原子性:lua脚本解锁:if redis call('get',KEYS[1]) == ARGV[1] then return redis call('del',KEYS[1]) else return 0 end"

在这里插入图片描述
保证加锁【占位+过期时间】和删除锁【判断+删除】的原子性。更难的事情,锁的自动续期

/**
     * 分布式锁
     *
     * @return
     */
    public Map<String, List<Catelog2Vo>> getCatelogJsonFromDbWithRedisLock() {

        //1、占用分布式锁,去redis占坑;setNx
        String uuid = UUID.randomUUID().toString();
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);
        if (lock){
            //占座成功
            Map<String, List<Catelog2Vo>> dataFromDb;

            try{
                dataFromDb = getDataFromDb();
            }finally {
                //删除锁,获取值对比+对比成功删除=原子操作  lua脚本解锁
//            String lockValue = redisTemplate.opsForValue().get("lock");
//            if (uuid.equals(lockValue))
//            redisTemplate.delete("lock");
                String script = "if redis call('get',KEYS[1]) == ARGV[1] then return redis call('del',KEYS[1]) else return 0 end";
                Long lock1 = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class)
                        , Arrays.asList("lock"), uuid);
            }
            return dataFromDb;
        }else {
            //占锁失败,重试,自旋
            try {
                Thread.sleep(200);
            }catch (Exception e){

            }
            return getCatelogJsonFromDbWithRedisLock();
        }
    }

官网说法:
在这里插入图片描述

6、Redisson

Redison使用手册:https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值