Redis 学习之一招击穿自己的系统,附送 N 个击穿解决大礼包 | 原力计划

作者 | Mark_MMXI

来源 | CSDN博客,责编 | 夕颜

出品 | CSDN(ID:CSDNnews)

缓存的存在是为了在高并发情形下,缓解DB压力,提高业务系统体验。业务系统访问数据,先去缓存中进行查询,假如缓存存在数据直接返回缓存数据,否则就去查询数据库再返回值。

Redis是一种缓存工具,是一种缓存解决方案,但是引入Redis又有可能出现缓存穿透、缓存击穿、缓存雪崩等问题。本文就对缓存雪崩问题进行较深入剖析,并通过场景模型加深理解,基于场景使用对应的解决方案尝试解决。

缓存原理及Redis解决方案

首先,我们来看一下缓存的工作原理图:

Redis 本质上是一个 Key-Value 类型的内存数据库。因为是纯内存操作,Redis 的性能非常出色,每秒可以处理超过 10、万次读写操作。Redis 还有一个优势就是是支持保存多种数据结构,例如 String、List、Set、Sorted Set、hash等。

缓存雪崩

2.1 缓存雪崩解释

缓存雪崩的情况是说,当某一时刻发生大规模的缓存失效的情况,比如你的缓存服务宕机了,DB直接负载大量请求压力导致挂掉。

2.2 模拟缓存雪崩

按照缓存雪崩的解释,其实我们要模拟,只需要达到以下几个点:

  1. 同一时刻大规模缓存失效。

  2. 失效的时刻有大量的查询请求冲击DB

   

 @Testpublic void testQuery(){ExecutorService es = Executors.newFixedThreadPool(10);int loop= 1000;int init=2000;//查询1k个key放进缓存for (int i = init; i < loop+init; i++) {userService.queryById(i);}//缓存过期时间为1s,等待1s同时过期try {Thread.sleep(1000);}catch (Exception e){e.printStackTrace();}//开始了使用多线程疯狂查询for (int i = 0; i < 100; i++) {es.execute(() -> {for (int k = init; k < loop+init; k++) {userService.queryById(k);}});}}

为了加快崩坏的速度,把数据库的最大连接数调整成5,同时增大数据库表的数据量达到百万级别。

然后执行测试程序,很快程序就报错并停止,详细错误如下:

Exception in thread "pool-1-thread-12" org.springframework.data.redis.RedisSystemException: Redis exception; nested exception is io.lettuce.core.RedisException: Connection is closed
at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:74)
at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:41)
at org.springframework.data.redis.PassThroughExceptionTranslationStrategy.translate(PassThroughExceptionTranslationStrategy.java:44)
at org.springframework.data.redis.FallbackExceptionTranslationStrategy.translate(FallbackExceptionTranslationStrategy.java:42)
at org.springframework.data.redis.connection.lettuce.LettuceConnection.convertLettuceAccessException(LettuceConnection.java:270)
at org.springframework.data.redis.connection.lettuce.LettuceStringCommands.convertLettuceAccessException(LettuceStringCommands.java:799)
at org.springframework.data.redis.connection.lettuce.LettuceStringCommands.get(LettuceStringCommands.java:68)
at org.springframework.data.redis.connection.DefaultedRedisConnection.get(DefaultedRedisConnection.java:260)
at org.springframework.data.redis.cache.DefaultRedisCacheWriter.lambda$get$1(DefaultRedisCacheWriter.java:109)
at org.springframework.data.redis.cache.DefaultRedisCacheWriter.execute(DefaultRedisCacheWriter.java:242)
at org.springframework.data.redis.cache.DefaultRedisCacheWriter.get(DefaultRedisCacheWriter.java:109)
at org.springframework.data.redis.cache.RedisCache.lookup(RedisCache.java:88)
at org.springframework.cache.support.AbstractValueAdaptingCache.get(AbstractValueAdaptingCache.java:58)
at org.springframework.cache.interceptor.AbstractCacheInvoker.doGet(AbstractCacheInvoker.java:73)
at org.springframework.cache.interceptor.CacheAspectSupport.findInCaches(CacheAspectSupport.java:554)
at org.springframework.cache.interceptor.CacheAspectSupport.findCachedItem(CacheAspectSupport.java:519)
at org.springframework.cache.interceptor.CacheAspectSupport.execute(CacheAspectSupport.java:401)
at org.springframework.cache.interceptor.CacheAspectSupport.execute(CacheAspectSupport.java:345)
at org.springframework.cache.interceptor.CacheInterceptor.invoke(CacheInterceptor.java:61)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:747)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:689)
at com.example.demo.user.service.impl.UserServiceImpl$$EnhancerBySpringCGLIB$$ba6638d2.queryById(<generated>)
at com.example.demo.DemoApplicationTests$1.run(DemoApplicationTests.java:55)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
Caused by: io.lettuce.core.RedisException: Connection is closed
at io.lettuce.core.protocol.DefaultEndpoint.validateWrite(DefaultEndpoint.java:195)
at io.lettuce.core.protocol.DefaultEndpoint.write(DefaultEndpoint.java:137)
at io.lettuce.core.protocol.CommandExpiryWriter.write(CommandExpiryWriter.java:112)
2020-03-08 22:31:14.432 ERROR 37892 --- [eate-1895102622] com.alibaba.druid.pool.DruidDataSource   : create connection SQLException, url: jdbc:mysql://localhost:3306/redis_demo?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC, errorCode 1040, state 08004java.sql.SQLNonTransientConnectionException: Data source rejected establishment of connection,  message from server: "Too many connections"

