当前位置:   article > 正文

微服务入门(三)反向代理、负载均衡、Nginx、缓存、分布式锁、Redisson、SpringCache_反向代理缓存

反向代理缓存

反向代理

代理其实就是一个中介,A和B本来可以直连,中间插入一个C,C就是中介。根据代理的角色,可以分为正向代理,反向代理
正向代理即是客户端代理, 代理客户端, 服务端不知道实际发起请求的客户端.
在这里插入图片描述
比如我们国内访问谷歌,直接访问访问不到,我们可以通过一个正向代理服务器,请求发到代理服务器,代理服务器能够访问谷歌,这样由代理去谷歌取到返回数据,再返回给我们,这样我们就能访问谷歌了
这个现实中可以类比为买票的黄牛

反向代理即是服务端代理, 代理服务端, 客户端不知道实际提供服务的服务端
在这里插入图片描述
反向代理(Reverse Proxy)实际运行方式是指以代理服务器来接受internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给internet上请求连接的客户端,此时代理服务器对外就表现为一个服务器
反向代理的作用:
(1)保证内网的安全,阻止web攻击,大型网站,通常将反向代理作为公网访问地址,Web服务器是内网

(2)负载均衡,通过反向代理服务器来优化网站的负载
反向代理在现实中可以类比为租房的中介

Nginx

使用Nginx作为反向代理服务器
本机搭建域名环境
如果正式发布,需要购买域名什么的,测试就在本地搭建,搭建步骤:
修改host文件,是用于本地dns服务的,采用ip 域名的格式写在一个文本文件当中,其作用就是将一些常用的网址域名与其对应的IP地址建立一个关联“数据库”,当用户在浏览器中输入一个网址时,系统会首先自动从Hosts文件中寻找对应的IP地址,一旦找到,系统会立即打开对应网页,如果没有找到,则系统再会将网址提交DNS域名解析服务器进行IP地址的解析。
这个文件的位置在C:\Windows\System32\drivers\etc下
在这里插入图片描述
如果嫌每次修改麻烦,可以使用SwitchHosts,这个可以自己尝试,我电脑使用这个容易死机,就先不用了
因为我在本地测试的,所以把本地的localhost修改为一个域名,比如mydomain.com,如果有虚拟机或者其他环境,可以换成对应的ip
在这里插入图片描述
然后我访问服务,http://127.0.0.1:7000/coupon/coupon/test,然后修改为对应域名

http://mydomain.com:7000/coupon/coupon/test
  • 1

同样可以访问

Nginx配置
上边演示的只是本地环境,如果在其他服务上,安装nginx,这个域名对应那个nginx的ip地址,然后在nginx里边对访问进行分配,根据规则,对不同url分配不同的服务
Nginx下载解压,然后在安装目录启动命令行,运行nginx.exe,

E:\springbootLearn\nginx-1.20.1\nginx-1.20.1>nginx.exe
  • 1

然后在浏览器输入http://localhost:8080/,如果出现
在这里插入图片描述
说明启动成功,然后打开conf目录下的nginx.conf,在里边进行配置,比如这里我访问http://mydomain.com:9001/coupon/coupon/test,正式环境,这个域名对应的ip设置一下

server {
        listen       9001;
        server_name  mydomain.com;

		location ~ /coupon/ {           
			proxy_pass http://127.0.0.1:7000;
		}
	}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

这样访问上一个url,请求里包含coupon,就可以跳转到对应ip的地址,这个正式发布,127.0.0.1可以改为对应服务的地址
Nginx基本使用

负载均衡

在这里插入图片描述
负载均衡,就是将负载分摊到多个服务器,有多种算法,这个在nginx里边配置,就需要配置多个服务器地址
nginx负载均衡

