高性能分布式缓存Redis-分布式锁与布隆过滤器

一、分布式锁

我们先来看一下本地锁

在并发编程中,我们通过锁,来避免由于竞争而造成的数据不一致问题。通常,我们以 synchronized 、Lock 来使用它(单机情况)
来看这段代码
@Autowired
RedisTemplate<String,String> redisTemplate;String maotai = "maotai20210321001";//茅台商品编号 @PostConstruct
public void init(){ //此处模拟向缓存中存入商品库存操作 redisTemplate.opsForValue().set(maotai,"100"); 
}@GetMapping("/get/maotai2")
public String seckillMaotai2() {synchronized (this) {Integer count = Integer.parseInt(redisTemplate.opsForValue().get(maotai)); // 1 //如果还有库存 if (count > 0) { //抢到了茅台,库存减一 redisTemplate.opsForValue().set(maotai,String.valueOf(count-1));//后续操作 do somethinglog.info("我抢到茅台了!");return "ok"; }else { return "no"; }}
}
  • 现象:本地锁在多节点下失效(集群/分布式)
  • 原因:本地锁它只能锁住本地JVM进程中的多个线程,对于多个JVM进程的不同线程间是锁不住的
  • 解决:分布式锁(在分布式环境下提供锁服务,并且达到本地锁的效果)

那么,到底什么是分布式锁呢?

  • 当在分布式架构下,数据只有一份(或有限制),此时需要利用锁的技术控制某一时刻修改数据的进程数。
  • 用一个状态值表示锁,对锁的占用和释放通过状态值来标识。

基于Redis实现分布式锁

锁的实现主要基于 redis SETNX 命令:

大致流程:

  • 1. 使用 SETNX 命令获取锁,若返回0key已存在,锁已存在)则获取失败,反之获取成功
  • 2. 执行业务逻辑
  • 3. 释放锁,使用 DEL 命令将锁数据删除

锁超时

仅仅使用这个流程,会出现什么问题吗?思考:如果程序在第2步执行出现异常退出或宕机,没有执行第3步释放锁,岂不就使得这个锁一直未释放,别的请求一直得不到这个锁,这就产生了死锁问题!这也就是分布式锁的锁超时特点。

如何解决?在抢到锁的时候,给这个锁加上一个过期时间,即使没有执行手动释放锁,也会在过期时间到了自动释放锁!

