redis学习

哪些数据适合放入缓存:

  • 即时性、数据一致性要求不高
  • 访问量大且更新频率不高(读多,写少)


读模式缓存使用流程

docker环境下使用redis

# 拉取redis
docker pull redis:6.0.9
# 启动redis并设置密码为123456
docker run --name redis -p 6379:6379 -d redis:6.0.9 --requirepass "123456"
# 设置开机自启
docker update redis --restart=always

使用redis作为缓存

spring boot引入redis

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

配置redis连接信息

spring:
 redis:
  host: 192.168.56.10
  port: 6379
  password: test

进行压测产生堆外内存溢出:OutOfDirectMemoryError

产生原因:
springboot2.0以后默认使用lettuce作为操作redis的客户端,它使用netty进行网络通信。lettuce的bug导致netty的堆外内存溢出(netty如果没有指定堆外内存,默认使用-Xmx100m)

解决方案(不能使用-Dio.netty.maxDirectMemory只去调整堆外内存):

  1. 升级lettuce的客户端
  2. 使用jedis
<!-- 切换客户端为jedis -->
<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-data-redis</artifactId>
 <exclusions>
  <exclusion>
   <groupId>io.lettuce</groupId>
   <artifactId>lettuce-core</artifactId>
  </exclusion>
 </exclusions>
</dependency>

<dependency>
 <groupId>redis.clients</groupId>
 <artifactId>jedis</artifactId>
</dependency>

高并发下缓存失效问题:

缓存穿透:
指查询一个一定不存在的数据,由于缓存不命中,将去查询数据库,但是数据库也无此纪录,我们没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都要到存储层查询,失去了缓存的意义

风险:
利用不存在的数据进行攻击,数据库瞬时压力增大,最终导致崩溃

解决:
null结果缓存,并加入短暂过期时间

缓存雪崩:
缓存雪崩是指在我们设置缓存时key采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。

解决:
原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

缓存击穿:
对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常"热点"的数据。如果这个key在大量请求同时进来前正好失效,那么所有对这个key的数据查询都落到db,我们称为缓存击穿。

解决:
加锁。大量并发只让一个去查,其他人等待,查到以后释放锁,其他人获取到锁,先查缓存,就会有数据,不用去db

加锁:
只要是同一把锁,就能锁住需要这个锁的所有线程。

  1. 本地锁:synchronized(this)、JUC(Lock),springboot所有组件在容器中都是单例的,只能锁住当前进程
  2. 分布式锁:由于在分布式情况下有多个容器,所以想要锁住所有,必须使用分布式锁
// 本地锁
private Map<Long,List<Category2Level>> getCategorysJsonFromDB() {
 synchronized (this){
  // 当其他的线程进来的时候直接查询缓存,避免操作数据库 
  String s = redisTemplate.opsForValue().get(CATEGORY_JSON_KEY);
  // 缓存中没有
  if (StringUtils.isEmpty(s)){
   // System.out.println("查询数据库"); getDatasFromDB();是业务逻辑
   Map<Long, List<Category2Level>> datasFromDB = getDatasFromDB();
   // 在redis缓存中存数据 在锁结束时保存到缓存中
   redisTemplate.opsForValue().set(KEY,VALUE,1, TimeUnit.DAYS);
   return datasFromDB;
  }
  Map<Long, List<Category2Level>> longListMap = JSON.parseObject(s, new TypeReference<Map<Long, List<Category2Level>>>() {});
  return longListMap;
 }
}

分布式锁的演进阶段:

// 伪代码
public void getData(){
    Boolean lock = redisTemplate.opsForValue.setUfAbsent("lock","1111");
    if(lock){
        // 执行业务..
        // 删除锁
        redisTemplate.delete("lock");
    } else{
        // 继续调用getData,等待锁的释放,在此期间可以休眠一段时间
        getData();
    }
}

// 命令:SET key value [EX seconds] [PX milliseconds] [NX'XX]
// EX seconds              设置键key的过期时间,单位时秒
// PX milliseconds         设置键key的过期时间,单位时毫秒
// NX                      只有键key不存在的时候才会设置key的值
// XX                      只有键key存在的时候才会设置key的值
// 伪代码
public void getData(){
    // 设置了过期时间 - 180s
    Boolean lock = redisTemplate.opsForValue.setUfAbsent("lock","1111",180,TimeUnit.SECONDS);
    if(lock){
        // 执行业务...
        // 删除锁
        redisTemplate.delete("lock");
    } else{
        // 继续调用getData,等待锁的释放,在此期间可以休眠一段时间
        getData();
    }
}

