缓存-分布式锁-Redisson

一、Redisson简介和整合

1、简介
Redisson 是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。其中包括(BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service) Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。
2、整合
1、引入依赖

  <!-- 以后使用redisson 作为所有分布式锁,分布式对象等框架 -->
    <dependency>
      <groupId>org.redisson</groupId>
      <artifactId>redisson</artifactId>
      <version>3.12.0</version>
    </dependency>
    

文档
为了学习Redisson 原理,我们先手动创建Redisson配置文件,以后项目中可以引用已经封装好的 spring-boot-redisson-starter 来快速安装。
2、增加redisson配置文件

package com.kun.product.config;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.redisson.config.TransportMode;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.io.IOException;

/**
 * @author zhoukun 86547462@qq.com
 * @version 1.0
 * @date 2020/12/4 18:40
 */
@Configuration
public class MyRedissonConfig {
    /**
     * @return
     * @throws IOException
     */
    @Bean(destroyMethod="shutdown")
    public RedissonClient redisson() throws IOException {
        //1、创建配置(单节点模式)
        Config config = new Config();
        config.useSingleServer()
                .setAddress("redis://localhost:6379")
                .setPassword("zk2000208.");
        //2、根据Config创建出RedissonClient实例
        //Redis url should start with redis:// or rediss://
        RedissonClient redissonClient = Redisson.create(config);
        return redissonClient;
    }

}

分布式锁和同步器

Redisson-lock锁测试

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.kun.product.entity.CategoryEntity;
import com.kun.product.service.CategoryService;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;


import java.util.List;
import java.util.concurrent.TimeUnit;


/**
 * @author zhoukun 86547462@qq.com
 * @version 1.0
 * @date 2020/11/27 15:19
 */
@Controller
public class IndexController {

    @Autowired
    CategoryService categoryService;

    @Autowired
    RedissonClient redissonClient;

    @GetMapping({"/","/index","/index.html"})
    public String index(Model model)
    {
        //查询所有1级分类进行展示
        List<CategoryEntity> list=categoryService.list(new QueryWrapper<CategoryEntity>().eq("parent_cid",0));
        model.addAttribute("categorys",list);
        //视图解析器进行拼串 classpath:/templates/index.html
        return "index";
    }
    @GetMapping({"/hello"})
    public @ResponseBody String hello()
    {
        RLock lock = redissonClient.getLock("my-lock");
        //枷锁
        // lock.lock(10,TimeUnit.SECONDS);   //10秒钟自动解锁,自动解锁时间一定要大于业务执行时间
        //执行业务逻辑
        //1)、锁的自动续期,如果业务超长,运行期间自动锁上新的30s。不用担心业务时间长,锁自动过期被删掉
        //2)、加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认会在30s内自动过期,不会产生死锁问题
        //问题:在锁时间到了以后,不会自动续期
        //1、如果我们传递了锁的超时时间,就发送给redis执行脚本,进行占锁,默认超时就是 我们制定的时间
        //2、如果我们指定锁的超时时间,就使用 lockWatchdogTimeout = 30 * 1000 【看门狗默认时间】
        //只要占锁成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】,每隔10秒都会自动的再次续期,续成30秒
        // internalLockLeaseTime 【看门狗时间】 / 3, 10s

        //最佳实战
        //lock.lock(30,TimeUnit.SECONDS);//省掉整个续期操作,手动解锁
        try {
            System.out.println("加锁成功,执行业务..." + Thread.currentThread().getId());
            try { TimeUnit.SECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
        } catch (Exception ex) {
            ex.printStackTrace();
        } finally {
            //3、解锁  假设解锁代码没有运行,Redisson会不会出现死锁
            System.out.println("释放锁..." + Thread.currentThread().getId());
            lock.unlock();
        }

        return "hello";

    }
}

读写锁

    /**
     * 保证一定能读到最新数据,修改期间,写锁是一个排它锁(互斥锁、独享锁),读锁是一个共享锁
     * 写锁没释放读锁必须等待
     * 读 + 读 :相当于无锁,并发读,只会在Redis中记录好,所有当前的读锁。他们都会同时加锁成功
     * 写 + 读 :必须等待写锁释放
     * 写 + 写 :阻塞方式
     * 读 + 写 :有读锁。写也需要等待
     * 只要有读或者写的存都必须等待
     * @return
     */
    @GetMapping("/write")
    public @ResponseBody String writeValue()
    {
        String uuid= UUID.randomUUID().toString();
        RReadWriteLock lock = redissonClient.getReadWriteLock("rw-lock");
        RLock writeLock = lock.writeLock();//获取写锁
        try {
            writeLock.lock();//修改数据加写锁
            Thread.sleep(30000);
            stringRedisTemplate.opsForValue().set("writeValue",uuid);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            writeLock.unlock();//解锁
        }
        return "写入成功!";
    }
    @GetMapping("/read")
    public @ResponseBody String readValue()
    {
        String uuid= UUID.randomUUID().toString();
        RReadWriteLock lock = redissonClient.getReadWriteLock("rw-lock");
        RLock readLock = lock.readLock();//读取数据,加读锁
        try {
            readLock.lock();//加读锁
            return stringRedisTemplate.opsForValue().get("writeValue");
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            readLock.unlock();//解锁
        }
        return "读取失败";
    }

信号量(Semaphore)

/**
     * 车库停车
     * 3车位
     * 信号量也可以做分布式限流
     */
    @GetMapping(value = "/park")
    @ResponseBody
    public String park() throws InterruptedException {

        RSemaphore park = redissonClient.getSemaphore("park");
//        park.acquire();     //获取一个信号、获取一个值,占一个车位
        boolean flag = park.tryAcquire();

        if (flag) {
            //执行业务
        } else {
            return "error";
        }

        return "ok=>" + flag;
    }