主要问题出在数据库连接已经满了,无法获取数据库连接进行查询,这个现象是就是缓存雪崩的效果。

2.3 解决缓存雪崩

2.3.1 分析雪崩场景

用图来说,实际上就是没有了redis这层担着上层流量压力

其实从这张图来看,对于我们一般的应用,客户端去访问应用到数据库的整个链路过程,其实在面临大流量的时候,我们一般是以"倒三角"模型进行流量缓冲,什么是“倒三角”模型

通过"倒三角"模型,按照并发需要优化系统,在面临雪崩这种情形,可以按照“倒三角”模型进行优化,注意雪崩是理论上没办法彻底解决的,可能到最终得提高硬件配置。

2.3.1 雪崩优化方案

经过分析得解决雪崩方案:

1.随机缓存过期时间,能一定程度缓解雪崩
2.使用锁或队列、设置过期标志更新缓存
3.添加本地缓存实现多级缓存
4.添加熔断降级限流,缓冲压力

2.3.1.1 随机缓存时间

随机缓存时间意在避免大量热点key同时失效。

接下来,我们基于Redis+SpringBoot+SpringCache基础项目搭建这个项目继续进行实践。

由于是使用了SpringCache,我们最优的方案就是直接在@Cacheable等注解上面加参数,比如像表达式之类的,让数据放进缓存的时候按照表达式/参数值定义过期时间。

因此我们先查看原有的RedisCache是怎么样的put逻辑

RedisCacheManager创建Cache

protected RedisCache createRedisCache(String name, @Nullable RedisCacheConfiguration cacheConfig) {return new RedisCache(name, this.cacheWriter, cacheConfig != null ? cacheConfig : this.defaultCacheConfig);}

打开RedisCache.class,查看put 方法如下:

 public void put(Object key, @Nullable Object value) {Object cacheValue = this.preProcessCacheValue(value);if (!this.isAllowNullValues() && cacheValue == null) {throw new IllegalArgumentException(String.format("Cache '%s' does not allow 'null' values. Avoid storing null via '@Cacheable(unless=\"#result == null\")' or configure RedisCache to allow 'null' via RedisCacheConfiguration.", this.name));} else {this.cacheWriter.put(this.name, this.createAndConvertCacheKey(key), this.serializeCacheValue(cacheValue), this.cacheConfig.getTtl());}}

这里this.cacheConfig.getTtl() 就是缓存的过期时间,可以看到数据的缓存过期时间是从全局缓存配置里面获取的过期时间配置的,而我需要实现的是让某个cache下每个key随机时间过期,因此我们需要改动这里 this.cacheConfig.getTtl(),我们在createRedisCache的时候改变这个值就行了。

