首页
关于
Search
1
给你10个市场数据调研报告的免费下载网站!以后竞品数据就从这里找!
182 阅读
2
php接口优化 使用curl_multi_init批量请求
144 阅读
3
《从菜鸟到大师之路 ElasticSearch 篇》
107 阅读
4
2024年备考系统架构设计师
104 阅读
5
PHP 文件I/O
92 阅读
php
thinkphp
laravel
工具
开源
mysql
数据结构
总结
思维逻辑
令人感动的创富故事
读书笔记
前端
vue
js
css
书籍
开源之旅
架构
消息队列
docker
教程
代码片段
redis
服务器
nginx
linux
科普
java
c
ElasticSearch
测试
php进阶
php基础
登录
Search
标签搜索
php函数
php语法
性能优化
安全
错误和异常处理
问题
vue
Composer
Session
缓存
框架
Swoole
api
并发
异步
正则表达式
php-fpm
mysql 索引
开发规范
协程
dafenqi
累计撰写
786
篇文章
累计收到
28
条评论
首页
栏目
php
thinkphp
laravel
工具
开源
mysql
数据结构
总结
思维逻辑
令人感动的创富故事
读书笔记
前端
vue
js
css
书籍
开源之旅
架构
消息队列
docker
教程
代码片段
副业
redis
服务器
nginx
linux
科普
java
c
ElasticSearch
测试
php进阶
php基础
页面
关于
搜索到
100
篇与
的结果
2023-12-13
如何使用ChatGPT写代码
如何使用ChatGPT写代码须知事项ChatGPT可以纠正错误、简化复杂的想法和解决漏洞,让现有的代码更加简洁。开发者想要节省时间的话,可以用ChatGPT给应用程序创建脚手架、模板和样板代码。ChatGPT会出错,所以无法取代软件工程师。一定要先测试ChatGPT写出来的代码,确认无误才能投入使用。方法1使用ChatGPT写代码。1给代码创建结构。ChatGPT在软件开发中最大的用途之一就是给程序创建脚手架。把你想要编写的程序类型告诉ChatGPT,然后粘贴任何库、依赖项、文件名和其它要包含的详细信息。你可以自然地使用完整的句子和段落输入请求,ChatGPT会在几秒钟内生成一个代码模板。2生成代码片段。请求ChatGPT协助编写函数、例程和其它代码,这样就不需要花太多时间在Stack Overflow网站上查找语法示例。比如说,如果你请求ChatGPT“编写一个利用切片反转字符串的Python函数”,ChatGPT不仅会生成代码,还会解释它的工作原理。你之后可以在整个项目中使用这些代码作为样板文件。3补充现有的代码。询问ChatGPT如何更改现有的代码,让它运行得更快、更流畅,或是做其它任务。把你的代码粘贴到对话框,然后阐明你想要做的更改。比如说,“根据ID列按数字顺序显示结果”,或是“把这些项目加到3号位置的列表中”。4简化和重构复杂的代码。给ChatGPT发送代码,请求它“简化这段代码”,让你的程序更紧凑。想要在不改变原始结构的情况下重新编写代码,可以说:“重构这段代码,提高它的效率。”ChatGPT会清理代码,让它变得更简洁,还会总结它所做的更改,作为你日后的参考。5解释代码片段。不管你对某个代码片段还是整个程序感到困惑,都可以要求ChatGPT解惑。把代码粘贴到ChatGPT中,然后询问:“这个函数是干什么用的?”,或是“这个算法是如何运作的?”6找出错误和漏洞。ChatGPT可以帮助识别和修复代码中的错误。把代码粘贴到字段中,然后询问:“这段代码有什么问题?”,或是“要怎样修复这个程序中的错误?”。必要时,ChatGPT会请求你输入更多信息,然后提供修复方法。7生成另一套代码。即使你的程序顺利运作,或许还有更好的编码方式。把你的代码发给ChatGPT,询问:“有没有更好的方法来完成<你的目标>?”ChatGPT会提出建议,比如建议替代的算法,然后详细解释它的所有建议。8将代码翻译成其它编程语言。如果你已经掌握了一种编程语言,可以使用ChatGPT将代码转换成你不太熟悉的语言。举个例子,将一个C++程序粘贴到ChatGPT中,请求它“把代码翻译成Java”。9在几秒钟内测试函数。与其自己花时间给函数编写测试用例,不如让ChatGPT帮你。ChatGPT会对你指定的函数执行几项测试用例,并报告结果。[1]10给人类编写文档和注释。不必自己给程序编写操作文档或注释,只需要把代码粘贴到ChatGPT中,请求它“解释这段代码”就行了。之后可以将ChatGPT的解释添加到程序的注释中,甚至把它放进官方文档里。方法2让Chat GPT写出有效的代码。1给ChatGPT提供充足的信息来编写你需要的代码。ChatGPT依靠你提供的上下文和它本身的训练数据生成响应。详细说明自己的要求,ChatGPT才能生成有用的代码。清楚说明你想让程序或网站执行的任务,想使用的编程语言和程序的用途。举个例子,如果你想让ChatGPT给你的宠物看护业务建立一个网站,让潜在客户查看你提供的服务,并咨询预订情况,可以这样写:[2]“建立一个简单的单页网站宣传我的宠物看护业务,包括遛狗和寄宿服务。创建一个表来罗列各项服务和价格,其中寄宿服务每晚60块,遛狗单次30分钟20块。创建一个联系表单让请求服务的客户填写。表单必须允许客户从日历中选择他们想要的日期。把客户的回复发送到我的电子邮件地址me@myemailaddress.com。”在这个示例中,ChatGPT会提供创建一个简单网站的HTML和CSS代码,你之后可以把它上传到网络主机。这段代码现在是你的了,你可以随意做一些细微的调整来个性化网站。2补充更多细节,以获取想要的代码。ChatGPT是会话式AI,它可以从当前会话中引用多达3000个单词,所以你有足够的空间来补充编码请求。[3]我们之前要求了ChatGPT编写一个宠物看护业务的网站。如果你还想在页面上的表格添加宠物美容价格,只需要说“把单次100块的美容服务添加到收费表上”,ChatGPT就会重新生成代码来包含你要求做的更改。你也可以添加其它细节,比如“把标题标签改成“上海平价宠物看护服务”,或是“在客户提交表格后显示一个感谢页面”。3通过提问来澄清错误或消除困惑。如果AI聊天机器人编写的代码令人困惑或完全错误,一定要告诉它。请求ChatGPT进行澄清,它会根据训练数据检查自己的工作,并对代码做出相应的调整。你也可以提供更详细的上下文,以获取更准确的答案。[4]记住,ChatGPT不是软件工程师,它无法理解细微差别和训练数据中没有的上下文信息。它主要依赖2021年前的互联网可用数据,无法随时根据用户需求在网络上搜索最新的内容。[5]4不要全盘相信ChatGPT写的代码,一定要亲自做测试。虽然ChatGPT作为一个AI聊天机器人可以写出好得令人难以置信的代码,但是它经常会犯错,而且不会或拒绝纠正。如果ChatGPT的训练数据含有错误的代码示例,它会把这些错误的示例当成事实。所以,ChatGPT在自己编写程序方面还不够可靠。你可以把它编写的代码作为参考,但是它不能取代软件工程师,你还是得自己学习编程。小提示ChatGPT的训练数据截至2021年,如果你正在开发的程序需要使用最新的库或编码约定,ChatGPT将无法提供准确的代码。你跟ChatGPT的对话可能被用于改进服务质量,切记不要泄露任何隐私。[6]滑到页面底部 --> 点击右下角 --> ChatGPT 传送门。
2023年12月13日
37 阅读
0 评论
0 点赞
2023-12-11
如何编写编写干净的 PHP 代码
如何编写编写干净的 PHP 代码介绍软件工程原理,摘自 Robert C. Martin 的著作 Clean Code, 适用于PHP。这不是风格指南。这是一份生产指南 PHP 中可读、可重用和可重构的软件。并非这里的每一项原则都必须严格遵守,甚至更少是普遍遵守的 商定。这些是指导方针,仅此而已,但它们是许多准则的编纂 _Clean Code_ 作者多年的集体经验。灵感来自 clean-code-javascript。尽管许多开发人员仍在使用 PHP 5,但本文中的大多数示例仅适用于 PHP 7.1+。变量使用有意义且可发音的变量名称坏:$ymdstr = $moment->format('y-m-d');好:$currentDate = $moment->format('y-m-d');对相同类型的变量使用相同的词汇坏:getUserInfo(); getUserData(); getUserRecord(); getUserProfile();好:getUser();使用可搜索的名称(第 1 部分)我们将阅读比我们编写的更多的代码。重要的是,我们编写的代码是 可读和可搜索。通过_不_命名最终有意义的变量 了解我们的程序,我们伤害了我们的读者。 使你的名字可搜索。坏:// What the heck is 448 for? $result = $serializer->serialize($data, 448);好:$json = $serializer->serialize($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);使用可搜索的名称(第 2 部分)坏:class User { // What the heck is 7 for? public $access = 7; } // What the heck is 4 for? if ($user->access & 4) { // ... } // What's going on here? $user->access ^= 2;好:class User { public const ACCESS_READ = 1; public const ACCESS_CREATE = 2; public const ACCESS_UPDATE = 4; public const ACCESS_DELETE = 8; // User as default can read, create and update something public $access = self::ACCESS_READ | self::ACCESS_CREATE | self::ACCESS_UPDATE; } if ($user->access & User::ACCESS_UPDATE) { // do edit ... } // Deny access rights to create something $user->access ^= User::ACCESS_CREATE;使用解释变量坏:$address = 'One Infinite Loop, Cupertino 95014'; $cityZipCodeRegex = '/^[^,]+,\s*(.+?)\s*(\d{5})$/'; preg_match($cityZipCodeRegex, $address, $matches); saveCityZipCode($matches[1], $matches[2]);不错:它更好,但我们仍然严重依赖正则表达式。$address = 'One Infinite Loop, Cupertino 95014'; $cityZipCodeRegex = '/^[^,]+,\s*(.+?)\s*(\d{5})$/'; preg_match($cityZipCodeRegex, $address, $matches); [, $city, $zipCode] = $matches; saveCityZipCode($city, $zipCode);好:通过命名子模式来减少对正则表达式的依赖。$address = 'One Infinite Loop, Cupertino 95014'; $cityZipCodeRegex = '/^[^,]+,\s*(?<city>.+?)\s*(?<zipCode>\d{5})$/'; preg_match($cityZipCodeRegex, $address, $matches); saveCityZipCode($matches['city'], $matches['zipCode']);避免嵌套太深,提早返回(第 1 部分)过多的 if-else 语句会使代码难以理解。显式更好 比隐含。坏:function isShopOpen($day): bool { if ($day) { if (is_string($day)) { $day = strtolower($day); if ($day === 'friday') { return true; } elseif ($day === 'saturday') { return true; } elseif ($day === 'sunday') { return true; } return false; } return false; } return false; }好:function isShopOpen(string $day): bool { if (empty($day)) { return false; } $openingDays = ['friday', 'saturday', 'sunday']; return in_array(strtolower($day), $openingDays, true); }避免嵌套太深,提早返回(第 2 部分)坏:function fibonacci(int $n) { if ($n < 50) { if ($n !== 0) { if ($n !== 1) { return fibonacci($n - 1) + fibonacci($n - 2); } return 1; } return 0; } return 'Not supported'; }好:function fibonacci(int $n): int { if ($n === 0 || $n === 1) { return $n; } if ($n >= 50) { throw new Exception('Not supported'); } return fibonacci($n - 1) + fibonacci($n - 2); }避免思维导图不要强迫代码的读者翻译变量的含义。 显式比隐式好。坏:$l = ['Austin', 'New York', 'San Francisco']; for ($i = 0; $i < count($l); $i++) { $li = $l[$i]; doStuff(); doSomeOtherStuff(); // ... // ... // ... // Wait, what is `$li` for again? dispatch($li); }好:$locations = ['Austin', 'New York', 'San Francisco']; foreach ($locations as $location) { doStuff(); doSomeOtherStuff(); // ... // ... // ... dispatch($location); }不要添加不需要的上下文如果你的类/对象名称告诉你一些事情,不要在你的 变量名称。坏:class Car { public $carMake; public $carModel; public $carColor; //... }好:class Car { public $make; public $model; public $color; //... }比较使用相同的比较不好:简单的比较会将字符串转换为整数。$a = '42'; $b = 42; if ($a != $b) { // The expression will always pass }比较回来了,但实际上是! 字符串与整数不同。$a != $b`FALSETRUE42`42好:相同的比较将比较类型和值。$a = '42'; $b = 42; if ($a !== $b) { // The expression is verified }比较返回 。$a !== $b`TRUE`Null 合并运算符Null 合并是 PHP 7 中引入的一个新运算符。已将 null 合并运算符添加为句法糖,用于需要将三元与 结合使用的常见情况。如果它存在并且不存在,则返回其第一个操作数;否则,它将返回其第二个操作数。??`isset()`null坏:if (isset($_GET['name'])) { $name = $_GET['name']; } elseif (isset($_POST['name'])) { $name = $_POST['name']; } else { $name = 'nobody'; }好:$name = $_GET['name'] ?? $_POST['name'] ?? 'nobody';功能使用默认参数而不是短路或条件不好:这不好,因为可以.$breweryName`NULL`function createMicrobrewery($breweryName = 'Hipster Brew Co.'): void { // ... }不错:这种观点比以前的版本更容易理解,但它更好地控制了变量的值。function createMicrobrewery($name = null): void { $breweryName = $name ?: 'Hipster Brew Co.'; // ... }好:您可以使用类型提示,并确保 不会是 .$breweryName`NULL`function createMicrobrewery(string $breweryName = 'Hipster Brew Co.'): void { // ... }函数参数(理想情况下为 2 个或更少)限制函数参数的数量非常重要,因为它使 更轻松地测试您的函数。拥有三个以上会导致组合爆炸 你必须用每个单独的参数测试大量不同的情况。零参数是理想的情况。一两个论点是可以的,应该避免三个论点。 除此之外,任何事情都应该被合并。通常,如果您有两个以上的 参数,那么你的函数试图做太多。如果不是,大多数 在更高级别的对象作为参数就足够了。坏:class Questionnaire { public function __construct( string $firstname, string $lastname, string $patronymic, string $region, string $district, string $city, string $phone, string $email ) { // ... } }好:class Name { private $firstname; private $lastname; private $patronymic; public function __construct(string $firstname, string $lastname, string $patronymic) { $this->firstname = $firstname; $this->lastname = $lastname; $this->patronymic = $patronymic; } // getters ... } class City { private $region; private $district; private $city; public function __construct(string $region, string $district, string $city) { $this->region = $region; $this->district = $district; $this->city = $city; } // getters ... } class Contact { private $phone; private $email; public function __construct(string $phone, string $email) { $this->phone = $phone; $this->email = $email; } // getters ... } class Questionnaire { public function __construct(Name $name, City $city, Contact $contact) { // ... } }函数名称应说明它们的作用坏:class Email { //... public function handle(): void { mail($this->to, $this->subject, $this->body); } } $message = new Email(...); // What is this? A handle for the message? Are we writing to a file now? $message->handle();好:class Email { //... public function send(): void { mail($this->to, $this->subject, $this->body); } } $message = new Email(...); // Clear and obvious $message->send();函数应该只是一个抽象级别当你有多个抽象级别时,你的函数通常是 做得太多了。拆分功能可提高可重用性,更轻松 测试。坏:function parseBetterPHPAlternative(string $code): void { $regexes = [ // ... ]; $statements = explode(' ', $code); $tokens = []; foreach ($regexes as $regex) { foreach ($statements as $statement) { // ... } } $ast = []; foreach ($tokens as $token) { // lex... } foreach ($ast as $node) { // parse... } }也不好:我们已经执行了一些功能,但功能仍然非常复杂且无法测试。parseBetterPHPAlternative()function tokenize(string $code): array { $regexes = [ // ... ]; $statements = explode(' ', $code); $tokens = []; foreach ($regexes as $regex) { foreach ($statements as $statement) { $tokens[] = /* ... */; } } return $tokens; } function lexer(array $tokens): array { $ast = []; foreach ($tokens as $token) { $ast[] = /* ... */; } return $ast; } function parseBetterPHPAlternative(string $code): void { $tokens = tokenize($code); $ast = lexer($tokens); foreach ($ast as $node) { // parse... } }好:最好的解决方案是移出函数的依赖关系。parseBetterPHPAlternative()class Tokenizer { public function tokenize(string $code): array { $regexes = [ // ... ]; $statements = explode(' ', $code); $tokens = []; foreach ($regexes as $regex) { foreach ($statements as $statement) { $tokens[] = /* ... */; } } return $tokens; } } class Lexer { public function lexify(array $tokens): array { $ast = []; foreach ($tokens as $token) { $ast[] = /* ... */; } return $ast; } } class BetterPHPAlternative { private $tokenizer; private $lexer; public function __construct(Tokenizer $tokenizer, Lexer $lexer) { $this->tokenizer = $tokenizer; $this->lexer = $lexer; } public function parse(string $code): void { $tokens = $this->tokenizer->tokenize($code); $ast = $this->lexer->lexify($tokens); foreach ($ast as $node) { // parse... } } }不要使用标志作为函数参数标志告诉用户此函数执行多项操作。函数应 做一件事。如果函数遵循不同的代码路径,请拆分函数 基于布尔值。坏:function createFile(string $name, bool $temp = false): void { if ($temp) { touch('./temp/' . $name); } else { touch($name); } }好:function createFile(string $name): void { touch($name); } function createTempFile(string $name): void { touch('./temp/' . $name); }避免副作用如果函数执行除 和 中的值之外的任何操作,则会产生副作用 返回另一个或多个值。副作用可能是写入文件、修改 一些全局变量,或者不小心把你所有的钱都汇给一个陌生人。现在,您确实需要偶尔在程序中出现副作用。和以前一样 例如,您可能需要写入文件。您要做的是集中在哪里 你正在这样做。不要有多个函数和类写入特定的 文件。有一个服务可以做到这一点。一个也是唯一一个。重点是避免常见的陷阱,例如在没有 任何结构,使用可变数据类型,可以由任何内容写入,而不是 集中出现副作用的位置。如果你能做到这一点,你会更快乐 比绝大多数其他程序员都要好。坏:// Global variable referenced by following function. // If we had another function that used this name, now it'd be an array and it could break it. $name = 'Ryan McDermott'; function splitIntoFirstAndLastName(): void { global $name; $name = explode(' ', $name); } splitIntoFirstAndLastName(); var_dump($name); // ['Ryan', 'McDermott'];好:function splitIntoFirstAndLastName(string $name): array { return explode(' ', $name); } $name = 'Ryan McDermott'; $newName = splitIntoFirstAndLastName($name); var_dump($name); // 'Ryan McDermott'; var_dump($newName); // ['Ryan', 'McDermott'];不要写入全局函数在许多语言中,污染全局变量是一种不好的做法,因为您可能会与另一种语言发生冲突 库和 API 的用户将不明智,直到他们在 生产。让我们考虑一个例子:如果你想拥有配置数组怎么办? 你可以像 一样编写全局函数,但它可能会与另一个库发生冲突 试图做同样的事情。config()坏:function config(): array { return [ 'foo' => 'bar', ]; }好:class Configuration { private $configuration = []; public function __construct(array $configuration) { $this->configuration = $configuration; } public function get(string $key): ?string { // null coalescing operator return $this->configuration[$key] ?? null; } }加载配置并创建类的实例Configuration$configuration = new Configuration([ 'foo' => 'bar', ]);现在,您必须在应用程序中使用实例。Configuration不要使用单一实例模式Singleton 是一种反模式。转述自布莱恩·巴顿(Brian Button):它们通常被用作全局实例,为什么这么糟糕?因为您将应用程序的依赖项隐藏在代码中,而不是通过接口公开它们。制作全球性的东西以避免传递它是一种代码味道。他们违反了单一责任原则:因为他们控制着自己的创造和生命周期。它们固有地导致代码紧密耦合。这使得在许多情况下在测试中伪造它们变得相当困难。它们在应用程序的生存期内携带状态。测试的另一个打击,因为你最终可能会遇到需要订购测试的情况,这对单元测试来说是一个很大的否定。为什么?因为每个单元测试都应该彼此独立。Misko Hevery对问题的根源也有很好的想法。坏:class DBConnection { private static $instance; private function __construct(string $dsn) { // ... } public static function getInstance(): self { if (self::$instance === null) { self::$instance = new self(); } return self::$instance; } // ... } $singleton = DBConnection::getInstance();好:class DBConnection { public function __construct(string $dsn) { // ... } // ... }创建类的实例并使用 DSN 对其进行配置。DBConnection$connection = new DBConnection($dsn);现在,您必须在应用程序中使用实例。DBConnection封装条件语句坏:if ($article->state === 'published') { // ... }好:if ($article->isPublished()) { // ... }避免负面条件坏:function isDOMNodeNotPresent(DOMNode $node): bool { // ... } if (! isDOMNodeNotPresent($node)) { // ... }好:function isDOMNodePresent(DOMNode $node): bool { // ... } if (isDOMNodePresent($node)) { // ... }避免使用条件这似乎是一项不可能完成的任务。第一次听到这句话时,大多数人会说, “没有声明,我怎么能做任何事情?”答案是 在许多情况下,您可以使用多态性来实现相同的任务。第二个 问题通常是,“嗯,这很好,但我为什么要这样做?这 答案是我们之前学到的一个干净的代码概念:一个函数应该只做 一件事。当您具有具有语句的类和函数时,您 告诉用户您的函数执行了不止一件事。记得 只做一件事。if`if`坏:class Airplane { // ... public function getCruisingAltitude(): int { switch ($this->type) { case '777': return $this->getMaxAltitude() - $this->getPassengerCount(); case 'Air Force One': return $this->getMaxAltitude(); case 'Cessna': return $this->getMaxAltitude() - $this->getFuelExpenditure(); } } }好:interface Airplane { // ... public function getCruisingAltitude(): int; } class Boeing777 implements Airplane { // ... public function getCruisingAltitude(): int { return $this->getMaxAltitude() - $this->getPassengerCount(); } } class AirForceOne implements Airplane { // ... public function getCruisingAltitude(): int { return $this->getMaxAltitude(); } } class Cessna implements Airplane { // ... public function getCruisingAltitude(): int { return $this->getMaxAltitude() - $this->getFuelExpenditure(); } }避免类型检查(第 1 部分)PHP 是非类型化的,这意味着您的函数可以接受任何类型的参数。 有时你会被这种自由所咬,它变得很诱人 函数中的类型检查。有很多方法可以避免这样做。 首先要考虑的是一致的 API。坏:function travelToTexas($vehicle): void { if ($vehicle instanceof Bicycle) { $vehicle->pedalTo(new Location('texas')); } elseif ($vehicle instanceof Car) { $vehicle->driveTo(new Location('texas')); } }好:function travelToTexas(Vehicle $vehicle): void { $vehicle->travelTo(new Location('texas')); }避免类型检查(第 2 部分)如果您正在使用字符串、整数和数组等基本基元值, 你使用 PHP 7+,你不能使用多态性,但你仍然觉得有必要 类型检查,您应该考虑类型声明或严格模式。它为您提供了基于标准PHP语法的静态类型。 手动类型检查的问题在于,这样做需要很多 额外的措辞是,你得到的虚假“类型安全”并不能弥补损失 可读性。保持你的PHP干净,编写好的测试,并有良好的代码审查。 否则,除了PHP严格类型声明或严格模式之外,还可以执行所有这些操作。坏:function combine($val1, $val2): int { if (! is_numeric($val1) || ! is_numeric($val2)) { throw new Exception('Must be of type Number'); } return $val1 + $val2; }好:function combine(int $val1, int $val2): int { return $val1 + $val2; }删除死代码死代码和重复代码一样糟糕。没有理由保留它 您的代码库。如果它没有被调用,请摆脱它!它仍然是安全的 如果您仍然需要它,请在您的版本历史记录中。坏:function oldRequestModule(string $url): void { // ... } function newRequestModule(string $url): void { // ... } $request = newRequestModule($requestUrl); inventoryTracker('apples', $request, 'www.inventory-awesome.io');好:function requestModule(string $url): void { // ... } $request = requestModule($requestUrl); inventoryTracker('apples', $request, 'www.inventory-awesome.io');对象和数据结构使用对象封装在 PHP 中,您可以为方法设置 、 和关键字。 使用它,您可以控制对象的属性修改。public`protected`private当您想在获取对象属性之外执行更多操作时,您没有 查找和更改代码库中的每个访问器。使在执行 .set封装内部表示形式。在获取和设置时易于添加日志记录和错误处理。继承此类后,可以重写默认功能。您可以延迟加载对象的属性,例如从 服务器。此外,这是开/闭原则的一部分。坏:class BankAccount { public $balance = 1000; } $bankAccount = new BankAccount(); // Buy shoes... $bankAccount->balance -= 100;好:class BankAccount { private $balance; public function __construct(int $balance = 1000) { $this->balance = $balance; } public function withdraw(int $amount): void { if ($amount > $this->balance) { throw new \Exception('Amount greater than available balance.'); } $this->balance -= $amount; } public function deposit(int $amount): void { $this->balance += $amount; } public function getBalance(): int { return $this->balance; } } $bankAccount = new BankAccount(); // Buy shoes... $bankAccount->withdraw($shoesPrice); // Get balance $balance = $bankAccount->getBalance();使对象具有私有/受保护的成员public方法和属性对于更改来说是最危险的,因为某些外部代码可能很容易依赖它们,并且您无法控制哪些代码依赖于它们。类中的修改对类的所有用户都是危险的。protected修饰符和 public 一样危险,因为它们在任何子类的范围内都可用。这实际上意味着公共和受保护之间的区别仅在于访问机制,但封装保证保持不变。类中的修改对于所有后代类都是危险的。privatemodifier 保证代码仅在单个类的边界内修改是危险的(修改是安全的,并且不会产生 Jenga 效应)。因此,在默认情况下以及需要为外部类提供访问权限时使用。private`public/protected`有关更多信息,您可以阅读 Fabien Potencier 撰写的有关此主题的博客文章。坏:class Employee { public $name; public function __construct(string $name) { $this->name = $name; } } $employee = new Employee('John Doe'); // Employee name: John Doe echo 'Employee name: ' . $employee->name;好:class Employee { private $name; public function __construct(string $name) { $this->name = $name; } public function getName(): string { return $this->name; } } $employee = new Employee('John Doe'); // Employee name: John Doe echo 'Employee name: ' . $employee->getName();类首选组合而不是继承正如四人帮在《设计模式》中所说的那样, 在可能的情况下,您应该更喜欢组合而不是继承。有很多 使用继承的充分理由和使用组合的充分理由。 这句格言的要点是,如果你的思想本能地去做 继承,试着想想组合是否可以更好地模拟你的问题。在一些 案例可以。那么,您可能想知道,“我什么时候应该使用继承?它 取决于你手头的问题,但这是一个不错的继承清单 比构图更有意义:你的继承代表一种“是”的关系,而不是“有”的关系 关系(人-动物与用户->>UserDetails)。您可以重用基类中的代码(人类可以像所有动物一样移动)。您希望通过更改基类来对派生类进行全局更改。 (改变所有动物移动时的热量消耗)。坏:class Employee { private $name; private $email; public function __construct(string $name, string $email) { $this->name = $name; $this->email = $email; } // ... } // Bad because Employees "have" tax data. // EmployeeTaxData is not a type of Employee class EmployeeTaxData extends Employee { private $ssn; private $salary; public function __construct(string $name, string $email, string $ssn, string $salary) { parent::__construct($name, $email); $this->ssn = $ssn; $this->salary = $salary; } // ... }好:class EmployeeTaxData { private $ssn; private $salary; public function __construct(string $ssn, string $salary) { $this->ssn = $ssn; $this->salary = $salary; } // ... } class Employee { private $name; private $email; private $taxData; public function __construct(string $name, string $email) { $this->name = $name; $this->email = $email; } public function setTaxData(EmployeeTaxData $taxData): void { $this->taxData = $taxData; } // ... }避免使用流畅的界面Fluent 接口是一个对象 面向 API,旨在通过使用方法链接提高源代码的可读性。虽然可能有一些上下文,通常是构建器对象,其中 pattern 降低了代码的冗长程度(例如 PHPUnit Mock Builder 或 Doctrine Query Builder), 更多时候,它需要付出一些代价:中断封装。中断装饰器。在测试套件中更难嘲笑。使提交的差异更难阅读。有关更多信息,您可以阅读 Marco Picetta 撰写的有关此主题的完整博客文章。坏:class Car { private $make = 'Honda'; private $model = 'Accord'; private $color = 'white'; public function setMake(string $make): self { $this->make = $make; // NOTE: Returning this for chaining return $this; } public function setModel(string $model): self { $this->model = $model; // NOTE: Returning this for chaining return $this; } public function setColor(string $color): self { $this->color = $color; // NOTE: Returning this for chaining return $this; } public function dump(): void { var_dump($this->make, $this->model, $this->color); } } $car = (new Car()) ->setColor('pink') ->setMake('Ford') ->setModel('F-150') ->dump();好:class Car { private $make = 'Honda'; private $model = 'Accord'; private $color = 'white'; public function setMake(string $make): void { $this->make = $make; } public function setModel(string $model): void { $this->model = $model; } public function setColor(string $color): void { $this->color = $color; } public function dump(): void { var_dump($this->make, $this->model, $this->color); } } $car = new Car(); $car->setColor('pink'); $car->setMake('Ford'); $car->setModel('F-150'); $car->dump();首选期末课程应尽可能使用关键字:final它可以防止不受控制的继承链。它鼓励作文。它鼓励单一责任原则。它鼓励开发人员使用您的公共方法,而不是扩展类来访问受保护的方法。它允许您更改代码,而不会破坏使用您的类的应用程序。唯一的条件是你的类应该实现一个接口,并且没有定义其他公共方法。有关更多信息,您可以阅读 Marco Pivetta (Ocramius) 撰写的有关此主题的博客文章。坏:final class Car { private $color; public function __construct($color) { $this->color = $color; } /** * @return string The color of the vehicle */ public function getColor() { return $this->color; } }好:interface Vehicle { /** * @return string The color of the vehicle */ public function getColor(); } final class Car implements Vehicle { private $color; public function __construct($color) { $this->color = $color; } public function getColor() { return $this->color; } }固体SOLID 是 Michael Feathers 为罗伯特·马丁 (Robert Martin) 命名的前五个原则引入的首字母缩写词,意思是面向对象编程和设计的五个基本原则。S:单一责任原则 (SRP)O:开/闭原理 (OCP)L:李氏替代原理(LSP)I:接口隔离原则(ISP)D:依赖反转原则 (DIP)单一责任原则 (SRP)正如 Clean Code 中所述,“一个类不应该有多个原因 改变”。将一个具有许多功能的类塞进包装是很诱人的,例如 当您在航班上只能携带一个行李箱时。这样做的问题是 你的班级在概念上不会有凝聚力,它会给出很多理由 来改变。尽量减少更改类所需的次数非常重要。 这很重要,因为如果一个类中有太多功能,并且您修改了其中的一部分, 可能很难理解这将如何影响其他依赖模块 您的代码库。坏:class UserSettings { private $user; public function __construct(User $user) { $this->user = $user; } public function changeSettings(array $settings): void { if ($this->verifyCredentials()) { // ... } } private function verifyCredentials(): bool { // ... } }好:class UserAuth { private $user; public function __construct(User $user) { $this->user = $user; } public function verifyCredentials(): bool { // ... } } class UserSettings { private $user; private $auth; public function __construct(User $user) { $this->user = $user; $this->auth = new UserAuth($user); } public function changeSettings(array $settings): void { if ($this->auth->verifyCredentials()) { // ... } } }开/闭原理 (OCP)正如 Bertrand Meyer 所说,“软件实体(类、模块、函数、 等)应该开放以进行扩展,但关闭以进行修改。那有什么作用 但意思是?这个原则基本上表明你应该允许用户 在不更改现有代码的情况下添加新功能。坏:abstract class Adapter { protected $name; public function getName(): string { return $this->name; } } class AjaxAdapter extends Adapter { public function __construct() { parent::__construct(); $this->name = 'ajaxAdapter'; } } class NodeAdapter extends Adapter { public function __construct() { parent::__construct(); $this->name = 'nodeAdapter'; } } class HttpRequester { private $adapter; public function __construct(Adapter $adapter) { $this->adapter = $adapter; } public function fetch(string $url): Promise { $adapterName = $this->adapter->getName(); if ($adapterName === 'ajaxAdapter') { return $this->makeAjaxCall($url); } elseif ($adapterName === 'httpNodeAdapter') { return $this->makeHttpCall($url); } } private function makeAjaxCall(string $url): Promise { // request and return promise } private function makeHttpCall(string $url): Promise { // request and return promise } }好:interface Adapter { public function request(string $url): Promise; } class AjaxAdapter implements Adapter { public function request(string $url): Promise { // request and return promise } } class NodeAdapter implements Adapter { public function request(string $url): Promise { // request and return promise } } class HttpRequester { private $adapter; public function __construct(Adapter $adapter) { $this->adapter = $adapter; } public function fetch(string $url): Promise { return $this->adapter->request($url); } }Liskov 替代原理 (LSP)对于一个非常简单的概念来说,这是一个可怕的术语。它的正式定义是“如果 S 是 T 的子类型,则 T 类型的对象可以替换为 S 类型的对象 (即,S 类型的对象可以替换 T 类型的对象),而不改变任何 该程序的理想属性(正确性、执行的任务、 等等)。这是一个更可怕的定义。对此最好的解释是,如果你有一个父类和一个子类, 那么基类和子类可以互换使用,而不会得到 结果不正确。这可能仍然令人困惑,所以让我们来看看 经典的 Square-Rectangle 示例。在数学上,正方形是一个矩形,但是 如果通过继承使用“is-a”关系对其进行建模,则很快 惹上麻烦了。坏:class Rectangle { protected $width = 0; protected $height = 0; public function setWidth(int $width): void { $this->width = $width; } public function setHeight(int $height): void { $this->height = $height; } public function getArea(): int { return $this->width * $this->height; } } class Square extends Rectangle { public function setWidth(int $width): void { $this->width = $this->height = $width; } public function setHeight(int $height): void { $this->width = $this->height = $height; } } function printArea(Rectangle $rectangle): void { $rectangle->setWidth(4); $rectangle->setHeight(5); // BAD: Will return 25 for Square. Should be 20. echo sprintf('%s has area %d.', get_class($rectangle), $rectangle->getArea()) . PHP_EOL; } $rectangles = [new Rectangle(), new Square()]; foreach ($rectangles as $rectangle) { printArea($rectangle); }好:最好的方法是将四边形分开,并为两种形状分配一个更通用的子类型。尽管正方形和矩形表面上相似,但它们是不同的。 正方形与菱形有很多共同之处,矩形与平行四边形有很多共同之处,但它们不是亚型。 正方形、矩形、菱形和平行四边形是独立的形状,具有各自的属性,尽管相似。interface Shape { public function getArea(): int; } class Rectangle implements Shape { private $width = 0; private $height = 0; public function __construct(int $width, int $height) { $this->width = $width; $this->height = $height; } public function getArea(): int { return $this->width * $this->height; } } class Square implements Shape { private $length = 0; public function __construct(int $length) { $this->length = $length; } public function getArea(): int { return $this->length ** 2; } } function printArea(Shape $shape): void { echo sprintf('%s has area %d.', get_class($shape), $shape->getArea()).PHP_EOL; } $shapes = [new Rectangle(4, 5), new Square(5)]; foreach ($shapes as $shape) { printArea($shape); }接口隔离原则 (ISP)ISP 指出,“不应强迫客户端依赖于 他们不使用。一个很好的例子可以证明这个原则是 需要大型设置对象的类。不需要客户端设置 大量的选择是有益的,因为大多数时候他们不需要 所有设置。使它们成为可选有助于防止出现“胖接口”。坏:interface Employee { public function work(): void; public function eat(): void; } class HumanEmployee implements Employee { public function work(): void { // ....working } public function eat(): void { // ...... eating in lunch break } } class RobotEmployee implements Employee { public function work(): void { //.... working much more } public function eat(): void { //.... robot can't eat, but it must implement this method } }好:不是每个工人都是雇员,但每个雇员都是工人。interface Workable { public function work(): void; } interface Feedable { public function eat(): void; } interface Employee extends Feedable, Workable { } class HumanEmployee implements Employee { public function work(): void { // ....working } public function eat(): void { //.... eating in lunch break } } // robot can only work class RobotEmployee implements Workable { public function work(): void { // ....working } }依赖关系反转原则 (DIP)该原则规定了两件基本的事情:高级模块不应依赖于低级模块。两者都应该 依赖于抽象。抽象不应依赖于细节。细节应取决于 抽象。一开始可能很难理解,但如果你使用过PHP框架(如Symfony),你就会看到这个原则以依赖的形式实现 注射 (DI)。虽然它们不是相同的概念,但 DIP 保持了高层次 模块,了解其低级模块的详细信息并设置它们。 它可以通过 DI 来实现这一点。这样做的一个巨大好处是它减少了 模块之间的耦合。耦合是一种非常糟糕的开发模式,因为 它使你的代码难以重构。坏:class Employee { public function work(): void { // ....working } } class Robot extends Employee { public function work(): void { //.... working much more } } class Manager { private $employee; public function __construct(Employee $employee) { $this->employee = $employee; } public function manage(): void { $this->employee->work(); } }好:interface Employee { public function work(): void; } class Human implements Employee { public function work(): void { // ....working } } class Robot implements Employee { public function work(): void { //.... working much more } } class Manager { private $employee; public function __construct(Employee $employee) { $this->employee = $employee; } public function manage(): void { $this->employee->work(); } }不要重复自己 (DRY)尽量遵守 DRY 原则。尽最大努力避免重复代码。重复代码很糟糕,因为 这意味着如果需要,有不止一个地方可以更改某些内容 改变一些逻辑。想象一下,如果你经营一家餐馆,你跟踪你的库存:所有的 西红柿、洋葱、大蒜、香料等。如果您有多个列表 你保持这个状态,那么当你端上一道菜时,一切都必须更新 西红柿在里面。如果您只有一个列表,则只有一个位置可以更新!通常,您有重复的代码,因为您有两个或更多代码 不同的东西,有很多共同点,但它们的差异迫使你 具有两个或多个单独的函数,它们执行许多相同的操作。删除 重复代码意味着创建一个抽象,可以处理这组不同的 只有一个函数/模块/类的东西。获得正确的抽象是至关重要的,这就是为什么你应该遵循 “类”部分中列出的 SOLID 原则。糟糕的抽象可能是 比重复代码更糟糕,所以要小心!话虽如此,如果你能做到 一个好的抽象,去做吧!不要重复自己,否则你会发现自己 随时更新多个位置,想要更改一件事。坏:function showDeveloperList(array $developers): void { foreach ($developers as $developer) { $expectedSalary = $developer->calculateExpectedSalary(); $experience = $developer->getExperience(); $githubLink = $developer->getGithubLink(); $data = [$expectedSalary, $experience, $githubLink]; render($data); } } function showManagerList(array $managers): void { foreach ($managers as $manager) { $expectedSalary = $manager->calculateExpectedSalary(); $experience = $manager->getExperience(); $githubLink = $manager->getGithubLink(); $data = [$expectedSalary, $experience, $githubLink]; render($data); } }好:function showList(array $employees): void { foreach ($employees as $employee) { $expectedSalary = $employee->calculateExpectedSalary(); $experience = $employee->getExperience(); $githubLink = $employee->getGithubLink(); $data = [$expectedSalary, $experience, $githubLink]; render($data); } }非常好:最好使用代码的紧凑版本。function showList(array $employees): void { foreach ($employees as $employee) { render([$employee->calculateExpectedSalary(), $employee->getExperience(), $employee->getGithubLink()]); } }
2023年12月11日
9 阅读
0 评论
0 点赞
2023-12-05
编程中常用的字符编码知识点
编程中常用的字符编码知识点字符集和字符编码字符集就是字符的集合,如常见的 ASCII字符集,GB2312字符集,Unicode字符集等。这些不同字符集之间最大的区别是所包含的字符数量的不同。字符编码则代表字符集的实际编码规则,是用于计算机解析字符的,如 GB2312,GBK,UTF-8 等。字符编码的本质就是如何使用二进制字节来表示字符的问题。字符集和编码是一对多的关系,同一字符集可能有多种字符编码,如Unicode字符集就有 UTF-8,UTF-16 等。在前端开发中,Javascript程序是使用Unicode字符集,Javascript源码文本通常是基于UTF-8编码。 但js代码中的字符串类型是UTF-16编码的,这也是为什么会碰到api接口返回字符串在前端出现乱码,因为多数服务都使用utf-8编码,前后编码方式不一致。说起字符集的发展历程,可以总结为一句话:几乎都是对ASCII字符集的扩展。ASCII我们知道,计算机是使用二进制来处理信息的。 其中,每一个二进制位(bit)有 0和1 两种状态。一个字节(byte)则有8个二进制位,可以有256种状态。而ASCII就是基于拉丁字母、主要用于显示英文的一种单字节字符集,它的编码和字符是一一对应的,因为它就是使用一个字节8个二进制位来表示,不会超过256个字符。标准的ASCII字符总计有128个字符(2^7),其中前面32个控制字符,后面96个是可打印字符,包括常用的大小写字母数字标点符号等。因为只占用了一个字节的后7位,那字节的最高位一般设置为0。'a'.charCodeAt() // 97 'A'.charCodeAt() // 65 '9'.charCodeAt() // 57 '.'.charCodeAt() // 46 如上,每个字符会对应一个编码(使用数字标识),总共会从0-128。完整的ASCII码表,网上很容易找到。通过ASCII码表,我们发现,小写字母并没有和大写字母挨着排序?这是为了方便大小写之间的转换, A 排在 65(64 + 1) 位,而 a 排在 97(64 + 32 + 1) 位。65 ^ 32 = 97 // A ^ 32 = a 字符集的发展历史ASCII是几乎所有字符集的基础。标准的ASCII码最多只能标识128个字符,欧美国家可以很好的使用,但其他国家的字符变多,自然就不够用了。这个时候,最高位就开始被惦记上,通过扩展ASCII码的最高位,又能满足用于特殊符号的一些国家的需求,这种就是扩展ASCII码。但是亚非拉更多非拉丁语系的国家,字符成千上万,只能使用新的方式。如中文,就又进行了扩展,小于127的字符的意义与标准ASCII码相同,当需要标识汉字时,使用2个字节,每个字节都大于127。这种多字节字符集即GB2312,后续因为不断的扩展,如繁体字和各种符号,甚至少数民族的语言符号等等,又使用了包括GBK等不同字符集。因此,很多国家都制定了自己的编码字符集,基本都是在ASCII的基础上进行的。各字符集虽然都能够兼容标准ASCII码,但在使用交流上的不便是显而易见的,乱码也是随处可见。为了解决这种各自为战的问题,Unicode字符集就诞生了。UnicodeUnicode是国际组织制定的,用于收纳世界上所有文字和符号的字符集方案。前128个字符同ASCII一样,进行扩充后,使用数字0-0x10FFFF来映射这些字符,最多可以有1114112个字符。目前仍然只使用了其中的一小部分。Unicode一般使用两个字节来表示一个字符。码点 Unicode 规定了每个字符的数字编号,这个编号被称为 码点(code point)。码点以 U+hex 的形式表示,U+是代表Unicode的前缀,而 hex 是一个16进制数。取值范围是从 U+0000 到 U+10FFFF。每个码点对应一个字符,绝大部分的常见字符在最前面的 65536 个字符,范围是 U+0000到U+FFFF。一般汉字的码点区间为 U+2E80 - U+9FFF。字符平面 目前的Unicode分成了17个编组,也称平面,每个平面有65536个码点。第一个平面是基本多语言平面,范围:U+0000 - U+FFFF,多数常见字符都在该区间。其他平面则为辅助平面,范围:U+10000 到 U+10FFFF,如我们在网上常见 Emoji 表情。码元 码元(Code Unit)可以理解为对码点进行编码时的最小基本单元,码元是一个整体。而字符编码的作用就是将Unicode码点转换成码元序列。Unicode常用的编码方式有 UTF-8 、UTF-16 和 UTF-32,UTF是Unicode TransferFormat的缩写。UTF-8是8位的单字节码元,UTF-16是16位的双字节码元,UTF-32是32位的四字节码元。另外,为什么总看到使用十六进制数据来表示如码点等各种数据呢?因为,两位的十六进制正好等于一个字节8位,0xff = 0b11111111。UTF-8UTF-8是一种可变长度的字符编码方式。目前是使用 1 到 4 个字节来编码字符;是互联网时代应用最广的一种编码方式,前端接触的相对最多。需要注意的是:汉字一般占3个字节,表情符号一般占4个字节。UTF-8的编码规则:1个字节的字符,第一位为0,后7位为码点,与ASCII相同。n个字节的字符,第一个字节前面 n 位都是1,n+1位是0,可据此判断有几个字节。后面的几个字节都是 10 为开头2位。 这里规定的都是前缀,对于字符的码点,需要进行截取后依次放入除前缀外的其他位,所以UTF-8又被称为前缀码。格式如表:通过上表的编码规则,我们就可以进行各种转换了。下面,我们以一个中文字符的编码转换为例,如汉字 '好':'好'的Unicode码点:'好'.codePointAt() \\ 22909,结果是22909; 22909在UTF-8的3字节数的编码区间 U+0800 (2048) ~ U+FFFF (65535); 22909的二进制值:101100101111101,有15位; 而3字节数的编码需要16位,前面补0,根据表中规则分成3组:0101 100101 111101; 依次填入对应的前缀:11100101 10100101 10111101,得到3个字节; 将得到的三个字节转成十六进制数据:E5 A5 BD,所以汉字 '好' 的UTF-8就是:E5 A5 BD。我们使用 encodeURI 进行验证——encodeURI函数支持将中文进行 UTF-8 编码:encodeURI('好') // '%E5%A5%BD' 去除百分号,结果正好一致。UTF-16UTF-16的编码方式:基本平面的字符占用 2 个字节(U+0000到U+FFFF),辅助平面的字符占用 4 个字节(U+010000到U+10FFFF)。也就是说,UTF-16的编码长度要么是2个字节要么是4个字节。当为2字节时,则实际上与Unicode相同。并且还有个原则,在Unicode基本多语言平面内,从U+D800到U+DFFF之间的码点区间是不对应字符的。而UTF-16需要利用这块码位来对辅助平面的字符进行编码。它的具体规则:码点小于U+FFFF,基本字符,不需处理,直接使用,占两个字节。否则,拆分成两个码元,四个字节,cp表示码点:低位——((cp - 65536) / 1024) + 0xD800,值范围是 0xD800~0xDBFF;高位——((cp - 65536) % 1024) + 0xDC00,值范围是 0xDC00~0xDFFF。看下面的示例:汉字 '好','好'.codePointAt() // 22909,码点小于U+FFFF,直接进行十六进制转换:579D。表情符号 '',''.codePointAt() // 128516,码点需要拆分:低位:Math.floor(((128516 - 65536) / 1024)) + 0xD800 // 55357, 得到 D83D高位:((128516 - 65536) % 1024) + 0xDC00 // 56836,得到 DE04使用 String.fromCharCode 方法进行验证:String.fromCharCode(0xD83D, 0xDE04) // '' 需要明确的一点,Javascript中的字符串是基于UTF-16编码的,大端序字节。UTF-32是定长的编码,每个码位使用四个字节进行编码。优点是和unicode一一对应,缺点是太浪费空间。比较下面将选取字母、汉字、表情字符,进行编码对比查看:// UTF-8 'a': 97 - 0x61 '好': 22909 - (0xE5 0xA5 0xBD) '': 128516 - (0xF0 0x9F 0x98 0x84) // UTF-16 'a': 97 - 0x0061 '好': 22909 - 0x597d '': 128516 - (0xD83D, 0xDE04) 可以看到,UTF-8是变长1-4个字节,码元为8位;UTF-16是2或4字节,码元是16位。这里记住UTF-16的码元,对于我们理解下面的问题,比较有帮助。前端开发中的编码前面已提到过,javascript中的字符串是基于UTF-16编码的,所以在计算字符串长度时,我们需要先理解UTF-16编码。下面,我们看一下处理字符串时可能会遇到的问题。字符串长度计算字符串的length属性,实际上是使用UTF-16的码元个数来进行计算的:ASCII码和大部分中文,都是一个码元而表情字符和其他特殊字符都是两个码元所以,当某个字符中存在2个码元时,就算显示的是一个字符,length却等于2。'a'.length // 1 '好'.length // 1,多数汉字都是基本字符平面,只有一个码元,长度就为1。 ''.length // 2 组合字符的长度还有一种特殊的,组合字符,一般指一些带标点符号的字符:é。'é'.length // 2 'e\u0301'.length // 2 // 获取码点时,忽略了标点符号,显示的是字母的码点 'é'.codePointAt() // 101 'e'.codePointAt() // 101 如要正常操作组合字符,使用normalize()。'é'.normalize().length = 1。 多码元字符操作对于多码元字符使用下标取值时,得到的将是它的码元:''[0] // '\uD83D' ''[1] // '\uDE04' '123'[0] // '1' 循环时,使用 for 会乱码,而 for-of 则正常:let smile = '' for(let i = 0; i < smile.length; i++) { console.log(smile[i]) } // � // � for (let tt of smile) { console.log(tt) } // 但是,可以使用转换成扩展数组的方式访问:[...''][0] // '' Array.from('') // [''] 还可以使用码点的方式:String.fromCodePoint(''.codePointAt()) // '' 对于这种特殊字符,使用下面的字符串方法都会分割码元:split(),slice(),charAt(),charCodeAt(),substr(),substring()。''.slice(0, 2) // '' ''.slice(0, 1) // '\uD83D' ''.slice(1, 2) // '\uDE04' ''.substr(0,1) // '\uD83D' ''.substr(0,2) // '' ''.split('') // ['\uD83D', '\uDE04'] 正则中的 u 修饰符ES6在正则中添加了u修饰符,用来正确处理大于\uFFFF的 Unicode 字符,也就是能够正确处理四个字节的 UTF-16 编码。/^\S$/.test('') // false /^\S$/u.test('') // true 但对组合字符,u修饰符不起作用:/^\S$/u.test('é') // false /^\S$/u.test('e\u0301') // false 转义字符我们还需要注意的,是转义字符的计算,结果会以实际字符为准:'\x3f'.length // 1 '?'.length // 1 读取操作时,也能正常处理:'\x3f'[0] // '?' '\x3f'.split('') // ['?'] 常用API前端在对Unicode编码处理时,提供了一些可以使用的API,在实际工作中,会方便我们处理这方面的问题。处理码点和字符charAt(index) 从一个字符串中返回指定的字符,对于多码元字符,仍会返回码元字符:'a'.charAt() // 'a' ''.charAt() // '\uD83D' ''.charAt(1) // '\uDE04' charCodeAt(index) 返回0到65535之间的整数码点值。对于多码元如果字符的码点大于U+FFFF,则返回第一个码元值,还可以加索引参数取后面码元的值。codePointAt(pos) 返回Unicode码点,多码元也能返回完整的码点值。codePointAt可以传入索引参数,对多码元字符取第二个码元值。// 小于 U+FFFF '好'.codePointAt() // 22909 '好'.charCodeAt() // 22909 // 大于 U+FFFF ''.charCodeAt() // 55357 ''.charCodeAt(1) // 56836 ''.codePointAt() // 128516 ''.codePointAt(1) // 56836 String.fromCharCode(num1[, ...[, numN]]) 返回由指定的UTF-16码点序列创建的字符串。参数范围0到65535,大于65535的数据将被截断,结果不准确。对于多码元字符,则会将两个码元组合得到该字符。String.fromCodePoint(num1[, ...[, numN]]) 返回使用指定的代码点序列创建的字符串。可以处理多码元字符的完整码点值。String.fromCharCode(55357, 56836, 123) // '{' String.fromCodePoint(128516, 123, 8776) // '{≈' TextEncoderTextEncoder,使用 UTF-8 编码将代码点流转换成字节流。TextDecoder:解码。默认编码方式就是UTF-8,可以解决字符转UTF-8编码的问题。const txtEn = new TextEncoder() const enVal = txtEn.encode('好') // Uint8Array(3) [229, 165, 189] const txtDe = new TextDecoder() txtDe.decode(enVal) // '好' IE不支持。String.prototype.normalize()对于语调符号和重音符号,Unicode提供了两种方法,一种是直接提供带符号的字符,如 é (码点233);另一种是组合字符,如上文提到的 é (码点101)。针对这种码点不同,但实质一样的字符,Javascript识别不了:'é' === 'é' // false 而 normalize() 方法的引入,正是为了解决这一问题,它会按照一定的方式将字符的不同表示方法统一为标准形式:'é' === 'é'.normalize() // true URL的UTF8编解码另外,在前端常接触的网页中,URL链接编码也是非常常见的。诸如:'http%3A%2F%2Fbaidu.com%2F%E4%B8%AD%E5%9B%BD'。这里面涉及到的就是关于UTF-8的编码。而JavaScript提供了四个URL的编码/解码方法,可以用于将非ASCII码的字符,如中文字符、特殊字符、表情字符等,进行UTF-8的编解码操作:encodeURI() 和 encodeURIComponent()decodeURI() 和 decodeURIComponent()他们的短处也很明显,对ASCII字符如英文数字等字符无法处理。这里的转换方式:先转为UTF-8的字节码,然后前面加个 % 进行拼接得到编码结果。encodeURI('好') // '%E5%A5%BD' decodeURI('%E5%A5%BD') // '好' encodeURIComponent('好') // '%E5%A5%BD' decodeURIComponent('%E5%A5%BD') // '好' encodeURI('hello') // 'hello' encodeURIComponent('hello') // 'hello' encodeURIComponent('') // '%F0%9F%98%84' encodeURI和encodeURIComponent的区别这两者的不同之处,在于对部分URL元字符符号的处理上。URL元字符:分号(;),逗号(’,’),斜杠(/),问号(?),冒号(:),at(@),&,等号(=),加号(+),美元符号($),井号(#)。encodeURIComponent会对这些URL元字符进行编码,但是encodeURI则不会:encodeURIComponent(';,/@&=') // '%3B%2C%2F%40%26%3D' encodeURI(';,/@&=') // ';,/@&='END作者:土豆居士来源:一口Linux版权归原作者所有,如有侵权,请联系删除。
2023年12月05日
4 阅读
0 评论
0 点赞
2023-11-29
关于MySQL的66个问题
关于MySQL的66个问题SQL基础掌握不错的小伙伴可以跳过这一部分。当然,可能会现场写一些SQL语句,SQ语句可以通过牛客、LeetCode、LintCode之类的网站来练习。1. 什么是内连接、外连接、交叉连接、笛卡尔积呢?内连接(inner join):取得两张表中满足存在连接匹配关系的记录。外连接(outer join):不只取得两张表中满足存在连接匹配关系的记录,还包括某张表(或两张表)中不满足匹配关系的记录。交叉连接(cross join):显示两张表所有记录一一对应,没有匹配关系进行筛选,它是笛卡尔积在SQL中的实现,如果A表有m行,B表有n行,那么A和B交叉连接的结果就有m*n行。笛卡尔积:是数学中的一个概念,例如集合A={a,b},集合B={1,2,3},那么A✖️B={<a,o>,<a,1>,<a,2>,<b,0>,<b,1>,<b,2>,}。2. 那MySQL 的内连接、左连接、右连接有有什么区别?MySQL的连接主要分为内连接和外连接,外连接常用的有左连接、右连接。 inner join 内连接,在两张表进行连接查询时,只保留两张表中完全匹配的结果集left join 在两张表进行连接查询时,会返回左表所有的行,即使在右表中没有匹配的记录。right join 在两张表进行连接查询时,会返回右表所有的行,即使在左表中没有匹配的记录。3.说一下数据库的三大范式?第一范式:数据表中的每一列(每个字段)都不可以再拆分。 例如用户表,用户地址还可以拆分成国家、省份、市,这样才是符合第一范式的。第二范式:在第一范式的基础上,非主键列完全依赖于主键,而不能是依赖于主键的一部分。 例如订单表里,存储了商品信息(商品价格、商品类型),那就需要把商品ID和订单ID作为联合主键,才满足第二范式。第三范式:在满足第二范式的基础上,表中的非主键只依赖于主键,而不依赖于其他非主键。 例如订单表,就不能存储用户信息(姓名、地址)。三大范式的作用是为了控制数据库的冗余,是对空间的节省,实际上,一般互联网公司的设计都是反范式的,通过冗余一些数据,避免跨表跨库,利用空间换时间,提高性能。4.varchar与char的区别?char:char表示定长字符串,长度是固定的;如果插入数据的长度小于char的固定长度时,则用空格填充;因为长度固定,所以存取速度要比varchar快很多,甚至能快50%,但正因为其长度固定,所以会占据多余的空间,是空间换时间的做法;对于char来说,最多能存放的字符个数为255,和编码无关varchar:varchar表示可变长字符串,长度是可变的;插入的数据是多长,就按照多长来存储;varchar在存取方面与char相反,它存取慢,因为长度不固定,但正因如此,不占据多余的空间,是时间换空间的做法;对于varchar来说,最多能存放的字符个数为65535日常的设计,对于长度相对固定的字符串,可以使用char,对于长度不确定的,使用varchar更合适一些。拓展:💻1、char的长度是不可变的,而varchar的长度是可变的例如值:abc类型char(10),存储值为:abc_______(abc+7个空格)类型varchar(10), 存储值为:abc (自动缩短为3个字母的长度,节省存储空间)🌈💻2、因为char类型长度固定,因此char的存取效率比varchar要快得多,方便程序的存储与查找。但是char也为此付出的是空间的代价,因为其长度固定,所以会占据多余的空间,可谓是以空间换取时间效率。而varchar则刚好相反,节省空间但存取效率相对较低。🌈💻3、存储的容量不同char类型最多可存放255个字符,并且和编码类型无关varchar类型复杂一些:varchar的最大长度为65535个字节,varchar可存放的字符数因编码类型不同而异👉字符类型若为gbk,每个字符最多占2个字节,最大长度不能超过32766个字符👉字符类型若为utf8,每个字符最多占3个字节,最大长度不能超过21845个字符🌈💻使用建议👉若字段长度固定,则可以使用char来保存,存取效率高,如:手机号、身份证👉若字段长度经常改变,可选用varchar来最大化利用存储空间,减少空间浪费。🌈💻扩展👉MySQL4.0版本及以下,varchar长度是按字节展示的,如varchar(10),指的是10字节;👉MySQL5.0版本及以上,varchar长度则按字符展示,如varchar(10),指的是10字符。MySQL varchar(n)能存储几个汉字5.blob和text有什么区别?blob用于存储二进制数据,而text用于存储大字符串。blob没有字符集,text有一个字符集,并且根据字符集的校对规则对值进行排序和比较6.DATETIME和TIMESTAMP的异同?相同点:两个数据类型存储时间的表现格式一致。均为 YYYY-MM-DD HH:MM:SS两个数据类型都包含「日期」和「时间」部分。两个数据类型都可以存储微秒的小数秒(秒后6位小数秒)区别: 日期范围:DATETIME 的日期范围是 1000-01-01 00:00:00.000000 到 9999-12-31 23:59:59.999999;TIMESTAMP 的时间范围是1970-01-01 00:00:01.000000 UTC到 `2038-01-09 03:14:07.999999` UTC存储空间:DATETIME 的存储空间为 8 字节;TIMESTAMP 的存储空间为 4 字节时区相关:DATETIME 存储时间与时区无关;TIMESTAMP 存储时间与时区有关,显示的值也依赖于时区默认值:DATETIME 的默认值为 null;TIMESTAMP 的字段默认不为空(not null),默认值为当前时间(CURRENT_TIMESTAMP)7.MySQL中 in 和 exists 的区别?MySQL中的in语句是把外表和内表作hash 连接,而exists语句是对外表作loop循环,每次loop循环再对内表进行查询。我们可能认为exists比in语句的效率要高,这种说法其实是不准确的,要区分情景:如果查询的两个表大小相当,那么用in和exists差别不大。如果两个表中一个较小,一个是大表,则子查询表大的用exists,子查询表小的用in。not in 和not exists:如果查询语句使用了not in,那么内外表都进行全表扫描,没有用到索引;而 not extsts 的子查询依然能用到表上的索引。所以无论那个表大,用not exists都比not in要快。8.MySQL里记录货币用什么字段类型比较好?货币在数据库中MySQL常用Decimal和Numric类型表示,这两种类型被MySQL实现为同样的类型。他们被用于保存与货币有关的数据。 例如salary DECIMAL(9,2),9(precision)代表将被用于存储值的总的小数位数,而2(scale)代表将被用于存储小数点后的位数。存储在salary列中的值的范围是从-9999999.99到9999999.99。 DECIMAL和NUMERIC值作为字符串存储,而不是作为二进制浮点数,以便保存那些值的小数精度。 之所以不使用float或者double的原因:因为float和double是以二进制存储的,所以有一定的误差。9.MySQL怎么存储emoji ?MySQL可以直接使用字符串存储emoji。 但是需要注意的,utf8 编码是不行的,MySQL中的utf8是阉割版的 utf8,它最多只用 3 个字节存储字符,所以存储不了 表情 。那该怎么办? 需要使用utf8mb4编码。 alter table blogs modify content text CHARACTER SET utf8mb4 COLLATE utf8mb4\_unicode\_ci not null;拓展:表情占四个字节 --> 编程中常用的字符编码知识点10.drop、delete与truncate的区别?三者都表示删除,但是三者有一些差别: 因此,在不再需要一张表的时候,用drop;在想删除部分数据行时候,用delete;在保留表而删除所有数据的时候用truncate。11.UNION与UNION ALL的区别?如果使用UNION ALL,不会合并重复的记录行效率 UNION 高于 UNION ALL12.count(1)、count(*) 与 count(列名) 的区别?执行效果:count(*)包括了所有的列,相当于行数,在统计结果的时候,不会忽略列值为NULLcount(1)包括了忽略所有列,用1代表代码行,在统计结果的时候,不会忽略列值为NULLcount(列名)只包括列名那一列,在统计结果的时候,会忽略列值为空(这里的空不是只空字符串或者0,而是表示null)的计数,即某个字段值为NULL时,不统计。执行速度:列名为主键,count(列名)会比count(1)快列名不为主键,count(1)会比count(列名)快如果表多个列并且没有主键,则 count(1) 的执行效率优于 count(*)如果有主键,则 select count(主键)的执行效率是最优的如果表只有一个字段,则 select count(*)最优。13.一条SQL查询语句的执行顺序?FROM:对FROM子句中的左表和右表执行笛卡儿积(Cartesianproduct),产生虚拟表VT1ON:对虚拟表VT1应用ON筛选,只有那些符合<join_condition>的行才被插入虚拟表VT2中JOIN:如果指定了OUTER JOIN(如LEFT OUTER JOIN、RIGHT OUTER JOIN),那么保留表中未匹配的行作为外部行添加到虚拟表VT2中,产生虚拟表VT3。如果FROM子句包含两个以上表,则对上一个连接生成的结果表VT3和下一个表重复执行步骤1)~步骤3),直到处理完所有的表为止WHERE:对虚拟表VT3应用WHERE过滤条件,只有符合<where_condition>的记录才被插入虚拟表VT4中GROUP BY:根据GROUP BY子句中的列,对VT4中的记录进行分组操作,产生VT5CUBE|ROLLUP:对表VT5进行CUBE或ROLLUP操作,产生表VT6HAVING:对虚拟表VT6应用HAVING过滤器,只有符合<having_condition>的记录才被插入虚拟表VT7中。SELECT:第二次执行SELECT操作,选择指定的列,插入到虚拟表VT8中DISTINCT:去除重复数据,产生虚拟表VT9ORDER BY:将虚拟表VT9中的记录按照进行排序操作,产生虚拟表VT10。11)LIMIT:取出指定行的记录,产生虚拟表VT11,并返回给查询用户数据库架构14.说说 MySQL 的基础架构?MySQL逻辑架构图主要分三层:客户端:最上层的服务并不是MySQL所独有的,大多数基于网络的客户端/服务器的工具或者服务都有类似的架构。比如连接处理、授权认证、安全等等。Server层:大多数MySQL的核心服务功能都在这一层,包括查询解析、分析、优化、缓存以及所有的内置函数(例如,日期、时间、数学和加密函数),所有跨存储引擎的功能都在这一层实现:存储过程、触发器、视图等。存储引擎层:第三层包含了存储引擎。存储引擎负责MySQL中数据的存储和提取。Server层通过API与存储引擎进行通信。这些接口屏蔽了不同存储引擎之间的差异,使得这些差异对上层的查询过程透明。15.一条 SQL 查询语句在 MySQL 中如何执行的?先检查该语句是否有权限,如果没有权限,直接返回错误信息,如果有权限会先查询缓存 (MySQL8.0 版本以前)。如果没有缓存,分析器进行语法分析,提取 sql 语句中 select 等关键元素,然后判断 sql 语句是否有语法错误,比如关键词是否正确等等。语法解析之后,MySQL的服务器会对查询的语句进行优化,确定执行的方案。完成查询优化后,按照生成的执行计划调用数据库引擎接口,返回执行结果。存储引擎16.MySQL有哪些常见存储引擎?主要存储引擎以及功能如下: MySQL5.5之前,默认存储引擎是MylSAM,5.5之后变成了InnoDB。 InnoDB支持的哈希索引是自适应的,InnoDB会根据表的使用情况自动为表生成哈希索引,不能人为干预是否在一张表中生成哈希索引。 MySQL 5.6开始InnoDB支持全文索引。17.那存储引擎应该怎么选择?大致上可以这么选择:大多数情况下,使用默认的InnoDB就够了。如果要提供提交、回滚和恢复的事务安全(ACID 兼容)能力,并要求实现并发控制,InnoDB 就是比较靠前的选择了。如果数据表主要用来插入和查询记录,则 MyISAM 引擎提供较高的处理效率。如果只是临时存放数据,数据量不大,并且不需要较高的数据安全性,可以选择将数据保存在内存的 MEMORY 引擎中,MySQL 中使用该引擎作为临时表,存放查询的中间结果。使用哪一种引擎可以根据需要灵活选择,因为存储引擎是基于表的,所以一个数据库中多个表可以使用不同的引擎以满足各种性能和实际需求。使用合适的存储引擎将会提高整个数据库的性能。18.InnoDB和MylSAM主要有什么区别?PS:MySQL8.0都开始慢慢流行了,如果不是面试,MylSAM其实可以不用怎么了解。 存储结构:每个MyISAM在磁盘上存储成三个文件;InnoDB所有的表都保存在同一个数据文件中(也可能是多个文件,或者是独立的表空间文件),InnoDB表的大小只受限于操作系统文件的大小,一般为2GB。事务支持:MyISAM不提供事务支持;InnoDB提供事务支持事务,具有事务(commit)、回滚(rollback)和崩溃修复能力(crash recovery capabilities)的事务安全特性。最小锁粒度:MyISAM只支持表级锁,更新时会锁住整张表,导致其它查询和更新都会被阻塞InnoDB支持行级锁。索引类型:MyISAM的索引为聚簇索引,数据结构是B树;InnoDB的索引是非聚簇索引,数据结构是B+树。主键必需:MyISAM允许没有任何索引和主键的表存在;InnoDB如果没有设定主键或者非空唯一索引,就会自动生成一个6字节的主键(用户不可见) ,数据是主索引的一部分,附加索引保存的是主索引的值。表的具体行数:MyISAM保存了表的总行数,如果select count(_) from table;会直接取出出该值; InnoDB没有保存表的总行数,如果使用select count(_) from table;就会遍历整个表;但是在加了wehre条件后,MyISAM和InnoDB处理的方式都一样。外键支持:MyISAM不支持外键;InnoDB支持外键。 日志19.MySQL日志文件有哪些?分别介绍下作用?MySQL日志文件有很多,包括 :错误日志(error log):错误日志文件对MySQL的启动、运行、关闭过程进行了记录,能帮助定位MySQL问题。慢查询日志(slow query log):慢查询日志是用来记录执行时间超过 long\_query\_time 这个变量定义的时长的查询语句。通过慢查询日志,可以查找出哪些查询语句的执行效率很低,以便进行优化。一般查询日志(general log):一般查询日志记录了所有对MySQL数据库请求的信息,无论请求是否正确执行。二进制日志(bin log):关于二进制日志,它记录了数据库所有执行的DDL和DML语句(除了数据查询语句select、show等),以事件形式记录并保存在二进制文件中。还有两个InnoDB存储引擎特有的日志文件:重做日志(redo log):重做日志至关重要,因为它们记录了对于InnoDB存储引擎的事务日志。回滚日志(undo log):回滚日志同样也是InnoDB引擎提供的日志,顾名思义,回滚日志的作用就是对数据进行回滚。当事务对数据库进行修改,InnoDB引擎不仅会记录redo log,还会生成对应的undo log日志;如果事务执行失败或调用了rollback,导致事务需要回滚,就可以利用undo log中的信息将数据回滚到修改之前的样子。20.binlog和redo log有什么区别?bin log会记录所有与数据库有关的日志记录,包括InnoDB、MyISAM等存储引擎的日志,而redo log只记InnoDB存储引擎的日志。记录的内容不同,bin log记录的是关于一个事务的具体操作内容,即该日志是逻辑日志。而redo log记录的是关于每个页(Page)的更改的物理情况。写入的时间不同,bin log仅在事务提交前进行提交,也就是只写磁盘一次。而在事务进行的过程中,却不断有redo ertry被写入redo log中。写入的方式也不相同,redo log是循环写入和擦除,bin log是追加写入,不会覆盖已经写的文件。21.一条更新语句怎么执行的了解吗?更新语句的执行是Server层和引擎层配合完成,数据除了要写入表中,还要记录相应的日志。 执行器先找引擎获取ID=2这一行。ID是主键,存储引擎检索数据,找到这一行。如果ID=2这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。执行器拿到引擎给的行数据,把这个值加上1,比如原来是N,现在就是N+1,得到新的一行数据,再调用引擎接口写入这行新数据。引擎将这行新数据更新到内存中,同时将这个更新操作记录到redo log里面,此时redo log处于prepare状态。然后告知执行器执行完成了,随时可以提交事务。执行器生成这个操作的binlog,并把binlog写入磁盘。执行器调用引擎的提交事务接口,引擎把刚刚写入的redo log改成提交(commit)状态,更新完成。从上图可以看出,MySQL在执行更新语句的时候,在服务层进行语句的解析和执行,在引擎层进行数据的提取和存储;同时在服务层对binlog进行写入,在InnoDB内进行redo log的写入。 不仅如此,在对redo log写入时有两个阶段的提交,一是binlog写入之前prepare状态的写入,二是binlog写入之后commit状态的写入。22.那为什么要两阶段提交呢?为什么要两阶段提交呢?直接提交不行吗? 我们可以假设不采用两阶段提交的方式,而是采用“单阶段”进行提交,即要么先写入redo log,后写入binlog;要么先写入binlog,后写入redo log。这两种方式的提交都会导致原先数据库的状态和被恢复后的数据库的状态不一致。 先写入redo log,后写入binlog: 在写完redo log之后,数据此时具有crash-safe能力,因此系统崩溃,数据会恢复成事务开始之前的状态。但是,若在redo log写完时候,binlog写入之前,系统发生了宕机。此时binlog没有对上面的更新语句进行保存,导致当使用binlog进行数据库的备份或者恢复时,就少了上述的更新语句。从而使得id=2这一行的数据没有被更新。 先写入binlog,后写入redo log: 写完binlog之后,所有的语句都被保存,所以通过binlog复制或恢复出来的数据库中id=2这一行的数据会被更新为a=1。但是如果在redo log写入之前,系统崩溃,那么redo log中记录的这个事务会无效,导致实际数据库中id=2这一行的数据并没有更新。 简单说,redo log和binlog都可以用于表示事务的提交状态,而两阶段提交就是让这两个状态保持逻辑上的一致。23.redo log怎么刷入磁盘的知道吗?redo log的写入不是直接落到磁盘,而是在内存中设置了一片称之为redo log buffer的连续内存空间,也就是redo 日志缓冲区。 什么时候会刷入磁盘? 在如下的一些情况中,log buffer的数据会刷入磁盘:log buffer 空间不足时log buffer 的大小是有限的,如果不停的往这个有限大小的 log buffer 里塞入日志,很快它就会被填满。如果当前写入 log buffer 的redo 日志量已经占满了 log buffer 总容量的大约一半左右,就需要把这些日志刷新到磁盘上。事务提交时在事务提交时,为了保证持久性,会把log buffer中的日志全部刷到磁盘。注意,这时候,除了本事务的,可能还会刷入其它事务的日志。后台线程输入有一个后台线程,大约每秒都会刷新一次log buffer中的redo log到磁盘。正常关闭服务器时触发checkpoint规则重做日志缓存、重做日志文件都是以块(block) 的方式进行保存的,称之为重做日志块(redo log block) ,块的大小是固定的512字节。我们的redo log它是固定大小的,可以看作是一个逻辑上的 log group,由一定数量的log block 组成。 它的写入方式是从头到尾开始写,写到末尾又回到开头循环写。 其中有两个标记位置: write pos是当前记录的位置,一边写一边后移,写到第3号文件末尾后就回到0号文件开头。checkpoint是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到磁盘。 当write_pos追上checkpoint时,表示redo log日志已经写满。这时候就不能接着往里写数据了,需要执行checkpoint规则腾出可写空间。 所谓的checkpoint规则,就是checkpoint触发后,将buffer中日志页都刷到磁盘。 SQL 优化24.慢SQL如何定位呢?慢SQL的监控主要通过两个途径: 慢查询日志:开启MySQL的慢查询日志,再通过一些工具比如mysqldumpslow去分析对应的慢查询日志,当然现在一般的云厂商都提供了可视化的平台。服务监控:可以在业务的基建中加入对慢SQL的监控,常见的方案有字节码插桩、连接池扩展、ORM框架过程,对服务运行中的慢SQL进行监控和告警。25.有哪些方式优化慢SQL?慢SQL的优化,主要从两个方面考虑,SQL语句本身的优化,以及数据库设计的优化。 避免不必要的列 这个是老生常谈,但还是经常会出的情况,SQL查询的时候,应该只查询需要的列,而不要包含额外的列,像slect * 这种写法应该尽量避免。 分页优化 在数据量比较大,分页比较深的情况下,需要考虑分页的优化。 例如: select * from table where type = 2 and level = 9 order by id asc limit 190289,10; 优化方案:延迟关联 先通过where条件提取出主键,在将该表与原数据表关联,通过主键id提取数据行,而不是通过原来的二级索引提取数据行 例如: select a.* from table a, (select id from table where type = 2 and level = 9 order by id asc limit 190289,10 ) b where a.id = b.id书签方式 书签方式就是找到limit第一个参数对应的主键值,根据这个主键值再去过滤并limit 例如:select from table where id > (select from table where type = 2 and level = 9 order by id asc limit 190 索引优化 合理地设计和使用索引,是优化慢SQL的利器。 利用覆盖索引 InnoDB使用非主键索引查询数据时会回表,但是如果索引的叶节点中已经包含要查询的字段,那它没有必要再回表查询了,这就叫覆盖索引 例如对于如下查询: select name from test where city='上海' 我们将被查询的字段建立到联合索引中,这样查询结果就可以直接从索引中获取 alter table test add index idx\_city\_name (city, name); 低版本避免使用or查询 在 MySQL 5.0 之前的版本要尽量避免使用 or 查询,可以使用 union 或者子查询来替代,因为早期的 MySQL 版本使用 or 查询可能会导致索引失效,高版本引入了索引合并,解决了这个问题。 避免使用 != 或者 <\> 操作符 SQL中,不等于操作符会导致查询引擎放弃查询索引,引起全表扫描,即使比较的字段上有索引 解决方法:通过把不等于操作符改成or,可以使用索引,避免全表扫描 例如,把column<>’aaa’,改成column>’aaa’ or column<’aaa’,就可以使用索引了 适当使用前缀索引 适当地使用前缀所云,可以降低索引的空间占用,提高索引的查询效率。 比如,邮箱的后缀都是固定的“@xxx.com”,那么类似这种后面几位为固定值的字段就非常适合定义为前缀索引 alter table test add index index2(email(6)); PS:需要注意的是,前缀索引也存在缺点,MySQL无法利用前缀索引做order by和group by 操作,也无法作为覆盖索引 避免列上函数运算 要避免在列字段上进行算术运算或其他表达式运算,否则可能会导致存储引擎无法正确使用索引,从而影响了查询的效率 select from test where id + 1 = 50; select from test where month(updateTime) = 7; 正确使用联合索引 使用联合索引的时候,注意最左匹配原则。 JOIN优化 优化子查询 尽量使用 Join 语句来替代子查询,因为子查询是嵌套查询,而嵌套查询会新创建一张临时表,而临时表的创建与销毁会占用一定的系统资源以及花费一定的时间,同时对于返回结果集比较大的子查询,其对查询性能的影响更大 小表驱动大表 关联查询的时候要拿小表去驱动大表,因为关联的时候,MySQL内部会遍历驱动表,再去连接被驱动表。 比如left join,左表就是驱动表,A表小于B表,建立连接的次数就少,查询速度就被加快了。 select name from A left join B ; 适当增加冗余字段 增加冗余字段可以减少大量的连表查询,因为多张表的连表查询性能很低,所有可以适当的增加冗余字段,以减少多张表的关联查询,这是以空间换时间的优化策略 避免使用JOIN关联太多的表 《阿里巴巴Java开发手册》规定不要join超过三张表,第一join太多降低查询的速度,第二join的buffer会占用更多的内存。 如果不可避免要join多张表,可以考虑使用数据异构的方式异构到ES中查询。 排序优化 利用索引扫描做排序 MySQL有两种方式生成有序结果:其一是对结果集进行排序的操作,其二是按照索引顺序扫描得出的结果自然是有序的 但是如果索引不能覆盖查询所需列,就不得不每扫描一条记录回表查询一次,这个读操作是随机IO,通常会比顺序全表扫描还慢 因此,在设计索引时,尽可能使用同一个索引既满足排序又用于查找行 例如: --建立索引(date,staff\_id,customer\_id) select staff\_id, customer\_id from test where date = '2010-01-01' order by staff\_id,customer\_id; 只有当索引的列顺序和ORDER BY子句的顺序完全一致,并且所有列的排序方向都一样时,才能够使用索引来对结果做排序 UNION优化 条件下推 MySQL处理union的策略是先创建临时表,然后将各个查询结果填充到临时表中最后再来做查询,很多优化策略在union查询中都会失效,因为它无法利用索引 最好手工将where、limit等子句下推到union的各个子查询中,以便优化器可以充分利用这些条件进行优化 此外,除非确实需要服务器去重,一定要使用union all,如果不加all关键字,MySQL会给临时表加上distinct选项,这会导致对整个临时表做唯一性检查,代价很高。26.怎么看执行计划(explain),如何理解其中各个字段的含义?explain是sql优化的利器,除了优化慢sql,平时的sql编写,也应该先explain,查看一下执行计划,看看是否还有优化的空间。 直接在 select 语句之前增加explain关键字,就会返回执行计划的信息。 id 列:MySQL会为每个select语句分配一个唯一的id值select_type 列,查询的类型,根据关联、union、子查询等等分类,常见的查询类型有SIMPLE、PRIMARY。table 列:表示 explain 的一行正在访问哪个表。type 列:最重要的列之一。表示关联类型或访问类型,即 MySQL 决定如何查找表中的行。 性能从最优到最差分别为:system > const > eq\_ref > ref > fulltext > ref\_or\_null > index\_merge > unique\_subquery > index\_subquery > range > index > ALLsystem system: 当表仅有一行记录时(系统表),数据量很少,往往不需要进行磁盘IO,速度非常快const const:表示查询时命中 primary key 主键或者 unique 唯一索引,或者被连接的部分是一个常量(const)值。这类扫描效率极高,返回数据量少,速度非常快。eq_ref eq_ref:查询时命中主键primary key 或者 unique key索引, type 就是 eq_ref。ref\_or\_null ref_or_null:这种连接类型类似于 ref,区别在于 MySQL会额外搜索包含NULL值的行。index_merge index_merge:使用了索引合并优化方法,查询使用了两个以上的索引。unique_subquery unique_subquery:替换下面的 IN子查询,子查询返回不重复的集合。index_subquery index_subquery:区别于unique_subquery,用于非唯一索引,可以返回重复值。range range:使用索引选择行,仅检索给定范围内的行。简单点说就是针对一个有索引的字段,给定范围检索数据。在where语句中使用 bettween...and、<、>、<=、in 等条件查询 type 都是 range。index index:Index 与ALL 其实都是读全表,区别在于index是遍历索引树读取,而ALL是从硬盘中读取。ALL 就不用多说了,全表扫描。possible_keys 列:显示查询可能使用哪些索引来查找,使用索引优化sql的时候比较重要。key 列:这一列显示 mysql 实际采用哪个索引来优化对该表的访问,判断索引是否失效的时候常用。key_len 列:显示了 MySQL使用ref 列:ref 列展示的就是与索引列作等值匹配的值,常见的有:const(常量),func,NULL,字段名。rows 列:这也是一个重要的字段,MySQL查询优化器根据统计信息,估算SQL要查到结果集需要扫描读取的数据行数,这个值非常直观显示SQL的效率好坏,原则上rows越少越好。Extra 列:显示不适合在其它列的额外信息,虽然叫额外,但是也有一些重要的信息:Using index:表示MySQL将使用覆盖索引,以避免回表Using where:表示会在存储引擎检索之后再进行过滤Using temporary :表示对查询结果排序时会使用一个临时表。索引 索引可以说是MySQL面试中的重中之重,一定要彻底拿下。27.能简单说一下索引的分类吗?从三个不同维度对索引分类: 例如从基本使用使用的角度来讲:主键索引: InnoDB主键是默认的索引,数据列不允许重复,不允许为NULL,一个表只能有一个主键。唯一索引: 数据列不允许重复,允许为NULL值,一个表允许多个列创建唯一索引。普通索引: 基本的索引类型,没有唯一性的限制,允许为NULL值。组合索引:多列值组成一个索引,用于组合搜索,效率大于索引合并28.为什么使用索引会加快查询?传统的查询方法,是按照表的顺序遍历的,不论查询几条数据,MySQL需要将表的数据从头到尾遍历一遍。 在我们添加完索引之后,MySQL一般通过BTREE算法生成一个索引文件,在查询数据库时,找到索引文件进行遍历,在比较小的索引数据里查找,然后映射到对应的数据,能大幅提升查找的效率。 和我们通过书的目录,去查找对应的内容,一样的道理。 29.创建索引有哪些注意点?索引虽然是sql性能优化的利器,但是索引的维护也是需要成本的,所以创建索引,也要注意:索引应该建在查询应用频繁的字段 在用于 where 判断、 order 排序和 join 的(on)字段上创建索引。索引的个数应该适量 索引需要占用空间;更新时候也需要维护。区分度低的字段,例如性别,不要建索引。 离散度太低的字段,扫描的行数降低的有限。频繁更新的值,不要作为主键或者索引 维护索引文件需要成本;还会导致页分裂,IO次数增多。组合索引把散列性高(区分度高)的值放在前面 为了满足最左前缀匹配原则创建组合索引,而不是修改单列索引。 组合索引代替多个单列索引(对于单列索引,MySQL基本只能使用一个索引,所以经常使用多个条件查询时更适合使用组合索引)过长的字段,使用前缀索引。 当字段值比较长的时候,建立索引会消耗很多的空间,搜索起来也会很慢。我们可以通过截取字段的前面一部分内容建立索引,这个就叫前缀索引。不建议用无序的值(例如身份证、UUID )作为索引 当主键具有不确定性,会造成叶子节点频繁分裂,出现磁盘存储的碎片化30.索引哪些情况下会失效呢?查询条件包含or,可能导致索引失效如果字段类型是字符串,where时一定用引号括起来,否则会因为隐式类型转换,索引失效like通配符可能导致索引失效。联合索引,查询时的条件列不是联合索引中的第一个列,索引失效。在索引列上使用mysql的内置函数,索引失效。对索引列运算(如,+、-、*、/),索引失效。索引字段上使用(!= 或者 < >,not in)时,可能会导致索引失效。索引字段上使用is null, is not null,可能导致索引失效。左连接查询或者右连接查询查询关联的字段编码格式不一样,可能导致索引失效。MySQL优化器估计使用全表扫描要比使用索引快,则不使用索引。31.索引不适合哪些场景呢?数据量比较少的表不适合加索引更新比较频繁的字段也不适合加索引离散低的字段不适合加索引(如性别)32.索引是不是建的越多越好呢?当然不是。索引会占据磁盘空间索引虽然会提高查询效率,但是会降低更新表的效率。比如每次对表进行增删改操作,MySQL不仅要保存数据,还有保存或者更新对应的索引文件。33.MySQL索引用的什么数据结构了解吗?MySQL的默认存储引擎是InnoDB,它采用的是B+树结构的索引。B+树:只有叶子节点才会存储数据,非叶子节点只存储键值。叶子节点之间使用双向指针连接,最底层的叶子节点形成了一个双向有序链表。在这张图里,有两个重点:最外面的方块,的块我们称之为一个磁盘块,可以看到每个磁盘块包含几个数据项(粉色所示)和指针(黄色/灰色所示),如根节点磁盘包含数据项17和35,包含指针P1、P2、P3,P1表示小于17的磁盘块,P2表示在17和35之间的磁盘块,P3表示大于35的磁盘块。真实的数据存在于叶子节点即3、4、5……、65。非叶子节点只不存储真实的数据,只存储指引搜索方向的数据项,如17、35并不真实存在于数据表中。叶子节点之间使用双向指针连接,最底层的叶子节点形成了一个双向有序链表,可以进行范围查询。34.那一棵B+树能存储多少条数据呢?假设索引字段是 bigint 类型,长度为 8 字节。指针大小在 InnoDB 源码中设置为 6 字节,这样一共 14 字节。非叶子节点(一页)可以存储 16384/14=1170 个这样的 单元(键值+指针),代表有 1170 个指针。 树深度为 2 的时候,有 1170^2 个叶子节点,可以存储的数据为 1170_1170_16=21902400。 在查找数据时一次页的查找代表一次 IO,也就是说,一张 2000 万左右的表,查询数据最多需要访问 3 次磁盘。 所以在 InnoDB 中 B+ 树深度一般为 1-3 层,它就能满足千万级的数据存储。35.为什么要用 B+ 树,而不用普通二叉树?可以从几个维度去看这个问题,查询是否够快,效率是否稳定,存储数据多少,以及查找磁盘次数。 为什么不用普通二叉树? 普通二叉树存在退化的情况,如果它退化成链表,相当于全表扫描。平衡二叉树相比于二叉查找树来说,查找效率更稳定,总体的查找速度也更快。 为什么不用平衡二叉树呢? 读取数据的时候,是从磁盘读到内存。如果树这种数据结构作为索引,那每查找一次数据就需要从磁盘中读取一个节点,也就是一个磁盘块,但是平衡二叉树可是每个节点只存储一个键值和数据的,如果是 B+ 树,可以存储更多的节点数据,树的高度也会降低,因此读取磁盘的次数就降下来啦,查询效率就快。36.为什么用 B+ 树而不用 B 树呢?B+相比较B树,有这些优势:它是 B Tree 的变种,B Tree 能解决的问题,它都能解决。 B Tree 解决的两大问题:每个节点存储更多关键字;路数更多扫库、扫表能力更强 如果我们要对表进行全表扫描,只需要遍历叶子节点就可以 了,不需要遍历整棵 B+Tree 拿到所有的数据。B+Tree 的磁盘读写能力相对于 B Tree 来说更强,IO次数更少 根节点和枝节点不保存数据区, 所以一个节点可以保存更多的关键字,一次磁盘加载的关键字更多,IO次数更少。排序能力更强 因为叶子节点上有下一个数据区的指针,数据形成了链表。效率更加稳定 B+Tree 永远是在叶子节点拿到数据,所以 IO 次数是稳定的。37.Hash 索引和 B+ 树索引区别是什么?B+ 树可以进行范围查询,Hash 索引不能。B+ 树支持联合索引的最左侧原则,Hash 索引不支持。B+ 树支持 order by 排序,Hash 索引不支持。Hash 索引在等值查询上比 B+ 树效率更高。B+ 树使用 like 进行模糊查询的时候,like 后面(比如 % 开头)的话可以起到优化的作用,Hash 索引根本无法进行模糊查询。38.聚簇索引与非聚簇索引的区别?首先理解聚簇索引不是一种新的索引,而是而是一种数据存储方式。 聚簇表示数据行和相邻的键值紧凑地存储在一起。我们熟悉的两种存储引擎——MyISAM采用的是非聚簇索引,InnoDB采用的是聚簇索引。 可以这么说:索引的数据结构是树,聚簇索引的索引和数据存储在一棵树上,树的叶子节点就是数据,非聚簇索引索引和数据不在一棵树上。一个表中只能拥有一个聚簇索引,而非聚簇索引一个表可以存在多个。聚簇索引,索引中键值的逻辑顺序决定了表中相应行的物理顺序;索引,索引中索引的逻辑顺序与磁盘上行的物理存储顺序不同。聚簇索引:物理存储按照索引排序;非聚集索引:物理存储不按照索引排序;39.回表了解吗?在InnoDB存储引擎里,利用辅助索引查询,先通过辅助索引找到主键索引的键值,再通过主键值查出主键索引里面没有符合要求的数据,它比基于主键索引的查询多扫描了一棵索引树,这个过程就叫回表。 例如:select * from user where name = ‘张三’; 40.覆盖索引了解吗?在辅助索引里面,不管是单列索引还是联合索引,如果 select 的数据列只用辅助索引中就能够取得,不用去查主键索引,这时候使用的索引就叫做覆盖索引,避免了回表。 比如,select name from user where name = ‘张三’; 41.什么是最左前缀原则/最左匹配原则?注意:最左前缀原则、最左匹配原则、最左前缀匹配原则这三个都是一个概念。 最左匹配原则:在InnoDB的联合索引中,查询的时候只有匹配了前一个/左边的值之后,才能匹配下一个。 根据最左匹配原则,我们创建了一个组合索引,如 (a1,a2,a3),相当于创建了(a1)、(a1,a2)和 (a1,a2,a3) 三个索引。 为什么不从最左开始查,就无法匹配呢? 比如有一个user表,我们给 name 和 age 建立了一个组合索引。 ALTER TABLE user add INDEX comidx\_name\_phone (name,age); 组合索引在 B+Tree 中是复合的数据结构,它是按照从左到右的顺序来建立搜索树的 (name 在左边,age 在右边)。 从这张图可以看出来,name 是有序的,age 是无序的。当 name 相等的时候, age 才是有序的。 这个时候我们使用where name= ‘张三‘ and age = ‘20 ‘去查询数据的时候, B+Tree 会优先比较 name 来确定下一步应该搜索的方向,往左还是往右。如果 name 相同的时候再比较age。但是如果查询条件没有 name,就不知道下一步应该查哪个 节点,因为建立搜索树的时候 name 是第一个比较因子,所以就没用上索引。42.什么是索引下推优化?索引条件下推优化(Index Condition Pushdown (ICP) )是MySQL5.6添加的,用于优化数据查询。不使用索引条件下推优化时存储引擎通过索引检索到数据,然后返回给MySQL Server,MySQL Server进行过滤条件的判断。当使用索引条件下推优化时,如果存在某些被索引的列的判断条件时,MySQL Server将这一部分判断条件下推给存储引擎,然后由存储引擎通过判断索引是否符合MySQL Server传递的条件,只有当索引符合条件时才会将数据检索出来返回给MySQL服务器。例如一张表,建了一个联合索引(name, age),查询语句:select * from t_user where name like '张%' and age=10;,由于name使用了范围查询,根据最左匹配原则: 不使用ICP,引擎层查找到name like '张%'的数据,再由Server层去过滤age=10这个条件,这样一来,就回表了两次,浪费了联合索引的另外一个字段age。 但是,使用了索引下推优化,把where的条件放到了引擎层执行,直接根据name like '张%' and age=10的条件进行过滤,减少了回表的次数。 索引条件下推优化可以减少存储引擎查询基础表的次数,也可以减少MySQL服务器从存储引擎接收数据的次数。 锁43.MySQL中有哪几种锁,列举一下?如果按锁粒度划分,有以下3种:表锁: 开销小,加锁快;锁定力度大,发生锁冲突概率高,并发度最低;不会出现死锁。行锁: 开销大,加锁慢;会出现死锁;锁定粒度小,发生锁冲突的概率低,并发度高。页锁: 开销和加锁速度介于表锁和行锁之间;会出现死锁;锁定粒度介于表锁和行锁之间,并发度一般如果按照兼容性,有两种,共享锁(S Lock),也叫读锁(read lock),相互不阻塞。排他锁(X Lock),也叫写锁(write lock),排它锁是阻塞的,在一定时间内,只有一个请求能执行写入,并阻止其它锁读取正在写入的数据。44.说说InnoDB里的行锁实现?我们拿这么一个用户表来表示行级锁,其中插入了4行数据,主键值分别是1,6,8,12,现在简化它的聚簇索引结构,只保留数据记录。 InnoDB的行锁的主要实现如下:Record Lock 记录锁记录锁就是直接锁定某行记录。当我们使用唯一性的索引(包括唯一索引和聚簇索引)进行等值查询且精准匹配到一条记录时,此时就会直接将这条记录锁定。例如select * from t where id =6 for update;就会将id=6的记录锁定。 Gap Lock 间隙锁间隙锁(Gap Locks) 的间隙指的是两个记录之间逻辑上尚未填入数据的部分,是一个左开右开空间。 间隙锁就是锁定某些间隙区间的。当我们使用用等值查询或者范围查询,并且没有命中任何一个record,此时就会将对应的间隙区间锁定。例如select * from t where id =3 for update;或者select * from t where id > 1 and id < 6 for update;就会将(1,6)区间锁定。Next-key Lock 临键锁临键指的是间隙加上它右边的记录组成的左开右闭区间。比如上述的(1,6]、(6,8]等。 临键锁就是记录锁(Record Locks)和间隙锁(Gap Locks)的结合,即除了锁住记录本身,还要再锁住索引之间的间隙。当我们使用范围查询,并且命中了部分record记录,此时锁住的就是临键区间。注意,临键锁锁住的区间会包含最后一个record的右边的临键区间。例如select * from t where id > 5 and id <= 7 for update;会锁住(4,7]、(7,+∞)。mysql默认行锁类型就是临键锁(Next-Key Locks)。当使用唯一性索引,等值查询匹配到一条记录的时候,临键锁(Next-Key Locks)会退化成记录锁;没有匹配到任何记录的时候,退化成间隙锁。 间隙锁(Gap Locks)和临键锁(Next-Key Locks)都是用来解决幻读问题的,在已提交读(READ COMMITTED)隔离级别下,间隙锁(Gap Locks)和临键锁(Next-Key Locks)都会失效! 上面是行锁的三种实现算法,除此之外,在行上还存在插入意向锁。Insert Intention Lock 插入意向锁一个事务在插入一条记录时需要判断一下插入位置是不是被别的事务加了意向锁 ,如果有的话,插入操作需要等待,直到拥有 gap锁 的那个事务提交。但是事务在等待的时候也需要在内存中生成一个 锁结构 ,表明有事务想在某个 间隙 中插入新记录,但是现在在等待。这种类型的锁命名为 Insert Intention Locks ,也就是插入意向锁 。 假如我们有个T1事务,给(1,6)区间加上了意向锁,现在有个T2事务,要插入一个数据,id为4,它会获取一个(1,6)区间的插入意向锁,又有有个T3事务,想要插入一个数据,id为3,它也会获取一个(1,6)区间的插入意向锁,但是,这两个插入意向锁锁不会互斥。 45.意向锁是什么知道吗?意向锁是一个表级锁,不要和插入意向锁搞混。 意向锁的出现是为了支持InnoDB的多粒度锁,它解决的是表锁和行锁共存的问题。 当我们需要给一个表加表锁的时候,我们需要根据去判断表中有没有数据行被锁定,以确定是否能加成功。 假如没有意向锁,那么我们就得遍历表中所有数据行来判断有没有行锁; 有了意向锁这个表级锁之后,则我们直接判断一次就知道表中是否有数据行被锁定了。 有了意向锁之后,要执行的事务A在申请行锁(写锁)之前,数据库会自动先给事务A申请表的意向排他锁。当事务B去申请表的互斥锁时就会失败,因为表上有意向排他锁之后事务B申请表的互斥锁时会被阻塞。 46.MySQL的乐观锁和悲观锁了解吗?悲观锁(Pessimistic Concurrency Control):悲观锁认为被它保护的数据是极其不安全的,每时每刻都有可能被改动,一个事务拿到悲观锁后,其他任何事务都不能对该数据进行修改,只能等待锁被释放才可以执行。 数据库中的行锁,表锁,读锁,写锁均为悲观锁。乐观锁(Optimistic Concurrency Control)乐观锁认为数据的变动不会太频繁。 乐观锁通常是通过在表中增加一个版本(version)或时间戳(timestamp)来实现,其中,版本最为常用。 事务在从数据库中取数据时,会将该数据的版本也取出来(v1),当事务对数据变动完毕想要将其更新到表中时,会将之前取出的版本v1与数据中最新的版本v2相对比,如果v1=v2,那么说明在数据变动期间,没有其他事务对数据进行修改,此时,就允许事务对表中的数据进行修改,并且修改时version会加1,以此来表明数据已被变动。 如果,v1不等于v2,那么说明数据变动期间,数据被其他事务改动了,此时不允许数据更新到表中,一般的处理办法是通知用户让其重新操作。不同于悲观锁,乐观锁通常是由开发者实现的。47.MySQL 遇到过死锁问题吗,你是如何解决的?排查死锁的一般步骤是这样的: (1)查看死锁日志 show engine innodb status; (2)找出死锁 sql (3)分析 sql 加锁情况 (4)模拟死锁案发 (5)分析死锁日志 (6)分析死锁结果 当然,这只是一个简单的流程说明,实际上生产中的死锁千奇百怪,排查和解决起来没那么简单。 事务48.MySQL 事务的四大特性说一下?原子性:事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。一致性:指在事务开始之前和事务结束以后,数据不会被破坏,假如 A 账户给 B 账户转 10 块钱,不管成功与否,A 和 B 的总金额是不变的。隔离性:多个事务并发访问时,事务之间是相互隔离的,即一个事务不影响其它事务运行效果。简言之,就是事务之间是进水不犯河水的。持久性:表示事务完成以后,该事务对数据库所作的操作更改,将持久地保存在数据库之中。49.那ACID靠什么保证的呢?事务的隔离性是通过数据库锁的机制实现的。事务的一致性由undo log来保证:undo log是逻辑日志,记录了事务的insert、update、deltete操作,回滚的时候做相反的delete、update、insert操作来恢复数据。事务的原子性和持久性由redo log来保证:redolog被称作重做日志,是物理日志,事务提交的时候,必须先将事务的所有日志写入redo log持久化,到事务的提交操作才算完成。50.事务的隔离级别有哪些?MySQL 的默认隔离级别是什么?读未提交(Read Uncommitted)读已提交(Read Committed)可重复读(Repeatable Read)串行化(Serializable)MySQL默认的事务隔离级别是可重复读 (Repeatable Read)。51.什么是幻读,脏读,不可重复读呢?事务 A、B 交替执行,事务 A 读取到事务 B 未提交的数据,这就是脏读。在一个事务范围内,两个相同的查询,读取同一条记录,却返回了不同的数据,这就是不可重复读。事务 A 查询一个范围的结果集,另一个并发事务 B 往这个范围中插入 / 删除了数据,并静悄悄地提交,然后事务 A 再次查询相同的范围,两次读取得到的结果集不一样了,这就是幻读。不同的隔离级别,在并发事务下可能会发生的问题: 隔离级别脏读不可重复读幻读Read Uncommited 读取未提交是是是Read Commited 读取已提交否是否Repeatable Read 可重复读否否是Serialzable 可串行化否否否52.事务的各个隔离级别都是如何实现的?读未提交 读未提交,就不用多说了,采取的是读不加锁原理。事务读不加锁,不阻塞其他事务的读和写事务写阻塞其他事务写,但不阻塞其他事务读;读取已提交&可重复读 读取已提交和可重复读级别利用了ReadView和MVCC,也就是每个事务只能读取它能看到的版本(ReadView)。READ COMMITTED:每次读取数据前都生成一个ReadViewREPEATABLE READ : 在第一次读取数据时生成一个ReadView串行化 串行化的实现采用的是读写都加锁的原理。 串行化的情况下,对于同一行事务,写会加写锁,读会加读锁。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。53.MVCC了解吗?怎么实现的?MVCC(Multi Version Concurrency Control),中文名是多版本并发控制,简单来说就是通过维护数据历史版本,从而解决并发访问情况下的读一致性问题。关于它的实现,要抓住几个关键点,隐式字段、undo日志、版本链、快照读&当前读、Read View。 版本链 对于InnoDB存储引擎,每一行记录都有两个隐藏列DB\_TRX\_ID、DB\_ROLL\_PTRDB_TRX_ID,事务ID,每次修改时,都会把该事务ID复制给DB_TRX_ID;DB_ROLL_PTR,回滚指针,指向回滚段的undo日志。假如有一张user表,表中只有一行记录,当时插入的事务id为80。此时,该条记录的示例图如下: 接下来有两个DB_TRX_ID分别为100、200的事务对这条记录进行update操作,整个过程如下: 由于每次变动都会先把undo日志记录下来,并用DB_ROLL_PTR指向undo日志地址。因此可以认为,对该条记录的修改日志串联起来就形成了一个版本链,版本链的头节点就是当前记录最新的值。如下: ReadView 对于Read Committed和Repeatable Read隔离级别来说,都需要读取已经提交的事务所修改的记录,也就是说如果版本链中某个版本的修改没有提交,那么该版本的记录时不能被读取的。所以需要确定在Read Committed和Repeatable Read隔离级别下,版本链中哪个版本是能被当前事务读取的。于是就引入了ReadView这个概念来解决这个问题。 Read View就是事务执行快照读时,产生的读视图,相当于某时刻表记录的一个快照,通过这个快照,我们可以获取: m_ids :表示在生成 ReadView 时当前系统中活跃的读写事务的事务id 列表。min\_trx\_id :表示在生成 ReadView 时当前系统中活跃的读写事务中最小的 事务id ,也就是 m_ids 中的最小值。max\_trx\_id :表示生成 ReadView 时系统中应该分配给下一个事务的 id 值。creator\_trx\_id :表示生成该 ReadView 的事务的 事务id有了这个 ReadView ,这样在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可见:如果被访问版本的 DB\_TRX\_ID 属性值与 ReadView 中的 creator\_trx\_id 值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。如果被访问版本的 DB\_TRX\_ID 属性值小于 ReadView 中的 min\_trx\_id 值,表明生成该版本的事务在当前事务生成 ReadView 前已经提交,所以该版本可以被当前事务访问。如果被访问版本的 DB\_TRX\_ID 属性值大于 ReadView 中的 max\_trx\_id 值,表明生成该版本的事务在当前事务生成 ReadView 后才开启,所以该版本不可以被当前事务访问。如果被访问版本的 DB\_TRX\_ID 属性值在 ReadView 的 min\_trx\_id 和 max\_trx\_id 之间,那就需要判断一下trx\_id 属性值是不是在 m\_ids 列表中,如果在,说明创建 ReadView 时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问。如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本。如果最后一个版本也不可见的话,那么就意味着该条记录对该事务完全不可见,查询结果就不包含该记录。 在 MySQL 中, READ COMMITTED 和 REPEATABLE READ 隔离级别的的一个非常大的区别就是它们生成ReadView的时机不同。 READ COMMITTED 是每次读取数据前都生成一个ReadView,这样就能保证自己每次都能读到其它事务提交的数据;REPEATABLE READ 是在第一次读取数据时生成一个ReadView,这样就能保证后续读取的结果完全一致。 高可用/性能54.数据库读写分离了解吗?读写分离的基本原理是将数据库读写操作分散到不同的节点上,下面是基本架构图: 读写分离的基本实现是:数据库服务器搭建主从集群,一主一从、一主多从都可以。数据库主机负责读写操作,从机只负责读操作。数据库主机通过复制将数据同步到从机,每台数据库服务器都存储了所有的业务数据。业务服务器将写操作发给数据库主机,将读操作发给数据库从机。55.那读写分离的分配怎么实现呢?将读写操作区分开来,然后访问不同的数据库服务器,一般有两种方式:程序代码封装和中间件封装。程序代码封装程序代码封装指在代码中抽象一个数据访问层(所以有的文章也称这种方式为 "中间层封装" ) ,实现读写操作分离和数据库服务器连接的管理。例如,基于 Hibernate 进行简单封装,就可以实现读写分离: 目前开源的实现方案中,淘宝的 TDDL (Taobao Distributed Data Layer, 外号:头都大了)是比较有名的。中间件封装中间件封装指的是独立一套系统出来,实现读写操作分离和数据库服务器连接的管理。中间件对业务服务器提供 SQL 兼容的协议,业务服务器无须自己进行读写分离。 对于业务服务器来说,访问中间件和访问数据库没有区别,事实上在业务服务器看来,中间件就是一个数据库服务器。 其基本架构是: 56.主从复制原理了解吗?master数据写入,更新binlogmaster创建一个dump线程向slave推送binlogslave连接到master的时候,会创建一个IO线程接收binlog,并记录到relay log中继日志中slave再开启一个sql线程读取relay log事件并在slave执行,完成同步slave记录自己的binglog57.主从同步延迟怎么处理?主从同步延迟的原因 一个服务器开放N个链接给客户端来连接的,这样有会有大并发的更新操作, 但是从服务器的里面读取 binlog 的线程仅有一个,当某个 SQL 在从服务器上执行的时间稍长 或者由于某个 SQL 要进行锁表就会导致,主服务器的 SQL 大量积压,未被同步到从服务器里。这就导致了主从不一致, 也就是主从延迟。 主从同步延迟的解决办法 解决主从复制延迟有几种常见的方法:写操作后的读操作指定发给数据库主服务器例如,注册账号完成后,登录时读取账号的读操作也发给数据库主服务器。这种方式和业务强绑定,对业务的侵入和影响较大,如果哪个新来的程序员不知道这样写代码,就会导致一个bug。读从机失败后再读一次主机这就是通常所说的 "二次读取" ,二次读取和业务无绑定,只需要对底层数据库访问的 API 进行封装即可,实现代价较小,不足之处在于如果有很多二次读取,将大大增加主机的读操作压力。例如,黑客暴力破解账号,会导致大量的二次读取操作,主机可能顶不住读操作的压力从而崩溃。关键业务读写操作全部指向主机,非关键业务采用读写分离例如,对于一个用户管理系统来说,注册 + 登录的业务读写操作全部访问主机,用户的介绍、爰好、等级等业务,可以采用读写分离,因为即使用户改了自己的自我介绍,在查询时却看到了自我介绍还是旧的,业务影响与不能登录相比就小很多,还可以忍受。58.你们一般是怎么分库的呢?垂直分库:以表为依据,按照业务归属不同,将不同的表拆分到不同的库中。水平分库:以字段为依据,按照一定策略(hash、range 等),将一个库中的数据拆分到多个库中。59.那你们是怎么分表的?水平分表:以字段为依据,按照一定策略(hash、range 等),将一个表中的数据拆分到多个表中。垂直分表:以字段为依据,按照字段的活跃性,将表中字段拆到不同的表(主表和扩展表)中。60.水平分表有哪几种路由方式?什么是路由呢?就是数据应该分到哪一张表。 水平分表主要有三种路由方式:范围路由:选取有序的数据列 (例如,整形、时间戳等) 作为路由的条件,不同分段分散到不同的数据库表中。我们可以观察一些支付系统,发现只能查一年范围内的支付记录,这个可能就是支付公司按照时间进行了分表。 范围路由设计的复杂点主要体现在分段大小的选取上,分段太小会导致切分后子表数量过多,增加维护复杂度;分段太大可能会导致单表依然存在性能问题,一般建议分段大小在 100 万至2000 万之间,具体需要根据业务选取合适的分段大小。 范围路由的优点是可以随着数据的增加平滑地扩充新的表。例如,现在的用户是 100 万,如果增加到 1000 万,只需要增加新的表就可以了,原有的数据不需要动。范围路由的一个比较隐含的缺点是分布不均匀,假如按照 1000 万来进行分表,有可能某个分段实际存储的数据量只有 1000 条,而另外一个分段实际存储的数据量有 900 万条。Hash 路由:选取某个列 (或者某几个列组合也可以) 的值进行 Hash 运算,然后根据 Hash 结果分散到不同的数据库表中。同样以订单 id 为例,假如我们一开始就规划了 4个数据库表,路由算法可以简单地用 id % 4 的值来表示数据所属的数据库表编号,id 为 12的订单放到编号为 50的子表中,id为 13的订单放到编号为 61的字表中。 Hash 路由设计的复杂点主要体现在初始表数量的选取上,表数量太多维护比较麻烦,表数量太少又可能导致单表性能存在问题。而用了 Hash 路由后,增加子表数量是非常麻烦的,所有数据都要重分布。Hash 路由的优缺点和范围路由基本相反,Hash 路由的优点是表分布比较均匀,缺点是扩充新的表很麻烦,所有数据都要重分布。配置路由:配置路由就是路由表,用一张独立的表来记录路由信息。同样以订单id 为例,我们新增一张 order\_router 表,这个表包含 orderjd 和 tablejd 两列 , 根据 orderjd 就可以查询对应的 table\_id。配置路由设计简单,使用起来非常灵活,尤其是在扩充表的时候,只需要迁移指定的数据,然后修改路由表就可以了。 配置路由的缺点就是必须多查询一次,会影响整体性能;而且路由表本身如果太大(例如,几亿条数据) ,性能同样可能成为瓶颈,如果我们再次将路由表分库分表,则又面临一个死循环式的路由算法选择问题。61.不停机扩容怎么实现?实际上,不停机扩容,实操起来是个非常麻烦而且很有风险的操作,当然,面试回答起来就简单很多。第一阶段:在线双写,查询走老库建立好新的库表结构,数据写入久库的同时,也写入拆分的新库数据迁移,使用数据迁移程序,将旧库中的历史数据迁移到新库使用定时任务,新旧库的数据对比,把差异补齐第二阶段:在线双写,查询走新库完成了历史数据的同步和校验把对数据的读切换到新库第三阶段:旧库下线旧库不再写入新的数据经过一段时间,确定旧库没有请求之后,就可以下线老库62.常用的分库分表中间件有哪些?sharding-jdbcMycat63.那你觉得分库分表会带来什么问题呢?从分库的角度来讲:事务的问题使用关系型数据库,有很大一点在于它保证事务完整性。 而分库之后单机事务就用不上了,必须使用分布式事务来解决。跨库 JOIN 问题在一个库中的时候我们还可以利用 JOIN 来连表查询,而跨库了之后就无法使用 JOIN 了。 此时的解决方案就是在业务代码中进行关联,也就是先把一个表的数据查出来,然后通过得到的结果再去查另一张表,然后利用代码来关联得到最终的结果。 这种方式实现起来稍微比较复杂,不过也是可以接受的。 还有可以适当的冗余一些字段。比如以前的表就存储一个关联 ID,但是业务时常要求返回对应的 Name 或者其他字段。这时候就可以把这些字段冗余到当前表中,来去除需要关联的操作。 还有一种方式就是数据异构,通过binlog同步等方式,把需要跨库join的数据异构到ES等存储结构中,通过ES进行查询。 从分表的角度来看:跨节点的 count,order by,group by 以及聚合函数问题只能由业务代码来实现或者用中间件将各表中的数据汇总、排序、分页然后返回。数据迁移,容量规划,扩容等问题数据的迁移,容量如何规划,未来是否可能再次需要扩容,等等,都是需要考虑的问题。ID 问题数据库表被切分后,不能再依赖数据库自身的主键生成机制,所以需要一些手段来保证全局主键唯一。还是自增,只不过自增步长设置一下。比如现在有三张表,步长设置为3,三张表 ID 初始值分别是1、2、3。 这样第一张表的 ID 增长是 1、4、7。第二张表是2、5、8。第三张表是3、6、9,这样就不会重复了。UUID,这种最简单,但是不连续的主键插入会导致严重的页分裂,性能比较差。分布式 ID,比较出名的就是 Twitter 开源的 sonwflake 雪花算法运维64.百万级别以上的数据如何删除?关于索引:由于索引需要额外的维护成本,因为索引文件是单独存在的文件,所以当我们对数据的增加,修改,删除,都会产生额外的对索引文件的操作,这些操作需要消耗额外的IO,会降低增/改/删的执行效率。 所以,在我们删除数据库百万级别数据的时候,查询MySQL官方手册得知删除数据的速度和创建的索引数量是成正比的。所以我们想要删除百万数据的时候可以先删除索引然后删除其中无用数据删除完成后重新创建索引创建索引也非常快65.百万千万级大表如何添加字段?当线上的数据库数据量到达几百万、上千万的时候,加一个字段就没那么简单,因为可能会长时间锁表。 大表添加字段,通常有这些做法:通过中间表转换过去 创建一个临时的新表,把旧表的结构完全复制过去,添加字段,再把旧表数据复制过去,删除旧表,新表命名为旧表的名称,这种方式可能回丢掉一些数据。用pt-online-schema-change pt-online-schema-change是percona公司开发的一个工具,它可以在线修改表结构,它的原理也是通过中间表。先在从库添加 再进行主从切换 如果一张表数据量大且是热表(读写特别频繁),则可以考虑先在从库添加,再进行主从切换,切换后再将其他几个节点上添加字段。66.MySQL 数据库 cpu 飙升的话,要怎么处理呢?排查过程: (1)使用 top 命令观察,确定是 mysqld 导致还是其他原因。 (2)如果是 mysqld 导致的,show processlist,查看 session 情况,确定是不是有消耗资源的 sql 在运行。 (3)找出消耗高的 sql,看看执行计划是否准确, 索引是否缺失,数据量是否太大。 处理: (1)kill 掉这些线程 (同时观察 cpu 使用率是否下降), (2)进行相应的调整 (比如说加索引、改 sql、改内存参数) (3)重新跑这些 SQL。 其他情况: 也有可能是每个 sql 消耗资源并不多,但是突然之间,有大量的 session 连进来导致 cpu 飙升,这种情况就需要跟应用一起来分析为何连接数会激增,再做出相应的调整,比如说限制连接数等
2023年11月29日
13 阅读
0 评论
0 点赞
2023-11-20
nginx+php+mysql
nginx下载安装https://nginx.org/download yum -y install make zlib zlib-devel gcc-c++ libtool openssl openssl-devel #编译环境 cd /opt wget https://nginx.org/download/nginx-1.25.3.tar.gz tar zxvf nginx-1.25.3.tar.gz cd nginx-1.25.3 mkdir -p /www/nginx ./configure --prefix=/www/nginx make && make install启动/www/nginx/sbin/nginx常用指令nginx -V 查看版本,以及配置文件地址nginx -v 查看版本nginx -c filename 指定配置文件nginx -h 帮助nginx -s reload|reopen|stop|quit // 重新加载配置|重启|停止|退出 nginx")重新加载配置|重启|停止|退出 nginxnginx -t //查看配置是否有语法错误nginx -c /usr/local/etc/nginx/nginx.conf //启动是加载指定nginx.conf文件建立软链接建立软链接目的是省去根目录快捷操作nginx[root@xxx opt]# ln -s /www/nginx/sbin/nginx /usr/sbin/ [root@xxx opt]# nginx -v nginx version: nginx/1.25.3 # 删除软连接 rm -rf /usr/sbin/nginx安装启动好php-fpm后,修改nginx.conf并重启nginx把 /scripts$fastcgi_script_name** 改成 **$document_root$fastcgi_script_namelocation ~ \.php$ { root /www/nginx/php; fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; #支持解析php文件 include fastcgi_params; } 重启nginxnginx -t nginx -s reloadPS:如果还报错 [error] 37432#0: *85 FastCGI sent in stderr: "Primary script unknown" while reading response header from upstream 那一定是php文件路径不对phpyum方式安装用yum方式安装php最新版1 安装 epel-release源和 源管理工具yum-utilsyum -y install epel-release yum-utils2 安装Remi软件源Remi软件源官方地址: https://rpms.remirepo.net/CentOS7安装:yum -y install https://rpms.remirepo.net/enterprise/remi-release-7.rpmCentOS7启动php模块并安装:yum启动php模块,根据需要选择自己格式的版本,如remi-php74模块,就是php7.4版本。:yum-config-manager --enable remi-php80 #启用remi源的php8.0模块注:如果显示没有这个命令,则需要先安装yum -y install yum-utils 插件。安装php及相关扩展:yum install php php-cli php-fpm php-mysqlnd php-zip php-devel php-gd php-mbstring php-curl php-xml php-pear php-bcmath php-json php-redis安装成功:php -v 查看版本为php8.0版本3 启动php-fpm[root@xx logs]# whereis php-fpm php-fpm: /usr/sbin/php-fpm /etc/php-fpm.d /etc/php-fpm.conf /usr/share/man/man8/php-fpm.8.gz [root@xx logs]# systemctl start php-fpm mysqlyum安装mysql MySQL三种安装方法(yum安装、编译安装、二进制安装)yum安装mysql1.卸载旧版mysql如果安装过先卸载,第一次安装略过此步[root@bunian etc]# rpm -qa | grep mysql mysql-community-common-5.7.38-1.el7.x86_64 mysql-community-client-5.7.38-1.el7.x86_64 mysql80-community-release-el7-6.noarch mysql-community-server-5.7.38-1.el7.x86_64 mysql-community-libs-5.7.38-1.el7.x86_64 # 所有组件都要删除 [root@bunian etc]# rpm -e --nodeps mysql-community-common-5.7.38-1.el7.x86_64 [root@bunian etc]# rpm -e --nodeps mysql-community-client-5.7.38-1.el7.x86_64 [root@bunian etc]# rpm -e --nodeps mysql80-community-release-el7-6.noarch [root@bunian etc]# rpm -e --nodeps mysql-community-server-5.7.38-1.el7.x86_64 [root@bunian etc]# rpm -e --nodeps mysql-community-libs-5.7.38-1.el7.x86_64 # 清理yum headers cache [root@bunian etc]# yum clean all2.安装2.1 rpm安装yum源[root@bunian tmp]# rpm -ivh https://dev.mysql.com/get/mysql80-community-release-el7-6.noarch.rpm2.2 下载rpm源文件再安装[root@bunian tmp]# wget https://dev.mysql.com/get/mysql80-community-release-el7-6.noarch.rpm [root@bunian tmp]# yum localinstall mysql80-community-release-el7-6.noarch.rpm3.选择mysql版本[root@bunian tmp]# yum repolist all | grep mysql mysql-cluster-7.5-community/x86_64 MySQL Cluster 7.5 Comm 禁用 mysql-cluster-7.5-community-source MySQL Cluster 7.5 Comm 禁用 mysql-cluster-7.6-community/x86_64 MySQL Cluster 7.6 Comm 禁用 mysql-cluster-7.6-community-source MySQL Cluster 7.6 Comm 禁用 mysql-cluster-8.0-community/x86_64 MySQL Cluster 8.0 Comm 禁用 mysql-cluster-8.0-community-debuginfo/x86_64 MySQL Cluster 8.0 Comm 禁用 mysql-cluster-8.0-community-source MySQL Cluster 8.0 Comm 禁用 mysql-connectors-community/x86_64 MySQL Connectors Commu 启用: 192 mysql-connectors-community-debuginfo/x86_64 MySQL Connectors Commu 禁用 mysql-connectors-community-source MySQL Connectors Commu 禁用 mysql-tools-community/x86_64 MySQL Tools Community 启用: 90 mysql-tools-community-debuginfo/x86_64 MySQL Tools Community 禁用 mysql-tools-community-source MySQL Tools Community 禁用 mysql-tools-preview/x86_64 MySQL Tools Preview 禁用 mysql-tools-preview-source MySQL Tools Preview - 禁用 mysql57-community/x86_64 MySQL 5.7 Community Se 禁用 mysql57-community-source MySQL 5.7 Community Se 禁用 mysql80-community/x86_64 MySQL 8.0 Community Se 启用: 343 mysql80-community-debuginfo/x86_64 MySQL 8.0 Community Se 禁用 mysql80-community-source MySQL 8.0 Community Se 禁用默认是开启8.0版本4.去除公钥检索校验 (最重要的地方)这个地方容易出错,原因是没有去除校验导致服务安装后无法启动#gpgcheck改成0 [root@bunian tmp]# vim /etc/yum.repos.d/mysql-community.repo [mysql80-community] name=MySQL 8.0 Community Server baseurl=http://repo.mysql.com/yum/mysql-8.0-community/el/7/$basearch enabled=1 gpgcheck=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-mysql-2022 file:///etc/pki/rpm-gpg/RPM-GPG-KEY-mysql5.安装mysql[root@bunian tmp]# yum install mysql-community-server6.配置mysql[root@bunian tmp]# vim /etc/my.cnf [mysqld] # 不区分大小写 lower_case_table_names=1 # 端口号 port = 3306lower_case_table_names7.启动mysql[root@bunian tmp]# systemctl start mysqld8.修改mysql root密码及外网访问初始密码在这里/var/log/mysqld.log# 密码是:6;-#?gn*q;0H [root@bunian etc]# grep 'temporary password' /var/log/mysqld.log 2022-06-21T03:25:39.021858Z 1 [Note] A temporary password is generated for root@localhost: 6;-#?gn*q;0H登录mysql修改密码[root@bunian etc]# mysql -h localhost -p Enter password: Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 11 Server version: 8.0.35 Copyright (c) 2000, 2023, Oracle and/or its affiliates. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. mysql> ALTER USER 'root'@'localhost' IDENTIFIED BY 'newpasswd'; mysql> use mysql; Reading table information for completion of table and column names You can turn off this feature to get a quicker startup with -A Database changed修改外网访问在 mysql 数据库的 user 表中查看当前 root 用户的相关信息select host, user, authentication_string, plugin from user;执行完上面的命令后会显示一个表格查看表格中 root 用户的 host,默认应该显示的 localhost,只支持本地访问,不允许远程访问。授权 root 用户的所有权限并设置远程访问update user set host='%' where user='root'; GRANT ALL ON *.* TO 'root'@'%'; GRANT ALL ON *.* TO 'root'@'%';执行两次 GRANT ALL ON . TO 'root'@'%';刷新权限mysql> flush privileges;9.检验是否安装成功# 查看mysql端口号 [root@bunian etc]# netstat -nltp | grep 3306 tcp6 0 0 :::33060 :::* LISTEN 1292/mysqld tcp6 0 0 :::3306 :::* LISTEN 1292/mysqld 10.Navicat连接用Navicat连接报错:2059 - Authentication plugin 'caching_sha2_password' cannot be loaded原因以及解决办法由于目前已有的客户端连接软件还不支持Mysql8新增加的加密方式: caching_sha2_password 所以我们需要修改用户的加密方式,将其改为老的加密验证方式: mysql_native_password#登录 mysql -h localhost -p #选择数据库 use mysql; # 注意:先 select host, user from user; 查看host值再决定以下是'root'@'%' 还是 'root'@'localhost' ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '你的数据库密码'; #刷新权限 FLUSH PRIVILEGES; 11.php连接mysql连接Navicat创建test数据库、new表vim /www/nginx/php/index.php<?php $servername = "localhost"; $username = "root"; $password = "password"; $dbname = "test"; // 创建连接 $conn = new mysqli($servername, $username, $password, $dbname); // 检查连接 if ($conn->connect_error) { die("连接失败: " . $conn->connect_error); } // 执行查询 $sql = "SELECT * FROM new"; $result = $conn->query($sql); // 输出数据 if ($result->num_rows > 0) { while($row = $result->fetch_assoc()) { echo "id: " . $row["id"]. " - name: " . $row["name"]. "<br>"; } } else { echo "0 结果"; } //访问ip/index.php id: 1 - name: 测试文章
2023年11月20日
37 阅读
0 评论
0 点赞
1
2
3
4
...
20