    @GetMapping(value = "/go")
    @ResponseBody
    public String go() {
        RSemaphore park = redissonClient.getSemaphore("park");
        park.release();     //释放一个车位
        return "ok";
    }

闭锁(CountDownLatch)

/**
	闭锁,在完成某些运算是,只有其他所有线程的运算全部完成,当前运算才继续执行。
     * 放假、锁门
     * 1班没人了
     * 5个班,全部走完,我们才可以锁大门
     * 分布式闭锁
     */

    @GetMapping(value = "/lockDoor")
    @ResponseBody
    public String lockDoor() throws InterruptedException {

        RCountDownLatch door = redissonClient.getCountDownLatch("door");
        door.trySetCount(5);
        door.await();       //等待闭锁完成

        return "放假了...";
    }

    @GetMapping(value = "/gogogo/{id}")
    @ResponseBody
    public String gogogo(@PathVariable("id") Long id) {
        RCountDownLatch door = redissonClient.getCountDownLatch("door");
        door.countDown();       //计数-1

        return id + "班的人都走了...";
    }

项目实战

  @Autowired
  private RedissonClient redissonClient;

/**
   * 缓存里的数据如何和数据库的数据保持一致??
   * 缓存数据一致性
   * 1)、双写模式
   * 2)、失效模式
   * @return
   */

  public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedissonLock() {

    //1、占分布式锁。去redis占坑
    //(锁的粒度,越细越快:具体缓存的是某个数据,11号商品) product-11-lock
    //RLock catalogJsonLock = redissonClient.getLock("catalogJson-lock");
    //创建读锁
    RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("catalogJson-lock");

    RLock rLock = readWriteLock.readLock();

    Map<String, List<Catelog2Vo>> dataFromDb = null;
    try {
      rLock.lock();
      //加锁成功...执行业务
      dataFromDb = getDataFromDb();
    } finally {
      rLock.unlock();
    }
    //先去redis查询下保证当前的锁是自己的
    //获取值对比,对比成功删除=原子性 lua脚本解锁
    // String lockValue = stringRedisTemplate.opsForValue().get("lock");
    // if (uuid.equals(lockValue)) {
    //     //删除我自己的锁
    //     stringRedisTemplate.delete("lock");
    // }

    return dataFromDb;

  }

数据一致性问题

双写模式

做法顺序:先写数据库,再写缓存
在这里插入图片描述
由于卡顿等原因,导致写缓存2在最前,写缓存1在后面就出现了不一致

脏数据问题:
这是暂时性的脏数据问题,但是在数据稳定,缓存过期以后,又能得到最新的正确数据读到的最新数据有延迟:最终一致性

失效模式

做法顺序:先写数据库,在删除缓存
在这里插入图片描述
在这里插入图片描述
由于网络或者i/o问题导致第三个请求拿到了数据库中数据:db-1,此时第二个请求数据库写更新db-1->db-2已完成,立刻删除缓存,第三个请求又将缓存刷新成第一个请求时的数据

还是会出现脏数据问题:最终不一致性

解决方案:

无论是双写模式还是失效模式,都会导致缓存的不一致问题。即多个实例同时更新会出事。怎么办?

1.如果是用户纬度数据(订单数据、用户数据),这种并发几率非常小,不用考虑这个问题,缓存数据加 上过期时间,每隔一段时间触发读的主动更新即可
2.如果是菜单,商品介绍等基础数据,也可以去使用canal订阅binlog的方式(比较优秀)。
3.缓存数据+过期时间也足够解决大部分业务对于缓存的要求。
4.通过加锁保证并发读写,写写的时候按顺序排好队。读读无所谓。所以适合使用读写锁。(业务不关心 脏数据,允许临时脏数据可忽略);

总结:

我们能放入缓存的数据本就不应该是实时性、一致性要求超高的。所以缓存数据的时候加上过期时间,保证每天拿到当前最新数据即可。
我们不应该过度设计,增加系统的复杂性 。
遇到实时性、一致性要求高的数据,就应该查数据库,即使慢点。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值