微服务进阶学习整合高级篇--缓存与分布式锁

缓存的使用选择条件

  • 即时性、数据一致性要求不高的
  • 读的频率高,修改的频率少的

redis基本使用

redis基本使用

缓存中间件redis入门使用

  • 引入redis坐标
#这里注意springboot版本,springboot稍微旧一点的版本不需要加上-data-字样
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  • yml配置redis相关信息
  • 注入需要使用的redistemplate,也可以根据自己的需求配置对应的redisTemplate。可以在自定义redisTemplate中进行序列化与反序列化,也可以在代码中每次书写的时候手动序列化
@Override
    public Map<String, List<Catelog2Vo>> getCatalogJson2(){
        //先去redis中查询缓存是否存在需要读的数据
        String catalogJson = redisTemplate.opsForValue().get("catalogJson");
        //如果读不到缓存,缓存不存在
        if(StringUtils.isEmpty(catalogJson)){
            //去db读取缓存
            Map<String, List<Catelog2Vo>> catalogJson2FromDB = this.getCatalogJson2FromDB();
            //将查询的数据序列化成json对象,设置缓存方便下次使用
            String cacheCatalog = JSON.toJSONString(catalogJson2FromDB);
            redisTemplate.opsForValue().set("catalogJson",cacheCatalog);
            return catalogJson2FromDB;
        }
        //如果查到缓存,则读取缓存,将缓存的json转换成对象,使用匿名内部类将json转换成指定的类型。
        Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>(){});
        return result;
    }

压力测试下的堆外溢出

  • 使用jmeter对上了缓存的接口进行压力测试,一开始还不会出现错误,随着时间的推移,开始出现堆外内存溢出的情况,如:
io.netty.util.internal.OutOfDirectMemoryError: failed to allocate 46137344 byte(s) of direct memory (used: 58720256, max: 100663296)
	at io.netty.util.internal.PlatformDependent.incrementMemoryCounter(PlatformDependent.java:725) ~[netty-common-4.1.39.Final.jar:4.1.39.Final]
	at io.netty.util.internal.PlatformDependent.allocateDirectNoCleaner(PlatformDependent.java:680) ~[netty-common-4.1.39.Final.jar:4.1.39.Final]
	at io.netty.buffer.PoolArena$DirectArena.allocateDirect(PoolArena.java:772) ~[netty-buffer-4.1.39.Final.jar:4.1.39.Final]
	at io.netty.buffer.PoolArena$DirectArena.newUnpooledChunk(PoolArena.java:762) ~[netty-buffer-4.1.39.Final.jar:4.1.39.Final]
	at io.netty.buffer.PoolArena.allocateHuge(PoolArena.java:260) ~[netty-buffer-4.1.39.Final.jar:4.1.39.Final]
	at io.netty.buffer.PoolArena.allocate(PoolArena.java:232) ~[netty-buffer-4.1.39.Final.jar:4.1.39.Final]
	at io.netty.buffer.PoolArena.reallocate(PoolArena.java:400) ~[netty-buffer-4.1.39.Final.jar:4.1.39.Final]
	at io.netty.buffer.PooledByteBuf.capacity(PooledByteBuf.java:119) ~[netty-buffer-4.1.39.Final.jar:4.1.39.Final]
	at io.netty.buffer.AbstractByteBuf.ensureWritable0(AbstractByteBuf.java:303) ~[netty-buffer-4.1.39.Final.jar:4.1.39.Final]
	at io.netty.buffer.AbstractByteBuf.ensureWritable(AbstractByteBuf.java:274) ~[netty-buffer-4.1.39.Final.jar:4.1.39.Final]
	at io.netty.buffer.AbstractByteBuf.writeBytes(AbstractByteBuf.java:1111) ~[netty-buffer-4.1.39.Final.jar:4.1.39.Final]
	at io.netty.buffer.AbstractByteBuf.writeBytes(AbstractByteBuf.java:1104) ~[netty-buffer-4.1.39.Final.jar:4.1.39.Final]
	at io.netty.buffer.AbstractByteBuf.writeBytes(AbstractByteBuf.java:1095) ~[netty-buffer-4.1.39.Final.jar:4.1.39.Final]
	at io.lettuce.core.protocol.CommandHandler.channelRead(CommandHandler.java:554) ~[lettuce-core-5.1.8.RELEASE.jar:na]
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:374) [netty-transport-4.1.39.Final.jar:4.1.39.Final]
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:360) [netty-transport-4.1.39.Final.jar:4.1.39.Final]
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:352) [netty-transport-4.1.39.Final.jar:4.1.39.Final]
	at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1421) [netty-transport-4.1.39.Final.jar:4.1.39.Final]
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:374) [netty-transport-4.1.39.Final.jar:4.1.39.Final]
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:360) [netty-transport-4.1.39.Final.jar:4.1.39.Final]
	at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:930) [netty-transport-4.1.39.Final.jar:4.1.39.Final]
	at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:163) [netty-transport-4.1.39.Final.jar:4.1.39.Final]
	at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:697) [netty-transport-4.1.39.Final.jar:4.1.39.Final]
	at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:632) [netty-transport-4.1.39.Final.jar:4.1.39.Final]
	at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:549) [netty-transport-4.1.39.Final.jar:4.1.39.Final]
	at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:511) [netty-transport-4.1.39.Final.jar:4.1.39.Final]
	at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:918) [netty-common-4.1.39.Final.jar:4.1.39.Final]
	at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) [netty-common-4.1.39.Final.jar:4.1.39.Final]
	at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) [netty-common-4.1.39.Final.jar:4.1.39.Final]
	at java.lang.Thread.run(Thread.java:748) [na:1.8.0_291]