1. 基于java动态执行字符串代码,返回过期时间。

实现基于Spring.expression的ExpressService

/*** @title: ExpressUtil* @projectName redisdemo* @description: 动态执行字符串代码* @author lps* @date 2020/3/912:01*/
@Slf4j
public class ExpressService {private ExpressionParser spelExpressionParser;private ParserContext parserContext;// 表达式解析上下文private StandardEvaluationContext evaluationContext;public static enum ExpressType {/*** ${}表达式格式*/TYPE_FIRST,/*** #{}表达式格式*/TYPE_SECOND}private static final String PRE_TYPE_1 = "${";private static final String PRE_TYPE_2 = "#{";private static final String SUF_STR = "}";private ExpressService(String pre, String suf) {spelExpressionParser = new SpelExpressionParser();log.debug("表达式前缀={},表达式后缀={}", pre, suf);evaluationContext = new StandardEvaluationContext();// 增加map解析方案evaluationContext.addPropertyAccessor(new MapAccessor());parserContext = new TemplateParserContext(pre, suf);}/**** <p>* 创建表达式处理服务对象 默认为创建#{}格式表达式 通过ExpressType指定表达式格式,现有两种${}和#{}* </p>*** @param type*            表达式格式类型* @return 表达式解析对象*/public static ExpressService createExpressService(ExpressType type) {if (type == ExpressType.TYPE_FIRST) {log.debug("生成表达式,表达式前缀={}", PRE_TYPE_1);return new ExpressService(PRE_TYPE_1, SUF_STR);} else {return new ExpressService(PRE_TYPE_2, SUF_STR);}}public Object expressParse(String express, Object data) throws Exception {log.debug("解析表达式信息={}", express);Expression expression = spelExpressionParser.parseExpression(express, this.parserContext);return expression.getValue(evaluationContext, data);}}

测试调用:

    @Testpublic void testExpress(){ExpressService express = ExpressService.createExpressService(null);try {//固定超时时间System.out.println("ttl="+express.expressParse("#{60}", null));//调用方法生成随机过期时间System.out.println("ttl="+express.expressParse("#{T(org.apache.commons.lang3.RandomUtils).nextInt(60,200)}", null));} catch (Exception e) {e.printStackTrace();}}

2. 设计name拼接ttl规则

由于createRedisCache只有两个参数name以及cacheConfig,而只有name是对于单个cache来说的,cacheConfig是对于全局cache来说,因此我们需要设计name参数中指定cache的name以及过期时间的规则。

name赋值规则:name|ttlFun

eg: @Cacheable(cacheName="test|#{T(org.apache.commons.lang3.RandomUtils).nextInt(60,200)}")@Cacheable(cacheName="test|#{60}")

3. 编写解析name代码

  

  /*** 分隔符|*/private static final String SEPERATE_LINE = "|";public MyRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {super(cacheWriter, defaultCacheConfiguration);}protected RedisCache createRedisCache(String name, @Nullable RedisCacheConfiguration cacheConfig) {
//        ``name赋值规则:name|ttlFun ``if(name.contains(SEPERATE_LINE)){String cacheName = name.substring(0,name.indexOf(SEPERATE_LINE));String expression = name.substring(name.indexOf(SEPERATE_LINE)+1);try{ExpressService express = ExpressService.createExpressService(null);long ttl = Long.parseLong(express.expressParse(expression, null).toString());cacheConfig = cacheConfig.entryTtl(Duration.ofSeconds(ttl));return super.createRedisCache(cacheName, cacheConfig);}catch (Exception e){e.printStackTrace();return super.createRedisCache(name, cacheConfig);}}return super.createRedisCache(name, cacheConfig);}

4. 修改CacheConfig

将原本的RedisManager替换成#3编写的MyRedisManager

    /*** 配置缓存管理器*/@Beanpublic CacheManager cacheManager(RedisConnectionFactory factory) {//关键点,spring cache 的注解使用的序列化都从这来,没有这个配置的话使用的jdk自己的序列化,实际上不影响使用,只是打印出来不适合人眼识别RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig()// 将 key 序列化成字符串.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))// 将 value 序列化成 json.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))// 设置缓存过期时间,单位秒.entryTtl(Duration.ofSeconds(cacheExpireTime))// 不缓存空值.disableCachingNullValues();
/*        RedisCacheManager redisCacheManager = RedisCacheManager.builder(factory).cacheDefaults(cacheConfig).build();*///修改RedisCacheManager 为MyRedisCacheManager        MyRedisCacheManager redisCacheManager = new MyRedisCacheManager(RedisCacheWriter.nonLockingRedisCacheWriter(factory), cacheConfig);return redisCacheManager;}

5. 测试

编写单元测试

    @Testpublic void testQueryIdWithExpress(){Assert.assertNotNull(userService.queryById(3333));}

重新定义查询的cache定义

    @Override@Cacheable( value = "ca1|#{60}",key = "#id" ,unless="#result == null")
//    @Cacheable( value = "ca1|#{T(org.apache.commons.lang3.RandomUtils).nextInt(100,200)}",key = "#id" ,unless="#result == null")public User queryById(int id) {return this.userDao.queryById(id);}

当value=ca1|#{60}的时候,通过查看Redis的TTL 剩余为58s

当value=ca1|#{T(org.apache.commons.lang3.RandomUtils).nextInt(100,200)}的时候,随机100-220范围内秒数,通过查看Redis的TTL 剩余为107s

这时候使用random的方式就可以实现随机过期时间了,随机数最好选择符合高斯(正态)分布的会比较好。

new Random().nextGaussian()

2.3.1.2 互斥锁排队

业界比价普遍的一种做法,即根据key获取value值为空时,锁上,从数据库中load数据后再释放锁。若其它线程获取锁失败,则等待一段时间后重试。这里要注意,分布式环境中要使用分布式锁,单机的话用普通的锁(synchronized、Lock)就够了。

这样做思路比较清晰,也从一定程度上减轻数据库压力,但是锁机制使得逻辑的复杂度增加,吞吐量也降低了,有点治标不治本。

1.使用setnx的方式设置互斥锁

  

 public User queryById(int id) {try {if (redisTemplate.hasKey(id+"")) {return (User) redisTemplate.opsForValue().get(id+"");} else {//获取锁if(lock(id+"")){// 数据库查询User user = userDao.queryById(id);redisTemplate.opsForValue().set(id+"",user, Duration.ofSeconds(3000));//释放锁redisTemplate.delete(LOCK_PREFIX + "id");}}} catch (Exception e) {e.printStackTrace();}return (User) redisTemplate.opsForValue().get(""+id);}private static String LOCK_PREFIX = "prefix";private static long LOCK_EXPIRE =3000;/*** 互斥锁实现*/public boolean lock(String key) {String lock = LOCK_PREFIX + key;return (Boolean) redisTemplate.execute((RedisCallback) connection -> {long expireAt = System.currentTimeMillis() + LOCK_EXPIRE + 1;//SETNXBoolean acquire = connection.setNX(lock.getBytes(), String.valueOf(expireAt).getBytes());if (acquire) {return true;} else {byte[] value = connection.get(lock.getBytes());if (Objects.nonNull(value) && value.length > 0) {long expireTime = Long.parseLong(new String(value));   //判断锁是否过期                                                                          if (expireTime < System.currentTimeMillis()) {byte[] oldValue = connection.getSet(lock.getBytes(), String.valueOf(System.currentTimeMillis() + LOCK_EXPIRE + 1).getBytes());return Long.parseLong(new String(oldValue)) < System.currentTimeMillis();}}}return false;});}

2.3.1.3 设置过期标志更新缓存

定时更新缓存,阻塞部分请求,达到缓冲作用,也可以设置key永不过期

2.3.1.4 多级缓存

这个方案主要在redis宕机,或者key在更新进缓存的中间,可以响应业务应用,减轻压力

2.3.1.5 熔断降级限流

这个方案是直接在业务应用之上进行请求流量控制,减轻下层压力

原文链接:https://blog.csdn.net/qq_28540443/article/details/104746655

同时,欢迎所有开发者扫描下方二维码填写《开发者与AI大调研》,只需2分钟,便可收获价值299元的「AI开发者万人大会」在线直播门票!

推荐阅读:小网站的容器化(下):网站容器化的各种姿势,先跟着撸一波代码再说!
你知道吗?其实 Oracle 直方图自动统计算法存在这些缺陷!(附验证步骤)
详解以太坊虚拟机(EVM)的数据存储机制
比特币当赎金,WannaRen 勒索病毒二度来袭!平台抗住日访问量 7 亿次,研发品控流程全公开“手把手撕LeetCode题目,扒各种算法套路的裤子”北京四环堵车引发的智能交通大构想从Ngin到Pandownload,程序员如何避免面向监狱编程?
真香,朕在看了!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/518346.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

阿里巴巴王坚:用数据来改变世界

“传统信息化建设都是从无到有&#xff0c;加了杆子和机器&#xff0c;但是新一代数字建设就是从有到无&#xff0c;缴费的机器没有了&#xff0c;你回家缴&#xff0c;杆子没有了&#xff0c;你回家缴。” 7月21日&#xff0c;阿里巴巴技术委员会主席王坚在2019年中国电子政务…

Knative Service 之流量灰度和版本管理

本篇主要介绍 Knative Serving 的流量灰度&#xff0c;通过一个 rest-api 的例子演示如何创建不同的 Revision、如何在不同的 Revision 之间按照流量比例灰度。 部署 rest-api v1 代码 测试之前我们需要写一段 rest-api 的代码&#xff0c;并且还要能够区分不同的版本。下面…

在生产环境中使用 Sentinel

文章目录一、安装zookeeper1. linux环境2. windows环境2. 安装并启动zkui二、编译打包2.1. 拉取项目2.2. 启动2.3. 登录 sentinel2.4. 登录zkui2.5. 重启Sentinel2.6. 删除Sentinel的流控规则三、将客户端和zk打通3.1. 引入依赖3.2. 配置3.3. 启动springboot3.4. sentinel控制台…

JavaScript-浏览器控制台使用

基本语法 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>Title</title><!-- JavaScript 严格区分大小写 --><script>// 1. 定义变量 变量类型 变量名 变量值var num 1;num …

看完就入门系列!吞吐量、消息持久化、负载均衡和持久化、伸缩性…… 你真的了解 Kafka 了吗?...

作者| liuhehe123来源| CSDN博客 责编| Carol出品| CSDN云计算&#xff08;ID&#xff1a;CSDNcloud&#xff09;封图| CSDN下载于视觉中国 无论是已经接触过 Kafka 还是刚入坑的小伙伴&#xff0c;都应该时不时回头了解一下 Kafka &#xff0c;有时候会有不少新收获。今天这份…

Alibaba Cloud Linux 2 开源后又有什么新动作?

阿里妹导读&#xff1a;2019 年 4 月&#xff0c;Alibaba Cloud Linux 2 (Aliyun Linux 2) 正式开源。时至今日&#xff0c;已经走过三个月的里程。在这段时间内&#xff0c;这个刚诞生不久的为阿里云 ECS 环境定制优化的 Linux 操作系统发行版的装机量稳步上升。本文将重点介绍…

一站式数据采集存储的利器:阿里云InfluxDB®️数据采集服务

背景 随着时序数据的飞速增长&#xff0c;时序数据库不仅需要解决系统的稳定性和性能问题&#xff0c;还需实现数据从采集到分析的链路打通&#xff0c;才能让时序数据真正产生价值。在时序数据采集领域&#xff0c;一直缺少自动化的采集工具。虽然用户可以使用一些开源的采集…

Serverless 风起云涌,为什么阿里、微软、AWS 纷纷拥抱开源 OAM?

作者 | 张磊&#xff0c;阿里云原生应用平台高级技术专家邓洪超&#xff0c;阿里云技术专家责编 | 唐小引头图 | CSDN 下载自东方 IC出品 | CSDN&#xff08;ID&#xff1a;CSDNnews&#xff09;Serverless 这个词第一次被是 2012 年一篇名为《Why The Future of Software and …

K8S从懵圈到熟练 - 我们为什么会删除不了集群的命名空间?

阿里云售后技术团队的同学&#xff0c;每天都在处理各式各样千奇百怪的线上问题。常见的有&#xff0c;网络连接失败&#xff0c;服务器宕机&#xff0c;性能不达标&#xff0c;请求响应慢等。但如果要评选&#xff0c;什么问题看起来微不足道事实上却足以让人绞尽脑汁&#xf…

为什么技术人一定要懂点“可信计算”?

阿里妹导读&#xff1a;可信计算&#xff08;TrustedComputing&#xff0c;简称TC&#xff09;是一项由TCG(可信计算组)推动和开发的技术。可信的核心目标之一是保证系统和应用的完整性&#xff0c;从而确定系统或软件运行在设计目标期望的可信状态。可信和安全是相辅相成的&am…

很用心的为你写了 9 道 MySQL 面试题,建议收藏!

来源 | Java 建设者责编| Carol封图| CSDN下载于视觉中国 MySQL 也是作者本人正在学习的部分&#xff0c;后面会多输出 MySQL 的文章贡献给大家&#xff0c;毕竟 MySQL 涉及到数据存储、锁、磁盘寻道、分页等操作系统概念&#xff0c;而且互联网对 MySQL 的注重程度是不言而喻的…

OpenTelemetry-可观察性的新时代

有幸在2019KubeCon上海站听到Steve Flanders关于OpenTelemetry的演讲&#xff0c;之前Ops领域两个网红项目OpenTracing和OpenCensus终于走到了一起&#xff0c;可观察性统一的标准化已经扬帆起航。 这篇文章旨在抛砖引玉&#xff0c;希望能够和更多的同学一起交流可观察性相关的…

JavaScript-严格检查模式

<!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>Title</title><!--前提&#xff1a; IDEA需要设置支持ES6 语法use strict; 必须写在JavaScript 的第一行严格检查模式&#xff0c;预防JavaS…

Feign常见问题总结

https://www.imooc.com/article/289005

你知道 Java 类是如何被加载的吗?

一&#xff1a;前言 最近给一个非Java方向的朋友讲了下双亲委派模型&#xff0c;朋友让我写篇文章深度研究下JVM的ClassLoader&#xff0c;我确实也好久没写JVM相关的文章了&#xff0c;有点手痒痒&#xff0c;涂了皮炎平也抑制不住。 我在向朋友解释的时候是这么说的&#x…

我们为什么需要 SpringBoot?

作者 | 阿文&#xff0c;责编 | 郭芮头图 | CSDN 下载自东方IC出品 | CSDN&#xff08;ID&#xff1a;CSDNnews&#xff09;任何先进技术的产生都不是凭空出现的&#xff0c;SpringBoot 也不例外&#xff0c;SpringBoot 是基于Spring 的基础上产生的。总所周知&#xff0c;Spri…

高德网络定位之“移动WiFi识别”

导读 随着时代的发展&#xff0c;近10年来位置产业蓬勃发展&#xff0c;定位能力逐渐从低精度走向高精度&#xff0c;从部分场景走向泛在定位。设备和场景的丰富&#xff0c;使得定位技术和能力也不断的优化更新。定位能力包括GNSS、DR&#xff08;航迹推算&#xff09;、MM&a…

JavaScript-字符串

字符串 正常字符串使用单引号 或者 双引号 包裹注意转义字符 \ \ \n \t \u4e2d \u#### Unicode字符 \x41 Ascll字符多行字符串编写 // tab 上面 esc下面的引号 let msg helloworld你好 console.log(msg)模板字符串 let name ht let age 5; let msg hello${name}你…

Alibaba Sentinel规则持久化-拉模式-手把手教程【基于文件】

文章目录一、拉模式架构二、原理简述三、编写3.1 加依赖3.2 写代码3.3 配置四、优缺点分析五、你可能会有的疑问六、参考文档七、案例测试7.1. 添加流控规则7.2. 服务停止7.3. 重新启动服务7.4. 调用接口7.5. 查看流控规则本文实现基于拉模式的Alibaba Sentinel规则持久化。一、…