// 伪代码
public void getData(){
    // 使用UUID生成不重复值
    String uuid = UUID.randomUUID().toString();
    Boolean lock = redisTemplate.opsForValue.setUfAbsent("lock",uuid,180,TimeUnit.SECONDS);
    if(lock){
        // 执行业务...
        // 判断uuid是否相同
        String str = redisTemplate.get("lock");
        if(uuid.equals(str){
            // 相同,就删除锁
            redisTemplate.delete("lock");
        }
    } else{
        // 继续调用getData,等待锁的释放,在此期间可以休眠一段时间
        getData();
    }
}

public void getData(){
    // 使用UUID生成不重复值
    String uuid = UUID.randomUUID().toString();
    Boolean lock = redisTemplate.opsForValue.setUfAbsent("lock",uuid,180,TimeUnit.SECONDS);
    if(lock){
        try{
            // 执行业务...
        }
        finally{
            // Lua脚本 - 2个参数 key和value
            String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
            long result = redisTemplate.execute(new DefaultRedisScript<long>(script,long.class),Arrays.asList("lock"),uuid);
        }
    } else{
        // 继续调用getData,等待锁的释放,在此期间可以休眠一段时间
        getData();
    }
}

整合redisson作为分布式锁、分布式对象等功能框架

1、引入依赖

<!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
<dependency>
 <groupId>org.redisson</groupId>
 <artifactId>redisson</artifactId>
 <version>3.13.5</version>
</dependency>

2、redisson配置(参考:https://github.com/redisson/redisson/wiki/14.-%E7%AC%AC%E4%B8%89%E6%96%B9%E6%A1%86%E6%9E%B6%E6%95%B4%E5%90%88#142-spring-cache%E6%95%B4%E5%90%88

@Bean(destroyMethod="shutdown")
RedissonClient redisson() throws IOException {
    Config config = new Config();
         // 地址前必须添加"redis://",不然启动会报错
         // 可以用"rediss://"来启用ssl连接
    config.useClusterServers().addNodeAddress("redis://127.0.0.1:7004", "redis://127.0.0.1:7001");
    return Redisson.create(config);
}
// 修改为单节点模式(参考:https://github.com/redisson/redisson/wiki/2.-%E9%85%8D%E7%BD%AE%E6%96%B9%E6%B3%95#26-%E5%8D%95redis%E8%8A%82%E7%82%B9%E6%A8%A1%E5%BC%8F):
// 默认连接地址 127.0.0.1:6379
RedissonClient redisson = Redisson.create();
Config config = new Config();
config.useSingleServer().setAddress("redis://myredisserver:6379");
RedissonClient redisson = Redisson.create(config);

3、分布式锁

可重用锁(Reentrant Lock)https://github.com/redisson/redisson/wiki/8.-%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E5%92%8C%E5%90%8C%E6%AD%A5%E5%99%A8

// 加锁
RLock lock = redissonClient.getLock("hello");
// 阻塞式等待,默认加锁都是30s
// lock.lock();
// 优势:
//  1、锁的自动续期,如果业务时间超长,运行期间自动给锁续上新的30s,不用担心业务时间长,锁过期被自动删掉
//  2、加锁的业务只要运行完成,就不会给当前锁自动续期,即使不手动解锁,锁默认在30s后自动删除
// 加锁以后10秒钟自动解锁,自动解锁时间必须要大于业务时间
// 无需调用unlock方法手动解锁
// 1、如果传递了锁的超时时间,就会发送给redis执行脚本,进行占锁,默认超时时间就是指定的时间
// 2、如果未指定超时时间,就会使用30*1000【lookWatchdongTimeout看门狗的默认时间】
//  只要占锁成功,就会启动一个定时任务【重新设置过期时间,新的过期时间就是看门狗的默认时间】
//  每隔internalLockLeaseTime【看门狗时间】/ 3自动续30s
lock.lock(10, TimeUnit.SECONDS);
//省掉了整个续期时间
try {
	System.out.println("加锁成功" + Thread.currentThread().getId());
	Thread.sleep(3000);
} catch (Exception e) {
	System.out.print(e);
} finally {
	System.out.println("释放锁" + Thread.currentThread().getId());
	lock.unlock();
}

读写锁(ReadWriteLock)(含有写的过程都会被阻塞,只有读读不会被阻塞)

@GetMapping("/read")
@ResponseBody
public String read() {
     RReadWriteLock lock = redissonClient.getReadWriteLock("ReadWrite-Lock");
     RLock rLock = lock.readLock();
     String s = "";
     try {
          rLock.lock();
          System.out.println("读锁加锁"+Thread.currentThread().getId());
          Thread.sleep(5000);
          s= redisTemplate.opsForValue().get("lock-value");
         
    }
    finally {
          rLock.unlock();
          return "读取完成:"+s;
         
    }
}
@GetMapping("/write")
@ResponseBody
public String write() {
     RReadWriteLock lock = redissonClient.getReadWriteLock("ReadWrite-Lock");
     RLock wLock = lock.writeLock();
     String s = UUID.randomUUID().toString();
     try {
          wLock.lock();
          System.out.println("写锁加锁"+Thread.currentThread().getId());
          Thread.sleep(10000);
          redisTemplate.opsForValue().set("lock-value",s);
         
    }
    catch (InterruptedException e) {
          e.printStackTrace();
         
    }
    finally {
          wLock.unlock();
          return "写入完成:"+s;
         
    }
}

信号量(Semaphore)
信号量为存储在redis中的一个数字,当这个数字大于0时,即可以调用acquire()方法增加数量,也可以调用release()方法减少数量,但是当调用release()之后小于0的话方法就会阻塞,直到数字大于0

@GetMapping("/park")
@ResponseBody
public String park() {
    RSemaphore park = redissonClient.getSemaphore("park");
    try {
        park.acquire(2);
    }
    catch (InterruptedException e) {
        e.printStackTrace();
    }
    return "停进2";
}
@GetMapping("/go")
    @ResponseBody
    public String go() {
    RSemaphore park = redissonClient.getSemaphore("park");
    park.release(2);
    return "开走2";
}

闭锁(CountDownLatch)
可以理解为门栓,使用若干个门栓将当前方法阻塞,只有当全部门栓都被放开时,当前方法才能继续执行。
以下代码只有offLatch()被调用5次后 setLatch()才能继续执行

@GetMapping("/setLatch")
@ResponseBody
public String setLatch() {
     RCountDownLatch latch = redissonClient.getCountDownLatch("CountDownLatch");
     try {
          latch.trySetCount(5);
          latch.await();
         
    }
    catch (InterruptedException e) {
          e.printStackTrace();
         
    }
     return "门栓被放开";
}
@GetMapping("/offLatch")
@ResponseBody
public String offLatch() {
     RCountDownLatch latch = redissonClient.getCountDownLatch("CountDownLatch");
     latch.countDown();
     return "门栓被放开1";
}

缓存一致性

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值