http {
   upstream gulimail{
        server 127.0.0.1:7000;
        server 192.168.0.28:8002;
    }
	
	server {
        listen       80;
        server_name  gulimail.com;

		location / {           
			proxy_pass http://gulimail
			proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
		}
		
	}

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

gulimail是服务的域名或者项目的代号,然后设置这个proxy_set_header Host $host;原因是nginx会丢失Host,不加上,网关拦截host会失败
项目中,gateWay网关可以设置为

        - id: gulimail_host_route
           uri: lb://gulimall-product
          predicates:
            - Host=**.gulimail.com,gulimail.com
  • 1
  • 2
  • 3
  • 4

拦截到这个gulimail.com,就会跳转到商品服务
这种粗粒度的匹配最后放在最后,放在前边,会直接拦截掉一些请求,走不到下边配置的匹配

- id: product_route
          uri: lb://gulimall-product
          predicates:
            - Path=/api/product/**
          filters:
            - RewritePath=/api/(?<segment>.*),/$\{segment}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

像这样的可能无法匹配,在前边就被拦截了

按我的理解,总结一下流程,首先在本地新建域名,对应nginx的ip地址,然后再配置nginx,根据url来分配对应的服务

动静分离

项目中,有些请求是需要后台处理的(如:.jsp,.do等等),有些请求是不需要经过后台处理的(如:css、html、jpg、js等等文件),这些不需要经过后台处理的文件称为静态文件,否则动态文件。如果将静态文件也放在服务器,访问时候性能会降低,可以把静态资源放到另一个服务器,比如将静态资源放到nginx中,动态资源转发到tomcat服务器中。
在这里插入图片描述
nginx配置
通过location对请求url进行匹配即可,在/Users/Hao/Desktop/Test(任意目录)下创建 /static/imgs 配置如下

###静态资源访问
server {
  listen       80;
  server_name  static.haoworld.com;
  location /static/imgs {
       root /Users/Hao/Desktop/Test;
       index  index.html index.htm;
   }
}
###动态资源访问
 server {
  listen       80;
  server_name  www.haoworld.com;
    
  location / {
    proxy_pass http://127.0.0.1:8080;
     index  index.html index.htm;
   }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

参考文章:
【Nginx】实现动静分离
Nginx实现动静分离
Nginx的动静分离

缓存

为了系统性能的提升, 我们一般都会将部分数据放入缓存中, 加速访问。 而 db 承担数据落
盘工作。
哪些数据适合放入缓存?
1.即时性、 数据一致性要求不高的
2. 访问量大且更新频率不高的数据(读多, 写少)
在这里插入图片描述
伪代码:

data = cache.load(id);//从缓存加载数据
If(data == null){
	data = db.load(id);//从数据库加载数据
	cache.put(id,data);//保存到 cache 中
} 
return data;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

最简单的缓存是本地缓存,在代码里写一个Map,然后查询数据之前,查询Map里是否包含数据,没有就查询数据库,查询出数据,保存一份到Map中。
在这里插入图片描述
本地缓存在分布式下会出现问题,负载均衡可能访问A,B,C三台服务器,有可能更改A服务器的缓存,然后第二次到B服务器访问,B服务器缓存还是老的,没办法同步修改缓存,保持数据一致性
分布式情况下,将缓存提取出来,所有服务器访问和操作同一缓存
在这里插入图片描述注意: 在开发中, 凡是放入缓存中的数据我们都应该指定过期时间, 使其可以在系统即使没有主动更新数据也能自动触发数据加载进缓存的流程。 避免业务崩溃导致的数据永久不一致问题
spring boot整合redis
pom.xml增加引用

<dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>
  • 1
  • 2
  • 3
  • 4

application.yml中配置

 redis:
    host: 192.168.56.10
    port: 6379
  • 1
  • 2
  • 3

使用 RedisTemplate 操作 redis

@Autowired
StringRedisTemplate stringRedisTemplate;
@Test
public void testStringRedisTemplate(){
	ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
	ops.set("hello","world_"+ UUID.randomUUID().toString());
	String hello = ops.get("hello");
	System.out.println(hello);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

Idea搜索jar包中的文件,快捷键是ctrl+N,搜索redisautoconfiguration,
在这里插入图片描述
在这里插入图片描述
包含RedisTemplate和StringRedisTemplate,一个是任意类型的,另一个是方便操作字符串的
在程序中加入缓存逻辑

 public Map<String, List<Catelog2Vo>> getCatelogJson2() {
        //加入缓存逻辑,缓存中存的数据是JSON字符串
        //JSON跨语言,跨平台兼容
        //从缓存中取出的数据要逆转为能用的对象类型,序列化与发序列化
        String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON");
        if (StringUtils.isEmpty(catalogJSON)) {
            //缓存中没有数据,查询数据库
            Map<String, List<Catelog2Vo>> catelogJsonFromDb = getCatelogJsonFromDbWithRedisLock();
            //查到的数据再放入缓存,将对象转为JSON放入缓存中
            String s = JSON.toJSONString(catelogJsonFromDb);
            stringRedisTemplate.opsForValue().set("catalogJSON", s, 1, TimeUnit.DAYS);
            return catelogJsonFromDb;
        }
        Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
        });
        return result;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

因为JSON是跨平台的,所以redis里保存JSON字符串,然后保存和取出都进行序列化和反序列化
压力测试发现,会产生堆外内存溢出(OutOfDirectMemoryError),这个是因为spring boot 2.0以后,默认使用lettuce操作Redis客户端,它使用netty进行网络通信,lettuce的bug导致堆外内存溢出,解决方式有两个,第一个是升级lettuce的客户端,另一个是切换到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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

lettuce和jedis是操作redis的底层客户端,spring再次封装redisTemplate,通过redisTemplate进行操作,不需要关心底层客户端
高并发下缓存会出现的问题
缓存穿透
在这里插入图片描述
缓存雪崩
在这里插入图片描述
缓存击穿
在这里插入图片描述
解决办法:空结果缓存,解决缓存穿透问题,设置过期时间(加随机值),解决缓存雪崩问题,设置锁,解决缓存击穿问题

加锁

本地锁(单体服务)

  synchronized (this) {
            String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON");
            if (StringUtils.isEmpty(catalogJSON)) {
                Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
                });
                return result;
            }
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

只要是同一把锁,就能锁住需要这个锁的所有进程,synchronized(this):springboot所有的组件在容器中都是单例的。
本地锁:synchronized,JUC(Lock),只能锁住当前进程,在分布式情况下,想要锁住所有,必须使用分布式锁
使用本地锁的时候,要把查询数据库和存放缓存放在同一锁内,如果放入缓存在锁外,高并发下,就会出现1号线程处理完逻辑,释放锁,2号线程获取锁,1号线程存入缓存,在存缓存的过程中,可能网络延迟等原因,2号线程读取缓存的时候,1号还没有存入缓存,查询两边数据库
在这里插入图片描述
分布式锁
分布式锁演进原理:分布式锁就是多个服务去占锁,占住的执行逻辑,然后释放锁,在一个服务占锁期间,其他服务只能等待,等待释放锁,然后再去占锁,等待可以使用自旋方式
在这里插入图片描述
分布式程锁Redis
阶段一:
在这里插入图片描述
阶段二
在这里插入图片描述
阶段三
在这里插入图片描述
阶段四
在这里插入图片描述
最终结果
在这里插入图片描述

 //分布式程锁Redis
    public Map<String, List<Catelog2Vo>> getCatelogJsonFromDbWithRedisLock() {
        //占分布式锁,同时设置锁过期时间,必须和加锁同步原子操作
        String uuid = UUID.randomUUID().toString();
        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "111", 300, TimeUnit.SECONDS);
        if (lock) {
            //加锁成功,执行业务
            Map<String, List<Catelog2Vo>> dataFromDB;
            try {
                dataFromDB = getDataFromDB();
            } finally {
                String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1])else return 0 end";
                //删除锁,Lua脚本
                Long lock1 = stringRedisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock", uuid));
            }
            return dataFromDB;
        } else {
            //加锁失败,休眠2秒,重试
            try {
                Thread.sleep(2000);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return getCatelogJsonFromDbWithRedisLock();//自旋
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

不要设置固定的字符串,而是设置为随机的大字符串,可以称为token。
通过脚本删除指定锁的key,而不是DEL命令。
上述优化方法会避免下述场景:
a客户端获得的锁(键key)已经由于过期时间到了被redis服务器删除,但是这个时候a客户端还去执行DEL命令。而b客户端已经在a设置的过期时间之后重新获取了这个同样key的锁,那么a执行DEL就会释放了b客户端加好的锁。

解锁脚本的一个例子将类似于以下
if redis.call("get",KEYS[1]) == ARGV[1]
then
    return redis.call("del",KEYS[1])
else
    return 0
end
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

核心就是原子加锁,原子解锁
Redisson
Redisson是Redis官方推荐的Java版的Redis客户端。它提供的功能非常多,也非常强大,可以使用它来实现分布式锁
Redisson
maven引入

<dependency>
   <groupId>org.redisson</groupId>
   <artifactId>redisson</artifactId>
   <version>3.16.3</version>
</dependency> 
  • 1
  • 2
  • 3
  • 4
  • 5

有中文文档可以查看(Github网站经常访问不通。。。。。)
Wiki Home
config目录下,新建MyRedissonConfig

@Configuration
public class MyRedissonConfig {

    @Bean(destroyMethod = "shutdown")
    RedissonClient redisson() throws IOException{
        Config config = new Config();
        //集群模式
        //config.useClusterServers().addNodeAddress("127.0.0.1:7004","127.0.0.1:7001");
        //单节点模式
        config.useSingleServer().setAddress("redis://192.168.56.10:6379");
        return Redisson.create(config);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

测试redisson锁

分布式锁和同步器

 @ResponseBody
    @GetMapping("/hello")
    public String hello() {
        //只有锁名字一样,就是同一把锁
        RLock lock = redisson.getLock("my-lock");
        //阻塞式等待。默认加的锁都是30s时间。
        //1)、锁的自动续期,如果业务超长,运行期间自动给锁续上新的30s。不用担心业务时间长,锁自动过期被删掉
        //2)、加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30s以后自动删除
        lock.lock();
        try {
            System.out.println("加锁测试,执行业务..." + Thread.currentThread().getId());
            Thread.sleep(10000);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //解锁
            System.out.println("释放锁" + Thread.currentThread().getId());
            lock.unlock();
        }

        return "hello";
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

Redisson锁的特点:有个看门狗机制,在锁到期之前自动续期

  //阻塞式等待。默认加的锁都是30s时间。
        //1)、锁的自动续期,如果业务超长,运行期间自动给锁续上新的30s。不用担心业务时间长,锁自动过期被删掉
        //2)、加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30s以后自动删除
  • 1
  • 2
  • 3

如果自己指定解锁时间,一定要大于业务时间,不会自动续期,可能在业务执行之前就已经解锁了

// 加锁以后10秒钟自动解锁
// 无需调用unlock方法手动解锁
lock.lock(10, TimeUnit.SECONDS);
  • 1
  • 2
  • 3

如果传递了锁的超时时间,就发送给redis执行脚本,进行占锁,默认时间就是我们制定的时间
如果未指定锁的超时时间,就是用30*1000[LockWatchdogTimeout看门狗的默认时间]
在这里插入图片描述
leaseTime!=-1L,说明自己指定了时间,进入
在这里插入图片描述
执行脚本
未指定时间,从配置拿,

this.tryLockInnerAsync(this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
在这里插入图片描述

只要占锁成功,就会启动一个定时任务,重新给锁设置过期时间,新的过期时间就是看门狗的默认时间

 this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();
 ...
protected RFuture<Boolean> renewExpirationAsync(long threadId) {
        return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('pexpire', KEYS[1], ARGV[1]); return 1; end; return 0;", Collections.singletonList(this.getName()), new Object[]{this.internalLockLeaseTime, this.getLockName(threadId)});
    }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

在这里插入图片描述
internalLockLeaseTime / 3L ,3分之一看门狗时间会自动续期,续到30s
最佳实战:使用 lock.lock(30, TimeUnit.SECONDS);,省掉整个续期过程,手动解锁

读写锁
如果有写操作,加写锁,其他读取这个结果的都要等待,如果只是读取,互不影响,如果都是写,需要排队等待

 @GetMapping("/write")
    @ResponseBody
    public String writeValue() {
        RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");
        String s = "";
        //加写锁
        RLock rLock = lock.writeLock();
        try {
            rLock.lock();
            s = UUID.randomUUID().toString();
            redisTemplate.opsForValue().set("writeValue", s);
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            rLock.unlock();
        }
        return s;
    }

    @GetMapping("/read")
    @ResponseBody
    public String redValue() {
        RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");
        String s = "";
        //获取读锁,如果有其他的线程在写操作, 则需要等到写锁释放锁以后才可进行读的操作
        RLock rLock = lock.readLock();
        rLock.lock();
        try {
            s = redisTemplate.opsForValue().get("writeValue");
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            rLock.unlock();
        }
        return  s;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

写在操作的时候,读等待,如果操作完成,写释放锁,然后读进行操作,获得数据。读写锁保证一定能读到最新数据。修改期间,写锁是一个排他锁(互斥锁、独享锁),读锁是一个共享锁。写锁没释放,读就必须等待

+读:相当于无锁,并发读,只会在redis中记录好所有当前的读锁,同时加锁成功
写+读:等待写锁释放
写+写:阻塞方式
读+写:读的时候,写入数据,有读锁,写也需要等待
只要有写存在,都需要等待
  • 1
  • 2
  • 3
  • 4
  • 5

信号量(Semaphore)
基于Redis的Redisson的分布式信号量(Semaphore)Java对象RSemaphore采用了与java.util.concurrent.Semaphore相似的接口和用法。同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。

@GetMapping("/park")
    @ResponseBody
    public String park() throws InterruptedException {

        RSemaphore semaphore = redisson.getSemaphore("park");
        
//        semaphore.acquire(); // 阻塞等待,获取一个信号,获取一个值,占一个车位

        boolean flag = semaphore.tryAcquire();//不阻塞,获取不到直接返回false,可以做限流
        

        if (flag) {
            return "停车了!";
        } else {
            return "车库满了!";
        }

    }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
 @GetMapping("/go")
    @ResponseBody
    public String go() {

        RSemaphore semaphore = redisson.getSemaphore("park");

        semaphore.release();//释放一个车位

        return "车开走了!";
    }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

闭锁(CountDownLatch)

RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
latch.trySetCount(3);//等待3个
latch.await();//等待闭锁都完成


RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
latch.countDown();//计数减一

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

分布式锁Redisson

 public Map<String, List<Catelog2Vo>> getCatelogJsonFromDbWithRedissonLock() {
        //锁的名字,锁的粒度,越细越快
        RLock lock = redisson.getLock("catalogJson-lock");
        lock.lock();
        //加锁成功,执行业务
        Map<String, List<Catelog2Vo>> dataFromDB;
        try {
            dataFromDB = getDataFromDB();
        } finally {
           lock.unlock();
        }
        return dataFromDB;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

缓存数据一致性

双写模式
在这里插入图片描述
双写模式:写入数据库,同时写入缓存
失效模式
在这里插入图片描述
失效模式:读取数据库,然后删除缓存,下一次看缓存中没有,重新从数据库获取。可能出现的问题:操作1写数据库db-1,然后删掉缓存,操作2写入数据库db-2,同时操作时间比较长,然后操作3读缓存,缓存已经被操作1删除,操作3读取db-1,然后操作2删除缓存,操作3更新缓存,结果缓存中保存db-1的数据。加入读写锁,如果经常写,经常读,会有一点影响性能。偶尔读写影响不大
在这里插入图片描述

canal,译意为水道/管道/沟渠,主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费。
在这里插入图片描述
超详细的Canal入门,看这篇就够了!

SpringCache缓存简化分布式缓存的开发

整合springcache简化缓存开发
1) 、引入依赖
.spring-boot-starter-cache、 spring-boot-starter-data-redis2)
2)、写配置
(i)、自动配置了哪些
	CacheAuroConfiguration会导入 RedisCacheConfiguration;
	自动配好了缓存管理器RedisCachcManager
( 2)、配置使用redis作为缓存
	spring.cache.type=redis
3)、测试使用缓存
	@Cacheable: Triggers cache popuLation.:触发将数据保存到缓存的操作
	@CacheEvict: Triggers cache eviction.:触发将数据从缓存删除的操作
	@CachePut: Updates the cache without interfering with the method execution.:不影响方法执行更新缓存
	@Caching:Regroups multiple cache operations to be applied on a method.:组合以上多个操作
	@CacheConfig: Shares some common cache-related settings at class-Level.:在类级别共享相同的缓存配置
	1)、开启缓存功能@EnabLeCaching
	2) 、只需要使用注解就能完成缓存操作
4) 、原理:
CacheAutoConfiguration -> RedisCacheConfiguration ->
自动配置了RedisCacheManager->初始化所有的缓存->每个缓存决定使用什么配置->如果redisCacheConfiguration有就用已有的,没有就用默认配置
->想改缓存的配置,只需要给容器中放一个RedisCacheConfiguration即可->就会应用到当前RedisCacheManager管理的所有缓存分区中

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

引入 pom依赖

		<!-- 因为lettuce导致堆外内存溢出 这里暂时排除他 使用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>
		<!--  分布式缓存 需要和redis配合使用 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-cache</artifactId>
		</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

编写配置文件

# 配置使用redis作为缓存
spring.cache.type=redis
# 以毫秒为单位 1小时
spring.cache.redis.time-to-live=3600000
# 配置缓存名的前缀 如果没配置则使用缓存名作为前缀
# spring.cache.redis.key-prefix=CACHE_
# 配置前缀是否生效  默认为ture
#spring.cache.redis.use-key-prefix=false
# 是否缓存空值 默认为true,防止缓存穿透
spring.cache.redis.cache-null-values=true
#spring.cache.cache-names=
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

开启缓存
@EnableCaching
使用缓存
1)@Cacheable: 触发将数据写入缓存

     /**
     * 1、每一个需要缓存的数据我们都要来指定放到哪个名字的缓存;【按照业务类型来划分取名】
	 *@Cacheable(value = {"category"})
     * 	代表当前方法的结果需要缓存 如果缓存中有,方法不调用;
     * 	如果缓存中没有,会调用该方法,最后将方法的结果放入缓存
     * 2、默认行为
	 *	1)如果缓存中有,方法不调用
	 *  2)key默认自动生成,缓存的名字::SimpleKey[](自动生成的key值)
	 *	3)缓存的value值,默认使用jdk序列化机制,将序列化后的数据存到redis
	 *  4)默认ttl时间 -1
     * 3、自定义
     * 	1)指定缓存生成指定的key key属性指定,接受一个spel
     * 	2)指定缓存的过期时间  配置文件修改ttl
     * 	3)将缓存的value保存为json格式
     * 4、缓存的三大问题
     * 	1)缓存击穿: springcache 默认是不加锁的,需要设置sync = true
     * 	2)
     */
@Cacheable(value = {"category"}, key = "#root.method.name", sync = true)
public List<CategoryEntity> getLeve1Categorys() {
		System.out.println("getLeve11categorys.....");
long l = System.currentTimeMillis();
List<CategoryEntity> categoryEntities = baseMapper.selectList(new Querywrapper<CategoryEntity>()return categoryEntities;
		
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

@CacheEvict: 触发将数据从缓存中删除

/**
级联更新所有关联的数据
* cacheEvict:失效模式
*1、同时进行多种缓存操作@caching
* 2、指定删除某个分区下的所有数据ecacheEvict(valug,= "category" , alLEntries = true)*3、存储同一类型的数据,都可以指定成同一个分区。分区名默认就是缓存名
*@param category
*/
//    @Caching(evict = {
//            @CacheEvict(value = "category", key = "'getLeve1Categorys'"),
//            @CacheEvict(value = "category", key = "'getCatalogJson'")
//    })
//     allEntries = true 删除category 分区的所有缓存 批量清除
   // @CacheEvict(value = {"category"}, allEntries = true) //失效模式
    @CachePut//双写模式
    public void updateCascade(CategoryEntity category) {
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

@CachePut: 不影响方法执行更新缓存

@Caching: 组合以上缓存的操作

@Caching(evict = {
            @CacheEvict(value = "category", key = "'getLeve1Categorys'"),
            @CacheEvict(value = "category", key = "'getCatalogJson'")
    })
  • 1
  • 2
  • 3
  • 4

@CacheConfig: 在类级别共享相同的缓存配置

修改缓存存储的序列化器

@EnableConfigurationProperties(CacheProperties.class)
@EnableCaching
@Configuration
public class MyCacheConfig {

/**
配置文件中的东西没有用上;
*
1、原来和配置文件绑定的配置类是这样子的
@ConfigurationProperties(prefix = "spring.cache")public cLass CacheProperties
*2、要让他生效:@EnableConfigurationProperties(CacheProperties.class)
*@return
*/

    @Bean
    public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {

        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();

        // 修改默认的序列化器
        config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
        config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));

        CacheProperties.Redis redisProperties = cacheProperties.getRedis();

        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;
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

Spring-Cache的不足;
1)、读模式:
缓存穿透:查询一个null数据。解决:缓存空数据; ache-null-values=true
缓存击穿:大量并发进来同时查询一个正好过期的数据。解决。加锁;springcache 默认是不加锁的,需要设置sync = true
缓存雪崩:大量的key同时过期。解决:加随机时间。加上过期时间。: spring.cache.redis.time-to-live=3600000
2) 、写模式:(缓存与数据库一致)
1)、读写加锁。
2)、引入Canal,感知到MysQL的更新去更新数据库
3)、读多写多,直接去数据库查询就行

总结:
常规数据(读多写少,即时性,- 致性要求不高的数据) ;完全可以使用Spring-Cache
特殊数据:特殊设计
I

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Cpp五条/article/detail/195332
推荐阅读
相关标签
  

闽ICP备14008679号