高级-05缓存与分布式锁

一、缓存

1、缓存使用

为了系统性能的提升,我们一般都会将部分数据放入缓存中,加速访问。而db承担数据落
盘工作。
哪些数据适合放入缓存?

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

举例:电商类应用,商品分类,商品列表等适合缓存并加一个失效时间(根据数据更新频率
来定),后台如果发布一个商品,买家需要5分钟才能看到新的商品一般还是可以接受的。

data = cache.load(id); // 从缓存加载数据
if(data == null){
	data = db.load(id); // 从数据库中加载数据
	cache.put(id,data); // 保存到cache中
}
return data;

注意:在开发中,凡是放入缓存中的数据都应该指定过期时间,使其可以在系统即使没
有主动更新数据也能自动触发数据加载进缓存的流程。避免业务崩溃导致的数据永久不一致问题。

2、本地缓存

// 全局变量
private Map<String,Object> cache = new HashMap<>();

本地缓存在分布式下的问题

3、分布式缓存

4、整合redis

(1)引入redis

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
// 版本号由父项目控制

(2)简单配置redis的host等信息

spring:
  redis:
    host: 192.168.56.10
    port: 6379

(3)使用redis自动配置好的StringRedisTemplate来操作redis

(4)redis使用测试

@Autowired
StringRedisTemplate stringRedisTemplate;

public void test() {
    ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
    // 保存
    ops.set("hello", "world_" + UUID.randomUUID().toString());
    // 查询
    String hello = ops.get("hello");
    System.out.println(hello);
}

缓存中一般存json字符串(json跨语言、跨平台兼容)

// fastjson序列化
JSON.toJSONString(catelogVo);
// fastjson反序列化
JSONObject.parseObject("",new TypeReference<Map<String,List<Catelog>>>(){});

(5)redis可视化界面工具

5、对外内存溢出OutOfDirectMemoryError

SpringBoot2.0以后默认使用lettuce作为操作redis的客户端。它使用netty进行网络通信。

压力测试时,lettuce的bug会导致netty堆外内存溢出。

可以使用 -Dio.netty.maxDirectMemory 设置netty对外内存

如果没有单独指定内存大小 默认使用 -Xmx ***m 参数

》》》》》》》 解决方案《《《《《《《

不能使用 -Dio.netty.maxDirectMemory 只去调大堆外内存。(只会延长异常出现时间)

  • 升级lettuce客户端
  • 切换使用jedis
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <!--排除lettuce-->
    <exclusions>
        <exclusion>
            <groupId>io.lettuce</groupId>
            <artifactId>lettuce-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<!--引入jedis-->
<!--点进spring-boot-starter-parent、spring-boot-dependencies,确认jedis版本是否存在-->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>

二、缓存失效问题

1、缓存穿透

2、缓存雪崩

3、缓存击穿

4、解决问题

4.1 空结果缓存,解决缓存击穿

// 获取业务数据data
Object data = getData();
if(StringUtils.isEmpty(data)){
    // 查询不到数据就放一个特定的值
    stringRedisTemplate.opsForValue().set("id", "特定的值");
}
// 有值就放进来
stringRedisTemplate.opsForValue().set("id", JSONObject.toJSONString(data));

4.2 设置多起时间(加随机值),解决缓存雪崩

stringRedisTemplate.opsForValue()
    .set("id", JSONObject.toJSONString(data), 
         60 + new Random().nextInt(600), TimeUnit.MINUTES);

4.3 加锁,解决缓存击穿

单机应用下可以对当前对象加锁

但:分布式应用高并发下有X个应用就有可能同时进来X个线程查询同一条数据

本地锁: synchronized,JUC(Lock),只能锁住本地线程。在分布式情况下,想要锁住所有,必须使用分布式锁。

5 锁-时序问题

由于查询完数据库释放锁到结果写入缓存之间存在时间间隔,会导致有多个线程查询数据库

所以需要在缓存写入操作完成后再释放锁。

6 附录

6.1 IDEA复制应用

6.2 IDEA设置运行端口号

三、分布式锁介绍

只有一个能成功,这就是分布式锁的原理

1、基本原理

2、redis分布式锁演进

最重要的两个问题

1、原子加锁》》》set NX EX

2、原子解锁》》》lua脚本

public void getRedisLock() {
        // 占分布式锁,去redis占坑
        // 设置过期时间(必须和加锁是同步的、原子的),防止意外事故导致无法及时删除锁
        // 增加UUID防止删除别人的锁
        String uuid = UUID.randomUUID().toString();
        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("Lock", uuid, 300, TimeUnit.SECONDS);
        if (lock) {
            // 加锁成功...执行业务
            try {
                doData();
            } finally {
                // 业务处理完删除锁
        //    String lockValue = stringRedisTemplate.opsForValue().get("Lock");
        //    if(uuid.equals(lockValue)){
        //        // 删除自己的锁
        //        stringRedisTemplate.delete("Lock");
        //    }
                // 获取值对比+对比成功(中途可能耗时导致锁过期) 也必须是原子操作
                String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
                        "then\n" +
                        "    return redis.call(\"del\",KEYS[1])\n" +
                        "else\n" +
                        "    return 0\n" +
                        "end";
                // 原子删锁
                Long lock1 = stringRedisTemplate.execute(
                        new DefaultRedisScript<Long>(script, Long.class),
                        Arrays.asList("Lock"), uuid);
            }
        } else {
            // 加锁失败...
            // 休眠100ms重试
            try{
                Thread.sleep(200);
            }catch (Exception e){
                
            }
            getRedisLock(); // 自旋的方式
        }
    }

这只是一个简单实现的redis分布式锁

实际上redis官方并不推荐这种方法,市面上有许多成熟的完整的分布式锁应用

而redis官方推荐的是redisson

四、缓存数据一致性

1、双写模式

2、失效模式

3、解决方案

4、Canal

缺点:需要多开发一个中间件

优点:一次开发,后续就不用管了

posted @ 2020-11-10 16:58  这杯Java有毒  阅读(140)  评论(0)    收藏  举报