2021-06-18 10:54:17.263  WARN 9796 --- [ioEventLoop-4-2] io.lettuce.core.protocol.CommandHandler  : null Unexpected exception during request: io.netty.util.internal.OutOfDirectMemoryError: failed to allocate 46137344 byte(s) of direct memory (used: 58720256, max: 100663296)
  • 出现该问题的原因是因为springboot2.0后采用的是lettuce客户端对redis进行操作,lettuce底层使用的是netty,netty如果不指定堆外内存,那他使用的就是我们为项目定义的-Xms 的内存。而netty使用的内存不能够及时释放,从而造成对外内存溢出
  • 解决方案:可以在选择springboot版本的时候,选择高版本的springboot,如2.3.x(本质上也是升级lettuce客户端);也可以改用jedis客户端进行redis操作;

缓存的使用

@Override
    public Map<String, List<Catelog2Vo>> getCatalogJson2(){
        //先去redis中查询缓存是否存在需要读的数据
        String catalogJson = redisTemplate.opsForValue().get("catalogJson");
        //如果读不到缓存,缓存不存在
        if(StringUtils.isEmpty(catalogJson)){
            System.out.println("缓存不命中,查询数据库");
            //去db读取缓存
            Map<String, List<Catelog2Vo>> catalogJson2FromDB = this.getCatalogJson2FromDB();
            return catalogJson2FromDB;
        }
        System.out.println("缓存命中,没有查询数据库");
        //如果查到缓存,则读取缓存,将缓存的json转换成对象,使用匿名内部类。在这里也可以通过自己配置的redisTemplate,在配置文件中统一对数据进行序列化与反序列化处理
        Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>(){});
        return result;
    }


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

        /**
         * 本地锁的方式锁住进程,只允许一个进程进来查看数据,防止高并发下缓存击穿的问题。默认单例模式因此只会存在一个getCatalogJson2FromDB资源
         * 双重校验锁,确保进来的线程不会重复查询数据库
         * 分布式情况下本地锁方式还是会存在问题,假如说日后服务是集群的形式,有8台product服务,每一台服务都会只会锁住当前服务的当前线程,还是会存在多个服务访问统一资源
         * */
        synchronized (this){
            String catalogJson = redisTemplate.opsForValue().get("catalogJson");
            //双重校验锁,如果不为空则去查缓存,为空则继续执行查询数据库的逻辑
            if(!StringUtils.isEmpty(catalogJson)){
                Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>(){});
                System.out.println("缓存命中,没有重复查询数据库");
                return result;
            }
            /**
             * 业务逻辑代码
             * */

            //将查询的数据序列化成json对象,设置缓存方便下次使用,将缓存设置进redis的操作放进锁里面,避免时序性带来的重复查询db的问题
            String cacheCatalog = JSON.toJSONString(map);
            redisTemplate.opsForValue().set("catalogJson",cacheCatalog);
            return map;
        }
    }

分布式锁Redisson-lock

  • 其原理其实还是通过setnx实现的。setnx的意思是不存在即创建,存在的话则不创建。即同一时刻只能设置成功一个。
  • springboot整合redisson

