当前位置: 首页 > news >正文

Redis:原理速成+项目实战——Redis实战5(互斥锁、逻辑过期解决缓存击穿问题)

👨‍🎓作者简介:一位大四、研0学生,正在努力准备大四暑假的实习
🌌上期文章:Redis:原理速成+项目实战——Redis实战4(解决Redis缓存穿透、雪崩、击穿)
📚订阅专栏:Redis速成
希望文章对你们有所帮助

上一篇文章讲解了缓存击穿问题,以及解决缓存击穿问题的2种解决思路,即互斥锁与逻辑过期,这里将分别用这两种方式解决缓存击穿问题。

互斥锁、逻辑过期解决缓存击穿问题

  • 互斥锁解决缓存击穿
    • 获取锁与释放锁方法封装
    • 业务逻辑修改
    • 测试
  • 逻辑过期解决缓存击穿
    • 代码实现
    • 测试

互斥锁解决缓存击穿

根据上次讲解的互斥锁解决缓存击穿问题的方式,我们可以将客户端查询数据的流程修改为如下:
在这里插入图片描述
这里有比较关键的点,这里的锁并不是我们平常用的锁,平常的锁如果没有获取到的话就一直等待,而这边我们需要自定义逻辑。
我们该如何自定义获取锁跟释放锁的逻辑?我们可以利用Redis的一些数据结构的特性。
我们可以联想到之前学习Redis数据结构的时候,用到的setnx,当且仅当key不存在的时候,setnx才会执行set操作:
在这里插入图片描述
可以看到,当我们的key已经是存在的情况下,我们用setnx指令无法对其value进行修改。所以如果有无数线程进行setnx操作的时候,只有第一个进行操作的线程可以写入value,其他线程都会失败。
所以,这就满足了我们所说的互斥锁的实现方式,其实这也是分布式锁的一个基本原理(当然真正的分布式锁还是比较复杂的)。
而释放锁只需要执行del操作即可,这样其他线程就可以获取到这个锁了:
在这里插入图片描述
当然了,最好别把锁永远加在那,因为如果加了锁的线程,他的工作没办法完成,他就永远没办法释放锁,因此我们在执行setnx操作的时候需要设置一下有效期。

获取锁与释放锁方法封装

进行业务逻辑编写之前,我们可以先把获取锁与释放锁的方法给自定义一下:

private boolean tryLock(String key){//尝试获取锁//opsForValue里面没有真正的setnx,而是setIfAbsent,表示如果不存在就执行set//值就随便设定一下,重点是要获取到锁,但是设定了TTL为10sBoolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);/*** 如果是直接返回flag,可能会有拆箱操作,造成空指针,需要用BooleanUtil工具类* 因为Boolean不是基本类型的boolean,是boolean的封装类*/return BooleanUtil.isTrue(flag);
}private void unlock(String key){//释放锁stringRedisTemplate.delete(key);
}

业务逻辑修改

我们可以先把之前会出现缓存穿透的代码给封装起来,封装成返回值为Shop类型的,防止代码丢失(之后章节的代码优化会解决缓存穿透问题):

public Shop queryWithPassThrough(Long id){String key = CACHE_SHOP_KEY + id;//从Redis中查询商铺缓存,存储对象可以用String或者Hash,这里用StringString shopJson = stringRedisTemplate.opsForValue().get(key);//判断是否存在if (StrUtil.isNotBlank(shopJson)) {//存在,直接返回Shop shop = JSONUtil.toBean(shopJson, Shop.class);return shop;}//判断命中的是否是nullif (shopJson != null){return null;}//不存在,根据id查询数据库Shop shop = getById(id);if (shop == null){//存一个null到Redis中//这种没用的信息,TTL没必要设置太长了,这里我设置成了2minstringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);return null;}//存在,写入RedisstringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);//返回return shop;
}

同样的,我们也可以把互斥锁解决缓存击穿的代码给单独封装起来,注意,我们不管获得互斥锁的线程有没有正常执行,即便执行不成功,也要在最后进行unlock,因此我们要利用try…catch…finally:

