大型商城:缓存与分布式锁

整合Redis

1.product中引入Redis依赖

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

2.yml中配置Redis的地址

spring:
  redis:
    host: 192.168.56.10
    port: 6379

3.test

    @Autowired
    StringRedisTemplate stringRedisTemplate;
    @Test
    public void testRedis(){
        ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
        //保存
        ops.set("hello","word"+ UUID.randomUUID().toString());
        String hello = ops.get("hello");
        System.out.println("数据:"+hello);
    }

 缓存使用-改造三级分类业务

将原来的getCatalogJson方法改为getCatalogJsonFromDb,再新写一个getCatalogJson方法!

思路就是先去Redis中查

    @Override
    public Map<String, List<Catelog2Vo>> getCatalogJson(){
        //给缓存中放入json字符串,拿出的json字符串,还用逆转为能用的对象类型: 【序列化与反序列化】

        //1.加入缓存逻辑,缓存中存的数据是json字符串
        //json夸语言 夸平台兼容
        String catalogJSON = (String) redisTemplate.opsForValue().get("catalogJSON");
        if(StringUtils.isEmpty(catalogJSON)){
            //缓存中没有 查询数据库
            Map<String, List<Catelog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb();
            //查询到数据再放入缓存,将对象转为json放在缓存中
            String s = JSON.toJSONString(catalogJsonFromDb);
            redisTemplate.opsForValue().set("catalogJSON",s);
            return catalogJsonFromDb;
        }

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

        return result;

    }

lettuce堆外内存溢出bug

当进行压力测试时后期后出现堆外内存溢出OutOfDirectMemoryError

产生原因:

1)、springboot2.0以后默认使用lettuce作为操作redis的客户端,它使用netty进行网络通信

2)、lettuce的bug导致netty堆外内存溢出。netty如果没有指定堆外内存,默认使用Xms的值,可以使用-Dio.netty.maxDirectMemory进行设置

解决方案:由于是lettuce的bug造成,不要直接使用-Dio.netty.maxDirectMemory去调大虚拟机堆外内存,治标不治本。

1)、升级lettuce客户端。但是没有解决的
2)、切换使用jedis

        <!--Redis-->
        <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>

lettuce和jedis是操作redis的底层客户端,RedisTemplate是再次封装

 

再次进行测试发现没有异常!

缓存失效

关于缓存穿透,缓存雪崩,缓存击穿:https://www.tinstu.com/1714.html

缓存穿透:查询一个不存在的数据,缓存没有,数据库也没有,也不会缓存为null,就会一直查询数据库!

  • 对空结果进行缓存

缓存雪崩:key的过期时间相同,导致缓存在某一时刻同时失效,请求全部转发数据库

  • 设置过期时间(加随机值),解决缓存雪崩

缓存击穿:某个热点key过期,导致高并发都去查数据库

  • 加锁解决缓存穿透

加锁解决缓存击穿的问题

synchronized 关键字,代表这个方法加锁,相当于不管哪一个线程(例如线程A),运行到这个方法时,都要检查有没有其它线程B(或者C、 D等)正在用这个方法(或者该类的其他同步方法),有的话要等正在使用synchronized方法的线程B(或者C 、D)运行完这个方法后再运行此线程A,没有的话,锁定调用者,然后直接运行。

不好的方法是synchronized(this),肯定不能这么写 ,不具体写了  本地锁 不适用于分布式

锁时序问题:

之前逻辑是调用这个方法,加锁,查缓存,空就查数据库,释放锁,进行缓存(没有缓存完成之前,可能有一个线程进入,查不到缓存,又去查数据库)

解决:缓存完成再释放锁

 查缓存 
 null->锁
synchronized(this){
    查缓存
    null->数据库
    进行缓存
}

此为本地锁,只能锁定当前进程,分布下无法使用

 

分布式锁的原理与使用

redis常用命令:https://www.tinstu.com/1506.html

redis分布式锁:https://www.tinstu.com/1719.html

阶段一

使用redis的set  k v nx      表示当数据库中key不存在时,可以将key-value添加数据库

阶段二

上面遇到的问题,加锁成功后,程序异常了,锁没有删除,那么程序再启动的时候永久都是加锁失败了,形成死锁!

那么需要给锁设置一个过期时间,加锁和过期时间必须同步,保持原子性,否则还有可能形成死锁的情况!

boollean lock = redisTemplate.opsForValue( ).setIfAbsent("lock","111",300,TimeUnit.SECONDS);

 阶段三

如果由于业务很长,锁自己过期了,我们直接删除,有可能把别人正在持有的锁删除了

解决:占有锁的时候,值指定为UUID,每个人匹配的是自己的锁才删除!

阶段四

如正好正好判断当前值,正要删除的时候,锁已经过期了,别人已经设置到了新的值,那么我们删除是别人的锁

解决:删除锁必须保证原子性,使用redis+Lua脚本完成!

阶段五 - 最终形态

解锁lua脚本

if redis.call("get",KEYS[1]) == ARGV[1]
then
      return redis.call("del",KEYS[1])
esle
      return 0
end

保证加锁【占位+过期时间】和删除锁【判断+删除】的原子性,更难的事情,锁的自动续期!

更专业的分布式锁框架 Redission :https://www.tinstu.com/2642.html

 

 

 

 

阅读剩余
THE END