TODO

  • 非springboot整合redisson

1、导入maven坐标

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.13.4</version>
</dependency>

2、编写配置文件

@Configuration
public class MyRedisConfig {

    // destoryMethod为redisson被销毁时调用的方法。
    @Bean(destroyMethod = "shutdown")
    public RedissonClient redisson() {
        Config config = new Config();
        // 创建单例模式的配置
        config.useSingleServer().setAddress("redis://" + YourIP + ":6379");
        return Redisson.create(config);
    }
}

3、测试

 @ResponseBody
    @GetMapping("/hello")
    private String hello(){
        //注意这里,这里只要调用的锁名字相同,那么就是同一把锁。两个商品服务都调用了这个锁,那么只有一个会被锁住。
        //很好的解决了分布式下缓存不一致的问题
        //RLock本质上其实是可重入锁
        //阻塞等待机制(默认),可以自己设置等待时间以及上锁后自动解锁时间
        RLock lock = redisson.getLock("ny_lock");
        // 自动解锁,加锁以后10秒钟自动解锁,看门狗不续命,使用的话自动解锁时间必须大于业务时间。
		//lock.lock(10, TimeUnit.SECONDS);
		
        //上锁
        lock.lock();
        try{
            System.out.println(Thread.currentThread().getId()+":"+Thread.currentThread().getName()+"加锁成功");
            Thread.sleep(10000);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getId()+":"+Thread.currentThread().getName()+"解锁成功");
            lock.unlock();
        }
        return "hello";
    }
#
看门狗机制可以确保在redission在关闭之前,即该线程执行完之前,
或异常退出之前,该线程一直占有该线程需要的锁,锁续期时间为30秒。
很好的解决了如某线程占有锁期间服务宕机导致死锁的情况,以及某个
线程业务过长,业务执行期间锁自动过期被删掉的情况。

分布式锁Redisson-readwritelock

  • 读锁与写锁一般搭配使用,写锁期间只能有一个线程在写,其他线程只能等待。写锁完成后,占有读锁的可以一起读。即可以分布式读,不能分布式写。这样的好处就是可以保证读取到的数据一定是最新的。
#写锁
@ResponseBody
    @GetMapping("/write")
    private String writeHi(){
        String s = "";
        RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");
        lock.writeLock().lock();
        try {
            s = UUID.randomUUID().toString();
            redisTemplate.opsForValue().set("write",s);
            System.out.println(Thread.currentThread().getId()+"写锁正在写数据");
            Thread.sleep(10000);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.writeLock().unlock();
        }
        return s;
    }

#读锁
    @ResponseBody
    @GetMapping("/read")
    private String readHi(){
        String s = "";
        RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");
        lock.readLock().lock();
        try {
            s = redisTemplate.opsForValue().get("write");
            System.out.println(Thread.currentThread().getId()+"写锁正在读数据");
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.readLock().unlock();
        }
        return s;
    }
  • 注意的是,写+读模式,读必须等写完成才能读。读+写,写必须等读完成才能写。写+写阻塞等待。读+读则没关系。

分布式信号量semaphore

  • 信号量可以用于限流操作,例如说后期服务最大只能够支持10000个并发,那么可以定义10000个信号量。当获取到信号量的时候即可执行相关代码逻辑,当没有信号量时可以返回错误提示,如:
 	@ResponseBody
    @GetMapping("/park")
    private String park(){
        //从redis中获取key为semaphore的信号量
        RSemaphore semaphore = redisson.getSemaphore("semaphore");
        //判断信号量是否还有剩余
        boolean b = semaphore.tryAcquire();
        if(b){
            return "停车成功";
        }
        return "暂无车位,请稍后再试";
    }


    @ResponseBody
    @GetMapping("/go")
    private String go(){
        RSemaphore semaphore = redisson.getSemaphore("semaphore");
        //释放一个信号量
        semaphore.release();
        return "欢迎下次光临";
    }

分布式锁缓存的一致性问题

  • 双写模式

写数据的时候,同时写缓存
可能存在脏数据的问题,最终一致性存在误差
在这里插入图片描述
可以通过加锁的方式,保证写数据库与写缓存同一时间只能由一个线程执行,从而实现一致性。

  • 失效模式

写数据的时候,同时删除缓存。下一次查询的时候,再更新缓存。
在这里插入图片描述
还是存在一致性的问题。还是可以通过加读写锁进行解决。不管是读+写还是写+读的模式,都需要按照顺序执行完毕之后再进行锁内操作。

  • 使用canal订阅binlog的方式