public Shop queryWithMutex(Long id){String key = CACHE_SHOP_KEY + id;//从Redis中查询商铺缓存,存储对象可以用String或者Hash,这里用StringString shopJson = stringRedisTemplate.opsForValue().get(key);//判断是否存在if (StrUtil.isNotBlank(shopJson)) {//存在,直接返回Shop shop = JSONUtil.toBean(shopJson, Shop.class);return shop;}//判断命中的是否是nullif (shopJson != null){//返回错误信息return null;}//未命中,开始实现缓存重建//尝试获取互斥锁,锁的key跟缓存的key不一样String lockKey = "lock:shop:" + id;Shop shop = null;try {boolean isLock = tryLock(lockKey);//判断是否获取成功if (!isLock){//失败则休眠并重试(递归)Thread.sleep(50);//单位:ms//如果担心递归造成爆栈,可以用循环,一样的return queryWithMutex(id);}//成功就直接开始重建(查询数据库并处理后保存到Redis)shop = getById(id);//由于在这里我们的数据库在本地,重建很快,所以添加休眠时间来模拟数据库重建过程的耗时长,方便测试Thread.sleep(200);//不存在,返回错误if (shop == null){//存一个null到Redis中//这种没用的信息,TTL没必要设置太长了,这里我设置成了2minstringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);return null;}//存在,写入RedisstringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);} catch (InterruptedException e) {throw new RuntimeException(e);}finally {//释放互斥锁unlock(lockKey);}//返回return shop;
}