代码如下:

    @GetMapping("/get/maotai3")public String seckillMaotai3() {//获取锁Boolean islock = redisTemplate.opsForValue().setIfAbsent(lockey, "1");if (islock) {//设置锁的过期时间redisTemplate.expire(lockey,5, TimeUnit.SECONDS);try {Integer count = Integer.parseInt(redisTemplate.opsForValue().get(maotai)); // 1//如果还有库存if (count > 0) {//抢到了茅台,库存减一redisTemplate.opsForValue().set(maotai,String.valueOf(count-1));//后续操作 do somethinglog.info("我抢到茅台了!");return "ok";}else {return "no";}} catch (Exception e) {e.printStackTrace();} finally {//释放锁redisTemplate.delete(lockey);}}return "dont get lock";}

原子性操作

在以上代码中,setnx和expire的操作是分开执行的,假设此时,setnx执行完毕,程序异常退出或宕机了,那就无法设置好过期时间,这样也会导致锁一直得不到释放

究其原因,这两步是非原子性操作(解决:2.6以前可用使用lua脚本,2.6以后可用set命令),使用lua脚本把这两个命令变成一气呵成的操作,确保都同时执行成功。

@GetMapping("/get/maotai4")public String seckillMaotai4() {//获取锁String locklua ="" +"if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then redis.call('expire',KEYS[1],ARGV[2]) ; return true " +"else return false " +"end";Boolean islock = redisTemplate.execute(new RedisCallback<Boolean>() {@Overridepublic Boolean doInRedis(RedisConnection redisConnection) throws DataAccessException {Boolean eval = redisConnection.eval(locklua.getBytes(),ReturnType.BOOLEAN,1,lockey.getBytes(),requestid.getBytes(),"5".getBytes());return eval;}});if (islock) {try {Integer count = Integer.parseInt(redisTemplate.opsForValue().get(maotai)); // 1//如果还有库存if (count > 0) {//抢到了茅台,库存减一redisTemplate.opsForValue().set(maotai,String.valueOf(count-1));//后续操作 do somethinglog.info("我抢到茅台了!");return "ok";}else {return "no";}} catch (Exception e) {e.printStackTrace();} finally {//释放锁redisTemplate.delete(lockey);}}return "dont get lock";}

错误解锁

思考以上代码:假设设置的超时时间小于业务执行时间,导致业务代码没有执行完,锁就被释放了,这时候其他请求就能进到这个锁,操作一些共享变量导致线程安全问题

解决方案:在setnx时,将value设置成一个唯一标识,解锁的时候,要进行value的匹配,匹配上了才能解锁

@GetMapping("/get/maotai4")public String seckillMaotai4() {String requestid = UUID.randomUUID().toString() + Thread.currentThread().getId();/*String locklua ="" +"if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then redis.call('expire',KEYS[1],ARGV[2]) ; return true " +"else return false " +"end";Boolean islock = redisTemplate.execute(new RedisCallback<Boolean>() {@Overridepublic Boolean doInRedis(RedisConnection redisConnection) throws DataAccessException {Boolean eval = redisConnection.eval(locklua.getBytes(),ReturnType.BOOLEAN,1,lockey.getBytes(),requestid.getBytes(),"5".getBytes());return eval;}});*///获取锁Boolean islock = redisTemplate.opsForValue().setIfAbsent(lockey,requestid,5,TimeUnit.SECONDS);if (islock) {try {Integer count = Integer.parseInt(redisTemplate.opsForValue().get(maotai)); // 1//如果还有库存if (count > 0) {//抢到了茅台,库存减一redisTemplate.opsForValue().set(maotai,String.valueOf(count-1));//后续操作 do somethinglog.info("我抢到茅台了!");return "ok";}else {return "no";}} catch (Exception e) {e.printStackTrace();} finally {//释放锁//判断是自己的锁才能去释放 这种操作不是原子性的/*String id = redisTemplate.opsForValue().get(lockey);if (id !=null && id.equals(requestid)) {redisTemplate.delete(lockey);}*/String unlocklua = "" +"if redis.call('get',KEYS[1]) == ARGV[1] then redis.call('del',KEYS[1]) ; return true " +"else return false " +"end";redisTemplate.execute(new RedisCallback<Boolean>() {@Overridepublic Boolean doInRedis(RedisConnection redisConnection) throws DataAccessException {Boolean eval = redisConnection.eval(unlocklua.getBytes(),ReturnType.BOOLEAN,1,lockey.getBytes(),requestid.getBytes());return eval;}});}}return "dont get lock";}

锁续期

思考上面代码:在下图里面,只是解决了业务代码执行完,释放锁只能释放自己的锁,但是,当锁过期了,其他请求不也一样的进到了锁里面操作共享变量吗?究其原因是锁的过期时间小于业务代码执行时间,那解决办法自然就是把这个时间延长呗

那这个时间延长到底如何做?

给拿到锁的线程创建一个守护线程(看门狗),守护线程 定时/延迟(如每隔5s检查业务是否执行完毕) 判断拿到锁的线程是否还继续持有锁,如果持有则为其续期

    //模拟一下守护线程为其续期ScheduledExecutorService executorService;//创建守护线程池ConcurrentSkipListSet<String> set = new ConcurrentSkipListSet<String>();//队列@PostConstructpublic void init2(){executorService = Executors.newScheduledThreadPool(1);//编写续期的luaString expirrenew = "" +"if redis.call('get',KEYS[1]) == ARGV[1] then redis.call('expire',KEYS[1],ARGV[2]) ; return true " +"else return false " +"end";executorService.scheduleAtFixedRate(new Runnable() {@Overridepublic void run() {Iterator<String> iterator = set.iterator();while (iterator.hasNext()) {String rquestid = iterator.next();redisTemplate.execute(new RedisCallback<Boolean>() {@Overridepublic Boolean doInRedis(RedisConnection redisConnection) throws DataAccessException {Boolean eval = false;try {eval = redisConnection.eval(expirrenew.getBytes(),ReturnType.BOOLEAN,1,lockey.getBytes(),rquestid.getBytes(),"5".getBytes());} catch (Exception e) {log.error("锁续期失败,{}",e.getMessage());}return eval;}});}}},0,1,TimeUnit.SECONDS);}@GetMapping("/get/maotai5")public String seckillMaotai5() {String requestid = UUID.randomUUID().toString() + Thread.currentThread().getId();//获取锁Boolean islock = redisTemplate.opsForValue().setIfAbsent(lockey,requestid,5,TimeUnit.SECONDS);if (islock) {//获取锁成功后让守护线程为其续期set.add(requestid);try {Integer count = Integer.parseInt(redisTemplate.opsForValue().get(maotai)); // 1//如果还有库存if (count > 0) {//抢到了茅台,库存减一redisTemplate.opsForValue().set(maotai,String.valueOf(count-1));//后续操作 do something//seckillMaotai5();//模拟业务超时TimeUnit.SECONDS.sleep(10);log.info("我抢到茅台了!");return "ok";}else {return "no";}} catch (Exception e) {e.printStackTrace();} finally {//解除锁续期set.remove(requestid);//释放锁String unlocklua = "" +"if redis.call('get',KEYS[1]) == ARGV[1] then redis.call('del',KEYS[1]) ; return true " +"else return false " +"end";redisTemplate.execute(new RedisCallback<Boolean>() {@Overridepublic Boolean doInRedis(RedisConnection redisConnection) throws DataAccessException {Boolean eval = redisConnection.eval(unlocklua.getBytes(),ReturnType.BOOLEAN,1,lockey.getBytes(),requestid.getBytes());return eval;}});}}return "dont get lock";}

锁的可重入

思考一个问题,如果在一段代码里,我拿到了锁,这个代码要递归调用自己,就面临着再次获取锁,所以确保这次获取锁是成功的,这称为锁的可重入

可重入如何做呢?我们可不可以使得value是一个数值,在锁的内部每加一次锁就让数值加1,每释放一次锁就让数值减1。我们用hash的类型来做这个。基于这个思路我们看一下实现流程:

  1. 加锁:当一个客户端尝试获取锁时,首先检查锁是否已被其他客户端持有。如果没有,则设置锁并成为持有者;如果有,但持有者是自己,则增加计数器。
  2. 解锁:当一个客户端释放锁时,减少计数器。如果计数器变为0,则删除锁,这样其他客户端才能获取锁。

 加锁的lua脚本:

                        "if (redis.call('exists', KEYS[1]) == 0) then " +#设置锁key,field是唯一标识,value是重入次数"redis.call('hset', KEYS[1], ARGV[2], 1); " +#设置锁key的过期时间 默认30s"redis.call('pexpire', KEYS[1], ARGV[1]); " +"return nil; " +"end; " +#如果锁key存在"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +#重入次数+1"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +#重置过期时间"redis.call('pexpire', KEYS[1], ARGV[1]); " +"return nil; " +"end; " +"return redis.call('pttl', KEYS[1]);"

解锁的lua脚本:

                      "if (redis.call('exists', KEYS[1]) == 0) then " +"redis.call('publish', KEYS[2], ARGV[1]); " +"return 1; " +"end;" +// hash 中的field 不存在时直接返回,"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +"return nil;" +"end; " +"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +//重入次数-1后如果还大于0,延长过期时间"if (counter > 0) then " +"redis.call('pexpire', KEYS[1], ARGV[2]); " +"return 0; " +"else " +//重入次数-1后如果归0,则删除key,并向redisson_lock__channel:{key}频道发布锁释放消息"redis.call('del', KEYS[1]); " +"redis.call('publish', KEYS[2], ARGV[1]); " +"return 1; "+"end; " +"return nil;"

阻塞锁

我们要使得,每个请求来获取锁,如果锁已被占用,那么获取不到就等待锁的释放,直到获取到锁或者等待超时,常见方案有两种:

  • 1:基于客户端轮询的方案
  • 2:基于redis的发布/订阅方案

第2种方案:对于每个抢不到锁的进程,就订阅一个频道,当释放锁时,会向这个频道发布通知, 收到通知再进行重新抢锁

以上这些特性都是分布式锁应该满足的,那么自己写起来还是不太方便,Redisson就帮我们封装好了这一切,直接用

基于Redisson 实现分布式锁

Redisson的介绍

        Redisson内置了一系列的分布式对象,分布式集合,分布式锁,分布式服务等诸多功能特性,是一款基于Redis实现,拥有一系列分布式系统功能特性的工具包,是实现分布式系统架构中缓存中间件的最佳选择。

使用步骤

<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.8.2</version>
</dependency>
    @Beanpublic RedissonClient redissonClient() {Config config = new Config();config.useSingleServer().setAddress("redis://"+host+":"+port);return Redisson.create(config);}@AutowiredRedissonClient redissonClient;@GetMapping("/get/maotai6")public String seckillMaotai6() {//要去获取锁RLock lock = redissonClient.getLock(lockey);lock.lock();try {Integer count = Integer.parseInt(redisTemplate.opsForValue().get(maotai)); // 1//如果还有库存if (count > 0) {//抢到了茅台,库存减一redisTemplate.opsForValue().set(maotai,String.valueOf(count-1));//后续操作 do somethinglog.info("我抢到茅台了!");return "ok";}else {return "no";}} catch (Exception e) {e.printStackTrace();} finally {lock.unlock();;}return "";}

分布式锁特点

总结来说,设计一个完善的分布式锁,需要满足下面这些特点

  • 互斥性:不仅要在同一jvm进程下的不同线程间互斥,更要在不同jvm进程下的不同线程间互斥
  • 锁超时:支持锁的自动释放,防止死锁
  • 正确,高效,高可用(解决错误解锁问题):解铃还须系铃人(加锁和解锁必须是同一个线程),加锁和解锁操作一定要高效,提供锁的服务要具备容错性
  • 可重入:如果一个线程拿到了锁之后继续去获取锁还能获取到,我们称锁是可重入的(方法的递归调用)
  • 阻塞/非阻塞:如果获取不到直接返回视为非阻塞的,如果获取不到会等待锁的释放直到获取锁或者等待超时,视为阻塞的
  • 公平/非公平:按照请求的顺序获取锁视为公平的

前三点使我们必须要满足的,后两点是分布式锁的类型 

二、布隆过滤器

我们先从一个场景说起

如果数据在缓存和数据库中都不存在,以id=-1为例,如果频繁的请求这个id的数据,会被频繁的访问数据库判断是否存在,直至请求多到把数据库压垮,这就是缓存穿透问题

那如何解决呢?我们就可以用布隆过滤器,布隆过滤器就可以实现在海量元素中,快速判断一个元素是否存在

布隆过滤器本质上其实就是一个很长的二进制向量和一系列随机映射函数。专门用来检测集合中是否存在特定的元素

布隆过滤器的设计

BF是由一个长度为m比特的位数组(bit array)k个哈希函数(hash function)组成的数据结构。位数组均初始化为0,所有哈希函数都可以分别把输入数据尽量均匀地散列。

这个二进制位数据就由bitmap来实现

如果我们要映射一个值到布隆过滤器中,我们需要使用多个不同的哈希函数生成多个哈希值,并对每个生成的哈希值指向的 bit 位,设置为1

 

 当要插入一个元素时,将其数据分别输入k个哈希函数,产生k个哈希值。以哈希值作为位数组中的下标,将所有k个对应的比特置为1。

当要查询(即判断是否存在)一个元素时,同样将其数据输入哈希函数,然后检查对应的k个比特。如果有任意一个比特为0,表明该元素一定不在集合中。如果所有比特均为1,表明该集合有(较大的)可能性在集合中。为什么不是一定在集合中呢?因为一个比特被置为1有可能会受到其他元素的影响,这就是所谓“假阳性”(false positive)。相对地,“假阴性”(false negative)在BF中是绝不会出现的。

总结,对于BF的查询结果:

  • 如果这些点有任何一个 0,则被检索元素一定不在
  • 如果都是 1,则被检索元素很可能在。

 布隆过滤器的误判是指多个输入经过哈希之后在相同的bit位置1了,这样就无法判断究竟是哪个输入产生的,因此误判的根源在于相同的 bit 位被多次映射且置 1。

 误报率指的是你愿意接受的误报概率。误报是指布隆过滤器告诉你一个元素“可能”存在于集合中,但实际上并不在集合中。误报率越低,布隆过滤器所需的内存就越多。

在redis中使用BF

<dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>3.13.4</version>
</dependency>
public class RedissonBloomFilter {public static void main(String[] args) {Config config = new Config();config.useSingleServer().setAddress("redis://127.0.0.1:6379");config.useSingleServer().setPassword("1234");//构造RedissonRedissonClient redisson = Redisson.create(config);RBloomFilter<String> bloomFilter = redisson.getBloomFilter("phoneList");//初始化布隆过滤器:预计元素为100000000L,偏差率为3%bloomFilter.tryInit(100000000L,0.03);//将号码10086插入到布隆过滤器中bloomFilter.add("10086");//判断下面号码是否在布隆过滤器中//输出falseSystem.out.println(bloomFilter.contains("123456"));//输出trueSystem.out.println(bloomFilter.contains("10086"));}
}

解释一下bloomFilter.tryInit(100000000L,0.03);

  • 100000000L 表示你期望这个布隆过滤器能够容纳大约一亿(100,000,000)个元素。
  • 0.03 表示你希望这个布隆过滤器误判率不超过 3%。

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

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

相关文章

Flutter运行App时出现“Running Gradle task ‘assembleDebug“问题解决

在参考了众多解决办法中最有用并且最快的方法 Gradle Distributions 在这个地方下载对应你这个文件中的gradle版本 然后将 最后一行本来不是这样的,我们把下载好的zip包保存到本地,然后用这个代替网址,最后成功运行

【CUDA】认识CUDA

目录 一、CUDA编程 二、第一个CUDA程序 三、CUDA关键字 四、device管理 4.1 初始化 4.2 Runtime API查询GPU信息 4.3 决定最佳GPU CUDA C 编程指南CUDA C在线文档&#xff1a;CUDA C 编程指南 CUDA是并行计算的平台和类C编程模型&#xff0c;能很容易的实现并行算法。只…

【优选算法篇】微位至简,数之恢宏——解构 C++ 位运算中的理与美

文章目录 C 位运算详解&#xff1a;基础题解与思维分析前言第一章&#xff1a;位运算基础应用1.1 判断字符是否唯一&#xff08;easy&#xff09;解法&#xff08;位图的思想&#xff09;C 代码实现易错点提示时间复杂度和空间复杂度 1.2 丢失的数字&#xff08;easy&#xff0…

从0开始学习机器学习--Day21--算法的评估标准

准确率和召回率(precision and recall) 在上一章我们提到了在每次运行算法时通过返回一个实数值来判断算法的好坏&#xff0c;但是我们该如何构建这个实数的计算公式呢&#xff0c;毕竟这关乎于我们对算法的判断&#xff0c;不能过于夸大或贬低。有一个典型的会被影响的很大例…

自然语言处理在客户服务中的应用

&#x1f493; 博客主页&#xff1a;瑕疵的CSDN主页 &#x1f4dd; Gitee主页&#xff1a;瑕疵的gitee主页 ⏩ 文章专栏&#xff1a;《热点资讯》 自然语言处理在客户服务中的应用 自然语言处理在客户服务中的应用 自然语言处理在客户服务中的应用 引言 自然语言处理概述 定义…

【Ubuntu24.04】从双系统到虚拟机再到单系统的故事

故事 在大学前期&#xff0c;我使用Ubuntu系统都是为了学习一些命令或者其它Linux的东西&#xff0c;对性能的要求不高&#xff0c;所以选择了虚拟机&#xff0c;后来为了做毕设&#xff0c;选择安装了Ubuntu20.04双系统&#xff0c;因为虚拟机实在带不动&#xff0c;那时我的主…

初次体验Tauri和Sycamore(1)

原创作者&#xff1a;庄晓立&#xff08;LIIGO&#xff09; 原创时间&#xff1a;2024年11月10日 原创链接&#xff1a;https://blog.csdn.net/liigo/article/details/143666827 版权所有&#xff0c;转载请注明出处。 前言 Tauri 2.0发布于2024年10月2日&#xff0c;Sycamore…

【统计子矩阵——部分前缀和+双指针】

题目 代码 #include <bits/stdc.h> using namespace std; typedef long long ll; const int N 510; int s[N][N]; int main() {ios::sync_with_stdio(0);cin.tie(0);int n, m, k;cin >> n >> m >> k;for(int i 1; i < n; i)for(int j 1; j <…

「QT」QT5程序设计专栏目录

✨博客主页何曾参静谧的博客&#x1f4cc;文章专栏「QT」QT5程序设计&#x1f4da;全部专栏「VS」Visual Studio「C/C」C/C程序设计「UG/NX」BlockUI集合「Win」Windows程序设计「DSA」数据结构与算法「UG/NX」NX二次开发「QT」QT5程序设计「File」数据文件格式「PK」Parasolid…

Qt学习笔记第41到50讲

第41讲 UI美化遗留问题解决 如上图所示目前记事本的雏形已现&#xff0c;但是还是有待优化&#xff0c;比如右下角的拖动问题。 解决方法&#xff1a; ①首先修改了Widget类的构造函数。 Widget::Widget(QWidget *parent) : QWidget(parent) , ui(new Ui::Widget) {ui->s…

深度学习经典模型之ZFNet

1 ZFNet 1.1 模型介绍 ​ ZFNet是由 M a t t h e w Matthew Matthew D . Z e i l e r D. Zeiler D.Zeiler和 R o b Rob Rob F e r g u s Fergus Fergus在AlexNet基础上提出的大型卷积网络&#xff0c;在2013年ILSVRC图像分类竞赛中以11.19%的错误率获得冠军&#xff08;实际…

移动应用开发:简易登录页

文章目录 简介一&#xff0c;创建新活动二&#xff0c;设计UI布局三&#xff0c;编写活动代码四&#xff0c;运行应用程序注意 简介 使用Android Studio编写的简单Android 登录应用程序&#xff0c;该应用程序包含一个登录界面&#xff0c;具有账号和密码两个文本框&#xff0…

网络基础:http协议和内外网划分

声明 学习视频来自B站UP主泷羽sec,如涉及侵权马上删除文章 笔记的只是方便各位师傅学习知识,以下网站只涉及学习内容,其他的都与本人无关,切莫逾越法律红线,否则后果自负 泷羽sec的个人空间-泷羽sec个人主页-哔哩哔哩视频https://space.bilibili.com/350329294 一&#xff0c;H…

英飞凌Aurix2G TC3XX GPT12模块详解

英飞凌Aurix2G TC3XX GPT12模块详解 本文主要介绍英飞凌 Aurix2G TC3XX系列芯片GPT12模块硬件原理、MCAL相关配置和部分代码实现。 文章目录 英飞凌Aurix2G TC3XX GPT12模块详解1 模块介绍2 功能介绍2.1 结构2.2 独立运行模式2.2.1 定时器模式2.2.2 门控定时器模式2.2.3 计数…

大数据程序猿不可不看的资料大全

​ 随着大数据技术的发展&#xff0c;大数据程序猿在数据采集、处理、分析、存储等方面的技能需求不断增加。要在这个领域保持竞争力&#xff0c;系统性地学习和掌握大数据工具、技术架构和行业趋势是非常重要的。以下为您提供一份围绕大数据程序猿不可不看的资料大全&#xf…

抓包工具WireShark使用记录

目录 网卡选择&#xff1a; 抓包流程&#xff1a; 捕获过滤器 常用捕获过滤器&#xff1a; 抓包数据的显示 显示过滤器&#xff1a; 常用的显示过滤器&#xff1a; 实际工作中&#xff0c;在平台对接&#xff0c;设备对接等常常需要调试接口&#xff0c;PostMan虽然可以进…

MySQL数据迁移到SQLServer数据库

随着云计算技术的发展以及大数据时代的到来&#xff0c;越来越多的企业开始寻求更加高效、安全的数据管理解决方案。MySQL作为一种开源的关系型数据库管理系统&#xff0c;在互联网应用开发中占据了极其重要的位置&#xff1b;而另一方面&#xff0c;Microsoft SQL Server凭借其…

【STM32开发】-FreeRTOS开发入手学习

一、什么是FreeRTOS&#xff1f; FreeRTOS 是 RTOS 系统的一种&#xff0c;FreeRTOS 十分的小巧&#xff0c;可以在资源有限的微控制器中运行&#xff1b; 1、 FreeRTOS是免费的。 2、许多其他半导体厂商产品的 SDK 包就使用 FreeRTOS 作为其操作系统&#xff0c;尤其是 WIFI、…

【软考】系统分析师第二版 新增章节 第20章微服务系统分析与设计

微服务系统是一类基于微服务架构风格的分布式系统&#xff0c;它将应用程序拆分成多个独立的小型服务&#xff0c;每个服务都运行在独立的进程中&#xff0c;并采用轻量级通信协议进行通信。这些服务可以由不同的团队开发、不同的编程语言编写&#xff0c;并且可以按需部署。微…

【笔记】自动驾驶预测与决策规划_Part6_不确定性感知的决策过程

文章目录 0. 前言1. 部分观测的马尔可夫决策过程1.1 POMDP的思想以及与MDP的联系1.1.1 MDP的过程回顾1.1.2 POMDP定义1.1.3 与MDP的联系及区别POMDP 视角MDP 视角决策次数对最优解的影响 1.2 POMDP的3种常规解法1.2.1 连续状态的“Belief MDP”方法1. 信念状态的定义2. Belief …