这里是引用

SpringCache

springboot整合

  • 导入坐标依赖
<dependency>
    <groupId>org.springframework.b oot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
  • yml配置文件,配置使用redis缓存
spring:
  cache:
    # 指定缓存类型为redis
    type: redis
    redis:
      # 指定redis中的过期时间为1h,(不应统一设置缓存失效时间,存在缓存雪崩的风险)
      time-to-live: 3600000
      # 缓存前缀
      # 不指定前缀名,就让分区名作为前缀 key-prefix: cache_
      # 开启缓存前缀
      use-key-prefix: true
      # 防止缓存穿透,查询不到数据返回null
      cache-null-values: true
  • springboot启动类,启用缓存
@EnableCaching
  • 配置cache缓存文件
@Configuration
@EnableCaching
public class MyCacheConfig {
    @Bean
    public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
        
        CacheProperties.Redis redisProperties = cacheProperties.getRedis();
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
        //指定缓存序列化方式为json
        config = config.serializeValuesWith(
            RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
        //设置配置文件中的各项配置,如过期时间
        if (redisProperties.getTimeToLive() != null) {
            config = config.entryTtl(redisProperties.getTimeToLive());
        }

        if (redisProperties.getKeyPrefix() != null) {
            config = config.prefixKeysWith(redisProperties.getKeyPrefix());
        }
        if (!redisProperties.isCacheNullValues()) {
            config = config.disableCachingNullValues();
        }
        if (!redisProperties.isUseKeyPrefix()) {
            config = config.disableKeyPrefix();
        }
        return config;
    }
}
  • 接口上添加@Cacheable()注解
//Cacheable的使用表示当前方法返回的结果需要放入缓存中。如果缓存有该结果,则该接口不调用直接从缓存拿数据,如果没有,则调用该接口并放入缓存
//该注解里面的名字表示将该缓存的结果分到那个区
    @Cacheable(value = "{category}", key = "#root.method.name")
    @Override
    public List<CategoryEntity> getLevel1Categorys() {
        List<CategoryEntity> categoryEntities  = this.baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
        return categoryEntities;
    }

SpringCache相关的默认配置:
序列化采用的是jdk自带的序列化,可读性差、兼容性差,应将数据保存为json格式—
过期时间为-1,永不过期—yml配置文件修改存活时间
key名字自动生成—可通过注解的key属性配置自己的key
在这里插入图片描述

  • @CacheEvict注解。使用该注解可以对缓存进行删除,在缓存一致性的问题下,可以使用该注解达到失效策略的使用,即在更新db的时候删除缓存,下次查询的时候再写入缓存。有两个关键的属性值为key和value,要删除哪一块的缓存以及,这两个值就得对应前面新增缓存@CacheAble中key,value的值。
/**
     * 缓存失效下CacheEvict的使用
     * */
    @CacheEvict(value = "category", key = "'getLevel1Categorys'")
    @Override
    public void updateDetail(CategoryEntity category) {
        //先更新自己
        this.updateById(category);
        //级联更新其他
        categoryBrandRelationService.updateCategory(category.getCatId(),category.getName());
    }
  • Caching的使用。使用该注解,可以将多个注解相关的操作合并到一起,一起执行,如下所示:
@Caching(evict = {
            @CacheEvict(value = "category", key = "'getLevel1Categorys'"),
            @CacheEvict(value = "category", key = "'getCatalogJson2'")
    })
  • 当然,举个例子,像批量删除,,也可以使用CacheEvict进行批量操作,只需要:
#注意,使用该方式进行批量删除,一定要开启允许使用前缀。use-key-prefix: true
#不然的话进行删除的时候会把所有的key全部删掉
@CacheEvict(value = "category",allEntries = true)

小结

  • 读模式
    缓存穿透:大量查询一个不存在的数据。解决方案:缓存空数据,可通过spring.cache.redis.cache-null-values=true返回null数据
    缓存击穿:大量并发进来同时查询一个正好过期的数据。解决方案:CacheAble注解中添加sync=true加锁,解决缓存击穿问题。
    缓存雪崩:大量的key在同一时间过期。解决:加随机时间。
  • 写模式
    如果对于最终一致性(弱一致性)要求不高,加缓存过期时间即可。如果一致性要求高,可以通过加读写锁的方式解决。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值