queryById方法调用这个方法即可:

    @Overridepublic Result queryById(Long id) {//缓存穿透的代码调用//Shop shop = queryWithPassThrough(id);//互斥锁解决缓存击穿Shop shop = queryWithMutex(id);if (shop == null) {return Result.fail("店铺不存在");}//返回return Result.ok(shop);}

测试

我们先把缓存清空,然后运行代码,再进行测试,我们可以使用一个很好用的测试工具jmeter,下载可以看下面这个博主的文章:
jmeter下载教程
官网下载的实在是太慢了,建议大家直接去这个博主的网盘里面下载。
我们可以这样设定,我们设定我们的并发量为1000,即有1000个线程同时访问:
在这里插入图片描述
并配置好我们的http请求:
在这里插入图片描述
运行完以后打开查看结果树:
在这里插入图片描述
数据都查到了。
打开汇总报告可以看到我们的吞吐量接近200:
在这里插入图片描述
如此大的数据量打下去,但是我们的日志显示我们的数据库只执行了一次的查询语句:
在这里插入图片描述
说明我们已经使用互斥锁成功避免了缓存击穿。

逻辑过期解决缓存击穿

代码实现

根据上次讲解的逻辑过期解决缓存击穿问题的方式,我们可以将客户端查询数据的流程修改为如下:
在这里插入图片描述
其实通常情况,因为是热点key,一般都是会出现在Redis里面的,且因为我们没有设置TTL,所以热点key是一定会一直存在的,但为了严谨起见,还是在判定缓存未命中的时候返回空。
我们主要解析一下主要的流程:

1、我们判断一下缓存是否逻辑过期了,如果没有过期,我们直接返回信息到客户端即可
2、如果缓存逻辑过期了,这个线程就尝试获取互斥锁,如果获取成功,说明它是第一个访问Redis的这个过期key的线程,那么这个线程要做2件事:
(1)返回这个旧数据给客户,虽然数据是旧的,但是这是一种暂时的牺牲
(2)开辟新的线程来进行缓存数据的重建,重建完毕就释放这个互斥锁
3、除了第2种情况说的这个线程,其他线程在知道自己访问的数据过期之后,获取互斥锁都会失败,那么这时候只需要直接返还数据给客户就好了,可能是旧数据,也可能是新数据(第一个线程释放锁或者缓存数据重建成功了)

首先我们要对Shop类增加逻辑过期时间这样一个字段,一种方案是直接添加,这种会违背开闭原则,一种是可以新增加一个类,并且类中包含了逻辑过期时间expireTime,但是该怎么把这个属性添加到Shop里面呢?可以让Shop继承这个类,就可以获得这个类中的属性,但同样会修改Shop这个类的源代码,同样违背开闭原则,所以最好的方法就是用关联来代替继承:
在这里插入图片描述
如果不是很清楚的话可以先去学一下Java设计模式。

为了方便演示,我们首先要进行一个数据预热,我们需要有这个一个带着逻辑过期时间的店铺信息,因此这里就先用单元测试的方式,来让带着expireTime属性的数据存入Redis里面。

1、先编写一个类,用于将带着逻辑过期时间的Shop存入Redis里面:

    public void saveShop2Redis(Long id, Long expireSeconds){//查询店铺数据Shop shop = getById(id);//封装逻辑过期时间RedisData redisData = new RedisData();//注入Shop对象redisData.setData(shop);//注入逻辑过期时间redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));//写入Redis,不要设置TTLstringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));}

2、编写单元测试代码:

@SpringBootTest
class HmDianPingApplicationTests {@Resourceprivate ShopServiceImpl shopService;@Testvoid testSaveShop(){shopService.saveShop2Redis(1L, 10L);}}

我们运行这个单元测试后,打开Redis客户端:
在这里插入图片描述
证明已经存储成功。

现在正式开始用逻辑过期思想去解决缓存击穿的问题,编写这个方法:

	private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);public Shop queryWithLogicalExpire(Long id){String key = CACHE_SHOP_KEY + id;//从Redis中查询商铺缓存,存储对象可以用String或者Hash,这里用StringString shopJson = stringRedisTemplate.opsForValue().get(key);//判断是否存在if (StrUtil.isBlank(shopJson)) {//未命中,直接返回return null;}//命中,先把json反序列化成对象RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);JSONObject data = (JSONObject) redisData.getData();//注意我们获取到了data信息以后,返回的会是一个JSONObject的格式Shop shop = JSONUtil.toBean(data, Shop.class);LocalDateTime expireTime = redisData.getExpireTime();//判断是否过期if (expireTime.isAfter(LocalDateTime.now())){//未过期,直接返回店铺信息return shop;}//已过期,缓存重建//获取互斥锁String lockKey = LOCK_SHOP_KEY + id;//LOCK_SHOP_KEY="lock:shop:"boolean isLock = tryLock(lockKey);//判断是否获取锁成功if (isLock){//成功获取锁,开启独立线程来实现缓存重建,用线程池来做CACHE_REBUILD_EXECUTOR.submit(() -> {try {// 重建缓存this.saveShop2Redis(id, 20L);} catch (Exception e) {throw new RuntimeException(e);}finally {//释放锁unlock(lockKey);}});}//没有成功获取,直接返回过期商铺信息return shop;}

为了方便验证,之前的缓存重建函数我们给他增加一个模拟重建时间200ms:

    public void saveShop2Redis(Long id, Long expireSeconds) throws InterruptedException {//查询店铺数据Shop shop = getById(id);Thread.sleep(200);//封装逻辑过期时间RedisData redisData = new RedisData();//注入Shop对象redisData.setData(shop);//注入逻辑过期时间redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));//写入Redis,不要设置TTLstringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));}

之前存储在Redis的数据,其实早就已经逻辑过期了,只是因为我们没有设置TTL,它依旧存在于Redis里面,只是可能已经过期了。
queryById直接改成调用该方法,接着运行代码。

测试

1、我们直接将数据库中的餐厅名从101修改为102,使得数据库数据与缓存不一致:
在这里插入图片描述

2、打开jmeter进行测试,这里把线程数改小一点,不然不好观察:
在这里插入图片描述
运行后,观察结果树,可以发现我们一开始查到的就是旧数据,且这个过程会持续一段时间:
在这里插入图片描述
到后面,查询到的数据就是正常的数据了:
在这里插入图片描述
查看idea后台,可以发现我们只执行了一次重构,说明只有一个线程操作了数据库,其他数据库都被互斥锁拦住了:
在这里插入图片描述
这也证明了逻辑过期方法会造成短暂的数据不一致的情况。

综上,缓存击穿问题的两种解决思想,其demo都已经调通。

相关文章:

Redis:原理速成+项目实战——Redis实战5(互斥锁、逻辑过期解决缓存击穿问题)

👨‍🎓作者简介:一位大四、研0学生,正在努力准备大四暑假的实习 🌌上期文章:Redis:原理速成项目实战——Redis实战4(解决Redis缓存穿透、雪崩、击穿) 📚订阅专…...

前端优化之一:dns预获取 dns-prefetch 提升页面载入速度

问题:怎么做到dns域解析? 用于优化网站页面的图片 问题:怎么提升网站性能? dns域解析,是提升网站的一个办法。 DNS Prefetch,即DNS预获取,是前端优化的一部分。 一般来说,在前端…...

C语言中一些基本数据类型的典型大小

char:通常是1字节。表示一个字符。int:通常在现代系统中是4字节(但这取决于编译器和架构,有时可能是2字节)。float:通常是4字节。double:通常是8字节。short 和 short int:通常是2字…...

[C/C++]排序算法 快速排序 (递归与非递归)

目录 🚩概念: 🚩实现: ⚡1.hoare ⚡2.挖坑法 ⚡3.双指针法 🚩快速排序递归实现 🚩快速排序非递归实现 🚩概念: 通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据比另一部分的所有…...

『年度总结』逐梦编程之始:我的2023学习回顾与展望

目录 前言 我与Python 我与C语言 第一篇正式博客: 第二篇正式博客(扫雷): 指针学习笔记: C语言学习笔记: 我与数据结构: yuan 这篇博客,我将回顾2023年编程之旅的起点,同时展…...

MyBatis学习二:Mapper代理开发、配置文件完成增删改查、注解开发

前言 公司要求没办法,前端也要了解一下后端知识,这里记录一下自己的学习 学习教程:黑马mybatis教程全套视频教程,2天Mybatis框架从入门到精通 文档: https://mybatis.net.cn/index.html Mapper代理开发 目的 解决…...

【React系列】受控非受控组件

本文来自#React系列教程:https://mp.weixin.qq.com/mp/appmsgalbum?__bizMzg5MDAzNzkwNA&actiongetalbum&album_id1566025152667107329) 一. refs 的使用 在React的开发模式中,通常情况下不需要、也不建议直接操作DOM原生,但是某些…...

OpenCV-Python(22):2D直方图

目标 了解图像的2D直方图绘制2D直方图 介绍 在前面的部分我们介绍了如何绘制一维直方图,之所以称为一维,是因为我们只考虑了图像的一个特征:灰度值。但是在2D 直方图中我们就需要考虑两个图像特征。对于彩色图像的直方图通常情况下我们需要…...

Kubernetes 100个常用命令

本文简单总结关于使用 Kubectl 进行 Kubernetes 诊断的指南。列出了 100 个 Kubectl 命令,这些命令对于诊断 Kubernetes 集群中的问题非常有用。这些问题包括但不限于: 集群信息 Pod 诊断 服务诊断 部署诊断 网络诊断 持久卷和持久卷声明诊断 资源…...

labuladong日常刷题-差分数组 | LeetCode 1109航班预定统计 | 花式遍历 151反转字符串里的单词

差分数组–前缀和数组的升级 LeetCode 1109 航班预定统计 2024.1.1 题目链接labuladong讲解[链接] class Solution { public:vector<int> corpFlightBookings(vector<vector<int>>& bookings, int n) {//构建航班人数数组&#xff0c;数组大小为n,初…...

HbuilderX中的git的使用

原文链接https://blog.csdn.net/Aom_yt/article/details/119924356...

LeetCode每日一题 | 1944. 队列中可以看到的人数

文章目录 队列中可以看到的人数题目描述问题分析程序代码&#xff08;Golang 版本&#xff09; 队列中可以看到的人数 题目描述 原题链接 有 n 个人排成一个队列&#xff0c;从左到右 编号为 0 到 n - 1 。给你以一个整数数组 heights &#xff0c;每个整数 互不相同&#xff…...

React16源码: JSX2JS及React.createElement源码实现

JSX 到 Javascript 的转换 React中的 JSX 类似于 Vue中的template模板文件&#xff0c;Vue是基于编译时将template模板转换成render函数在React中&#xff0c;JSX是类似于html和javascript混编的语法&#xff0c;而javascript是真的javascript, html并非真的html它的可阅读性可…...

整理composer安装版本的python脚本

整理composer安装版本的python脚本 脚本实现的功能是去除composer安装命令后的版本号 def remove_version_numbers(commands):"""Remove version numbers from composer require commands.Args:commands (list of str): List of composer require commands.Retu…...

十、基本对话框大集合(Qt5 GUI系列)

目录 一、设计需求 二、实现代码 三、代码解析 四、总结 一、设计需求 Qt提供了很多标准的对话框。例如标准文件对话框(QFileDialog)、标准颜色对话框(QColorDialog)、标准字体对话框 (QFontDialog)、标准输入对话框 (QInputDialog) 及消息对话框 (QMessageBox)。本文展示各…...

大A又跌了

才开盘几天&#xff0c;又开始下跌了。生活更加苦难。期待高深算法。...

This error originates from a subprocess, and is likely not a problem with pip

我遇这个问题是的原因是包名错误 注意检查包名...

数据库基础知识1

关系模型的程序员不需熟悉数据库的存取路径 在3层模式结构中,___I___是数据库的核心和关键,___Ⅱ___通常是模式的子集,数据库模式的描述提供给用户,____Ⅲ__的描述存储在硬盘上。Ⅰ.模式Ⅱ. 外模式Ⅲ. 内模式 数据库中,数据的物理独立性是指用户的应用程序与存储在磁盘上数据库…...

【GO语言卵细胞级别教程】01.GO基础知识

01.GO基础知识 目录 01.GO基础知识1.GO语言的发展历程2.发展历程3.Windowns安装4.VSCode配置5.基础语法5.1 第一段代码5.2 GO执行的流程5.3 语法规则5.4 代码风格5.5 学习网址 1.GO语言的发展历程 Go语言是谷歌公司于2007年开始开发的一种编程语言&#xff0c;由Robert Griese…...

215.【2023年华为OD机试真题(C卷)】按身高和体重排排队(排序题-JavaPythonC++JS实现)

🚀点击这里可直接跳转到本专栏,可查阅顶置最新的华为OD机试宝典~ 本专栏所有题目均包含优质解题思路,高质量解题代码(Java&Python&C++&JS分别实现),详细代码讲解,助你深入学习,深度掌握! 文章目录 一. 题目-按身高和体重排排队二.解题思路三.题解代码Pyt…...

虚函数(C++)

四、多态4.1 虚函数 四、多态 多态性是面向对象程序设计语言的又一重要特征&#xff0c;多态&#xff08;polymorphism&#xff09;通俗的讲&#xff0c;就是用一个相同的名字定义许多不同的函数&#xff0c;这些函数可以针对不同数据类型实现相同或类似的功能&#xff0c;即所…...

力扣25题: K 个一组翻转链表

【题目链接】力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台&#xff0c;解题代码如下&#xff1a; class Solution {public ListNode reverseKGroup(ListNode head, int k) {ListNode curNode head;ListNode groupHead, groupTail head, lastGrou…...

网络安全应急响应工具之-流量安全取证NetworkMiner

在前面的一些文章中&#xff0c;用了很多的章节介绍流量分析和捕获工具wireshark。Wireshark是一款通用的网络协议分析工具&#xff0c;非常强大&#xff0c;关于wireshark的更多介绍&#xff0c;请关注专栏&#xff0c;wireshark从入门到精通。本文将介绍一个专注于网络流量取…...

http 401 错误

HTTP 401 错误表示未被授权&#xff0c;指客户端通过请求头中的身份验证数据进行身份验证&#xff0c;服务器返回401状态码表示身份验证失败。HTTP 401 错误通常与身份验证和授权相关的 Web 请求有关。 一、HTTP 401错误的定义 HTTP 401 错误是 HTTP 状态码的一种。由于服务器…...

Docker-Compose部署Redis(v7.2)哨兵模式

文章目录 一、前提准备1. 主从集群2. 文件夹结构 二、配置文件1. redis server配置文件2. redis sentinel配置文件3. docker compose文件 三、运行四、测试 环境 docker desktop for windows 4.23.0redis 7.2 一、前提准备 1. 主从集群 首先需要有一个redis主从集群&#x…...

解决问题:PPT中插入视频编辑模式可以播放,幻灯片放映后播放不了

目录 一、原因分析二、解决办法三、其它问题 一、原因分析 这可能是由于PowerPoint的硬件图形加速功能导致的。 二、解决办法 禁用硬件图形加速。 &#xff08;1&#xff09;点击《文件》选项卡 &#xff08;2&#xff09;点击《选项》 &#xff08;3&#xff09;在《高级》…...

使用react+vite开发项目时候,部署上线后刷新页面无法访问解决办法

说一下我这边的环境和使用的路由模式&#xff1a;vitereactBrowserRouter路由模式&#xff0c;所以如果你和我一样的话&#xff0c;可以试试我的这种解决办法&#xff0c;我是将项目打包后直接丢到服务器上的目录里面&#xff0c;然后配置nginx直接访问根目录。 我的nginx配置…...

45. 跳跃游戏 II(Java)

题目描述&#xff1a; 给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。 每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说&#xff0c;如果你在 nums[i] 处&#xff0c;你可以跳转到任意 nums[i j] 处: 0 < j < nums[i] i j < n 返回到…...

[足式机器人]Part4 南科大高等机器人控制课 CH12 Robotic Motion Control

本文仅供学习使用 本文参考&#xff1a; B站&#xff1a;CLEAR_LAB 笔者带更新-运动学 课程主讲教师&#xff1a; Prof. Wei Zhang 课程链接 &#xff1a; https://www.wzhanglab.site/teaching/mee-5114-advanced-control-for-robotics/ 南科大高等机器人控制课 Ch12 Robotic …...

【C++】知识点汇总(上)

C知识点复习上 一、C 概述1. 基本数据类型2. 变量定义和访问3. 常量与约束访问 二、程序控制结构详解与示例1. 表达式2. 选择控制2.1 if 语句2.2 switch 语句 3. 循环控制3.1 for 循环3.2 while 循环3.3 do-while 循环 4. goto 语句5. 控制语句的嵌套 三、函数1. 函数的定义和调…...

十年网站建设/重庆黄埔seo整站优化

题目相关 题目链接 洛谷&#xff0c;https://www.luogu.com.cn/problem/P1114。 计蒜客&#xff0c;https://nanti.jisuanke.com/t/T1853。 我的OJ&#xff0c;http://47.110.135.197/problem.php?id5252。 题目描述 近来&#xff0c;初一年的XXX小朋友致力于研究班上同…...

如何在网站上做免费代理/网址服务器查询

Map与List、Set接口不同&#xff0c;它是由一系列键值对组成的集合&#xff0c;提供了key到Value的映射。同时它也没有继承Collection。在Map中它保证了key与value之间的一一对应关系。也就是说一个key对应一个value&#xff0c;所以它不能存在相同的key值&#xff0c;当然valu…...

官方网站建立/企业培训权威机构

本篇项目地址&#xff0c;名字是媒体解码MediaCodec,MediaExtractor&#xff0c;求starhttps://github.com/979451341/Audio-and-video-learning-materials这次要用到新的东西SurfaceView、MediaCodec、MediaExtractor、MediaFormat 1.文字说明 SurfaceView&#xff1a;一个Vie…...

义乌免费做网站/国内新闻大事20条

一般的DBMS系统&#xff0c;默认都会使用读提交(Read-Comitted&#xff0c;RC)作为默认隔离级别&#xff0c;如Oracle、SQLServer等&#xff0c;而MySQL却使用可重复读(Read-Repeatable&#xff0c;RR)。要知道&#xff0c;越高的隔离级别&#xff0c;能解决的数据一致性问题越…...

wordpress改域名后500/网络营销的特征和功能

typedef struct bthnode { ElemType data; struct bthnode *lchild,*rchild; int ltag,rtag; }BthNode; 若ltag0,表示左指针指向左孩子&#xff0c;ltag1,表示左指针指向前趋节点 若rtag0,表示左指针指向左孩子&#xff0c;rtag1,表示左指针指向前趋节点...

专业网站建设品牌/昆明seo推广外包

一、ADB 简介 1什么是 ADB? ADB 全称为 Android Debug Bridge&#xff0c;起到调试桥的作用&#xff0c;是一个客户端-服务器端程序。其中客户端是用来操作的电脑&#xff0c;服务端是 Android 设备。ADB 也是 Android SDK 中的一个工具&#xff0c;可以直接操作管理 Androi…...