This commit is contained in:
louzefeng
2024-07-09 18:38:56 +00:00
parent 8bafaef34d
commit bf99793fd0
6071 changed files with 1017944 additions and 0 deletions

View File

@@ -0,0 +1,157 @@
<audio id="audio" title="38丨如何在Excel中使用SQL语言" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/34/bd/3499f47bf96798dd8722bd4569d35bbd.mp3"></audio>
在进阶篇中我们对设计范式、索引、页结构、事务以及查询优化器的原理进行了学习了解这些可以让我们更好地使用SQL来操作RDBMS。实际上SQL的影响力远不止于此在数据的世界里SQL更像是一门通用的语言虽然每种工具都会有一些自己的“方言”但是掌握SQL可以让我们接触其它以数据为核心的工具时更加游刃有余。
比如Excel。
你一定使用过Excel事实上Excel的某些部分同样支持我们使用SQL语言那么具体该如何操作呢
今天的课程主要包括以下几方面的内容:
1. 如何在Excel中获取外部数据源
1. 数据透视表和数据透视图是Excel的两个重要功能如何通过SQL查询在Excel中完成数据透视表和透视图
1. 如何让Excel与MySQL进行数据交互
## 如何在Excel中获取外部数据源
使用SQL查询数据首先需要数据源。如果我们用Excel来呈现这些数据的话就需要先从外部导入数据源。这里介绍两种直接导入的方式
1. 通过OLE DB接口获取外部数据源
1. 通过Microsoft Query导入外部数据源。
下面我们通过导入数据源heros.xlsx体验一下这两种方式你可以从[这里](https://github.com/cystanford/SQL-Excel)下载数据源。
### 通过OLE DB接口获取外部数据源
OLE的英文是Object Link and Embedding中文意思是对象连接与嵌入它是一种面向对象的技术。DB代表的就是数据库。OLE DB的作用就是通向不同的数据源的程序接口方便获取外部数据这里不仅包括ODBC也包括其他非SQL数据类型的通路你可以把OLE DB的作用理解成通过统一的接口来访问不同的数据源。
如果你想要在Excel中通过OLE DB接口导入数据需要执行下面的步骤
第一步,选择指定的文件。方法是通过“数据” → “现有连接”按钮选择连接。这里选择“浏览更多”然后选择指定的xls文件。
<img src="https://static001.geekbang.org/resource/image/b5/81/b53c4acda1cf19a1943cf0123f5a2481.png" alt=""><br>
第二步,选择指定的表格,勾选数据首行包含列标题,目的是将第一行的列名也加载进来。
<img src="https://static001.geekbang.org/resource/image/85/f9/8594b603410c1b7872de9d7ef38e7df9.png" alt=""><br>
第三步,通过“属性” → “定义”中的命令文本来使用SQL查询选择我们想要的数据也可以将整张表直接导入到指定的位置。
<img src="https://static001.geekbang.org/resource/image/1d/f2/1df9b6d9ab9d4a854ab532f1f98ed1f2.png" alt=""><br>
如果我们显示方式为“表”,导入全部的数据到指定的$A$1代表A1单元格那么在Excel中就可以导入整个数据表如下图所示
<img src="https://static001.geekbang.org/resource/image/ba/ab/baeb41a95f49eb1fb76d6afe48122aab.png" alt="">
### 通过Microsoft Query获取外部数据源
第二种方式是利用Microsoft Query功能导入外部数据源具体步骤如下
第一步,选择指定的文件。方法是通过“数据” → “获取外部数据”按钮选择数据库这里我选择了“Excel Files”然后选择我们想要导入的xls文件。
<img src="https://static001.geekbang.org/resource/image/c7/84/c7cb9168c77b11c0d7f90a86316c3b84.png" alt=""><br>
第二步。选择可用的表和列,在左侧面板中勾选我们想要导入的数据表及相应的列,点击 &gt; 按钮导入到右侧的面板中,然后点击下一步。
<img src="https://static001.geekbang.org/resource/image/1c/40/1ca1f6f8d11e2f0c70c0b81ff8647440.png" alt=""><br>
最后我们可以选择“将数据返回Microsoft Excel”还是“在Microsoft Query中查看数据或编辑查询”。这里我们选择第一个选项。
<img src="https://static001.geekbang.org/resource/image/75/46/753b382fa6246c4dd1d7634d703dc646.png" alt=""><br>
当我们选择“将数据返回到Microsoft Excel”后接下来的操作和使用OLE DB接口方式导入数据一样可以对显示方式以及属性进行调整
<img src="https://static001.geekbang.org/resource/image/24/65/24c70f964bb4dbdaf388af45bc57d865.png" alt=""><br>
这里,我们同样选择显示方式为“表”,导入全部的数据到指定的$A$1(代表A1单元格同样会看到如下的结果
<img src="https://static001.geekbang.org/resource/image/ba/ab/baeb41a95f49eb1fb76d6afe48122aab.png" alt="">
## 使用数据透视表和数据透视图做分析
通过上面的操作你也能看出来从外部导入数据并不难关键在于通过SQL控制想要的结果集这里我们需要使用到Excel的数据透视表以及数据透视图的功能。
我简单介绍下数据透视表和数据透视图:
数据透视表可以快速汇总大量数据,帮助我们统计和分析数据,比如求和,计数,查看数据中的对比情况和趋势等。数据透视图则可以对数据透视表中的汇总数据进行可视化,方便我们直观地查看数据的对比与趋势等。
假设我想对主要角色role_main的英雄数据进行统计分析他们平均的最大生命值hp_max平均的最大法力值(mp_max),平均的最大攻击值(attack_max)那么对应的SQL查询为
```
SELECT role_main, avg(hp_max) AS `平均最大生命`, avg(mp_max) AS `平均最大法力`, avg(attack_max) AS `平均最大攻击力`, count(*) AS num FROM heros GROUP BY role_main
```
### 使用SQL+数据透视表
现在我们使用SQL查询通过OLE DB的方式来完成数据透视表。我们在第三步的时候选择“属性”并且在命令文本中输入相应的SQL语句注意这里的数据表是[heros$],对应的命令文本为:
```
SELECT role_main, avg(hp_max) AS `平均最大生命`, avg(mp_max) AS `平均最大法力`, avg(attack_max) AS `平均最大攻击力`, count(*) AS num FROM [heros$] GROUP BY role_main
```
<img src="https://static001.geekbang.org/resource/image/5f/0d/5ff31b9bc54a6d0c02e23775273ada0d.png" alt=""><br>
然后我们在右侧面板中选择“数据透视表字段”以便对数据透视表中的字段进行管理比如我们勾选numrole_main平均最大生命平均最大法力平均最大攻击力。
<img src="https://static001.geekbang.org/resource/image/cf/9d/cf3e3da9ddedb2d886806ac4b490059d.png" alt=""><br>
最后会在Excel中呈现如下的数据透视表
<img src="https://static001.geekbang.org/resource/image/c4/08/c41bd917642147b103ec3524d7a9f408.png" alt=""><br>
操作视频如下:
<video poster="https://media001.geekbang.org/b29dda8f93f5475fa36cee2908a48e89/snapshots/d41bc8d4cddf43b1aad2baa48293dcc7-00002.jpg" preload="none" controls=""><source src="https://media001.geekbang.org/customerTrans/fe4a99b62946f2c31c2095c167b26f9c/4a4da64f-16d00e8d8a6-0000-0000-01d-dbacd.mp4" type="video/mp4"><source src="https://media001.geekbang.org/b29dda8f93f5475fa36cee2908a48e89/06365f50429e4e849bea2767ce97fbf9-28c4680fd9b67ce1559bbe18701345b0-sd.m3u8" type="application/x-mpegURL"><source src="https://media001.geekbang.org/b29dda8f93f5475fa36cee2908a48e89/06365f50429e4e849bea2767ce97fbf9-7300b80dc359dc62ac7bd12b7c613c4b-hd.m3u8" type="application/x-mpegURL"></video>
### 使用SQL+数据透视图
数据透视图可以呈现可视化的形式方便我们直观地了解数据的特征。这里我们使用SQL查询通过Microsoft Query的方式来完成数据透视图。我们在第三步的时候选择在Microsoft Query中查看数据或编辑查询来看下Microsoft Query的界面
<img src="https://static001.geekbang.org/resource/image/c7/20/c7d10db98c4e2226b663bbb7baefba20.png" alt=""><br>
然后我们点击“SQL”按钮可以对SQL语句进行编辑筛选我们想要的结果集可以得到
<img src="https://static001.geekbang.org/resource/image/2b/7d/2b9b5f3495013b62b7ba6e3186cf257d.png" alt=""><br>
然后选择“将数据返回Microsoft Excel”在返回时选择“数据透视图”然后在右侧选择数据透视图的字段就可以得到下面这张图
<img src="https://static001.geekbang.org/resource/image/46/02/46829993666745ecc578b22ffcde4802.png" alt=""><br>
你可以看到使用起来还是很方便。
具体操作视频如下:
<video poster="https://media001.geekbang.org/14c910e996e24edd885b36ad78b5669e/snapshots/e158d539e90e481da88ec1afbba55e0c-00003.jpg" preload="none" controls=""><source src="https://media001.geekbang.org/customerTrans/fe4a99b62946f2c31c2095c167b26f9c/205093c2-16cdc0fb400-0000-0000-01d-dbacd.mp4" type="video/mp4"><source src="https://media001.geekbang.org/14c910e996e24edd885b36ad78b5669e/cbda41eada774746bf4366e2d9b866cc-930f258a810ec8de0e10eccc7e3abe1f-sd.m3u8" type="application/x-mpegURL"><source src="https://media001.geekbang.org/14c910e996e24edd885b36ad78b5669e/cbda41eada774746bf4366e2d9b866cc-4b128136a12f79aeec3215cd333d5da0-hd.m3u8" type="application/x-mpegURL"></video>
## 让Excel与MySQL进行数据交互
刚才我们讲解的是如何从Excel中导入外部的xls文件数据并在Excel实现数据透视表和数据透视图的呈现。实际上Excel也可以与MySQL进行数据交互这里我们需要使用到MySQL for Excel插件
下载mysql-for-excel并安装地址[https://dev.mysql.com/downloads/windows/excel/](https://dev.mysql.com/downloads/windows/excel/)
下载mysql-connector-odbc并安装地址[https://dev.mysql.com/downloads/connector/odbc/](https://dev.mysql.com/downloads/connector/odbc/)
这次我们的任务是给数据表增加一个last_name字段并且使用Excel的自动填充功能来填充好英雄的姓氏。
第一步连接MySQL。打开一个新的Excel文件的时候会在“数据”面板中看到MySQL for Excel的插件点击后可以打开MySQL的连接界面如下
<img src="https://static001.geekbang.org/resource/image/ec/3c/ec96481d8517bc7b08728630d3b1aa3c.png" alt=""><br>
第二步导入heros数据表。输入密码后我们在右侧选择想要的数据表heros然后选择Import MySQL Data导入数据表的导入结果如下
<img src="https://static001.geekbang.org/resource/image/33/15/333b8dc9913bcdf19d74e685f6751015.png" alt=""><br>
第三步创建last_name字段使用Excel的自动填充功能来进行姓氏的填写Excel自带的“自动填充”可以帮我们智能填充一些数据完成之后如下图所示
<img src="https://static001.geekbang.org/resource/image/80/b7/801e1ec489d650a7df244b3737346cb7.png" alt=""><br>
第四步将修改好的Excel表导入到MySQL中创建一个新表heros_xls。选中整个数据表包括数据行及列名然后在右侧选择“Export Excel Data to New Table”。这时在MySQL中你就能看到相应的数据表heros_xls了我们在MySQL中使用SQL进行查询
```
mysql &gt; SELECT * FROM heros_xls
```
运行结果69条记录
<img src="https://static001.geekbang.org/resource/image/86/56/868182e27c6a4a80db0f0f7decbc7956.png" alt=""><br>
需要说明的是有时候自动填充功能并不完全准确我们还需要对某些数据行的last_name进行修改比如“夏侯惇”的姓氏应该改成“夏侯”“百里守约”改成“百里”等。
## 总结
我们今天讲解了如何在Excel中使用SQL进行查询在这个过程中你应该对”SQL定义了查询的标准“更有体会。SQL使得各种工具可以遵守SQL语言的标准当然也有各自的方言
如果你已经是个SQL高手你会发现原来SQL和Excel还可以如此“亲密”。Excel作为使用人数非常多的办公软件提供了SQL查询会让我们操作起来非常方便。如果你还没有使用过Excel的这些功能那么就赶快来用一下吧。
<img src="https://static001.geekbang.org/resource/image/4f/2a/4ffdbea0e37e11aedb9cdebb9d2a1c2a.png" alt=""><br>
SQL作为一门结构化查询语言具有很好的通用性你还在其他工具中使用过SQL语言吗如果有的话可以分享一下你的体会。
最后留一道动手题吧。你可以创建一个新的xls文件导入heros.xlsx数据表用数据透视图的方式对英雄主要定位为刺客、法师、射手的英雄数值进行可视化数据查询方式请使用SQL查询统计的英雄数值为平均生命成长hp_growth平均法力成长mp_growth平均攻击力成长attack_growth。
欢迎你在评论区写下你的体会与思考,也欢迎把这篇文章分享给你的朋友或者同事,一起来交流。

View File

@@ -0,0 +1,238 @@
<audio id="audio" title="39丨WebSQL如何在H5中存储一个本地数据库" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/07/2d/076c8dbf3c6c4934fd0886ac5bf45c2d.mp3"></audio>
上一篇文章中我们讲到了如何在Excel中使用SQL进行查询。在Web应用中即使不通过后端语言与数据库进行操作在Web前端中也可以使用WebSQL。WebSQL是一种操作本地数据库的网页API接口通过它我们就可以操作客户端的本地存储。
今天的课程主要包括以下几方面的内容:
1. 本地存储都有哪些什么是WebSQL
1. 使用WebSQL的三个核心方法是什么
1. 如何使用WebSQL在本地浏览器中创建一个王者荣耀英雄数据库并对它进行查询和页面的呈现
## 本地存储都有哪些什么是WebSQL
我刚才讲到了WebSQL实际上是本地存储。其实本地存储是个更大的概念你现在可以打开Chrome浏览器看下本地存储都包括了哪些。
Cookies是最早的本地存储是浏览器提供的功能并且对服务器和JS开放这意味着我们可以通过服务器端和客户端保存Cookies。不过可以存储的数据总量大小只有4KB如果超过了这个限制就会忽略没法进行保存。
Local Storage与Session Storage都属于Web Storage。Web Storage和Cookies类似区别在于它有更大容量的存储。其中Local Storage是持久化的本地存储除非我们主动删除数据否则会一直存储在本地。Session Storage只存在于Session会话中也就是说只有在同一个Session的页面才能使用当Session会话结束后数据也会自动释放掉。
WebSQL与IndexedDB都是最新的HTML5本地缓存技术相比于Local Storage和Session Storage来说存储功能更强大支持的数据类型也更多比如图片、视频等。
WebSQL更准确的说是WebSQL DB API它是一种操作本地数据库的网页API接口通过API可以完成客户端数据库的操作。当我们使用WebSQL的时候可以方便地用SQL来对数据进行增删改查。而这些浏览器客户端比如Chrome和Safari会用SQLite实现本地存储。
如果说WebSQL方便我们对RDBMS进行操作那么IndexedDB则是一种NoSQL方式。它存储的是key-value类型的数据允许存储大量的数据通常可以超过250M并且支持事务当我们对数据进行增删改查CRUD的时候可以通过事务来进行。
<img src="https://static001.geekbang.org/resource/image/58/a2/58a474019f55d9854034ed244c4ec4a2.png" alt=""><br>
你能看到本地存储包括了多种存储方式,它可以很方便地将数据存储在客户端中,在使用的时候避免重复调用服务器的资源。
需要说明的是今天我要讲的WebSQL并不属于HTML5规范的一部分它是一个单独的规范只是随着HTML5规范一起加入到了浏览器端。主流的浏览器比如Chrome、Safari和Firefox都支持WebSQL我们可以在JavaScript脚本中使用WebSQL对客户端数据库进行操作。
## 如何使用WebSQL
如果你的浏览器不是上面说的那三种怎么检测你的浏览器是否支持WebSQL呢这里你可以检查下window对象中是否存在openDatabase属性方法如下
```
if (!window.openDatabase) {
alert('浏览器不支持WebSQL');
}
完整代码如下:
&lt;!DOCTYPE HTML&gt;
&lt;html&gt;
&lt;head&gt;
&lt;meta charset=&quot;UTF-8&quot;&gt;
&lt;title&gt;SQL必知必会&lt;/title&gt;
&lt;script type=&quot;text/javascript&quot;&gt;
if (!window.openDatabase) {
alert('浏览器不支持WebSQL');
}
&lt;/script&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;div id=&quot;status&quot; name=&quot;status&quot;&gt;WebSQL Test&lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;
```
如果浏览器不支持WebSQL会有弹窗提示“浏览器不支持WebSQL”否则就不会有弹窗提示。使用WebSQL也比较简单主要的方法有3个。
### 打开数据库openDatabase()
我们可以使用openDatabase打开一个已经存在的数据库也可以创建新的数据库。如果数据库已经存在了就会直接打开如果不存在则会创建。方法如下
```
var db = window.openDatabase(dbname, version, dbdesc, dbsize,function() {});
```
这里openDatabase方法中一共包括了5个参数分别为数据库名、版本号、描述、数据库大小、创建回调。其中创建回调可以缺省。
使用openDatabase方法会返回一个数据库句柄我们可以将它保存在变量db中方便我们后续进行使用。
如果我们想要创建一个名为wucai的数据库版本号为1.0数据库的描述是“王者荣耀数据库”大小是1024*1024创建方法为下面这样。
```
var db = openDatabase('wucai', '1.0', '王者荣耀数据库', 1024 * 1024);
```
### 事务操作transaction()
我们使用transaction方法来对事务进行处理执行提交或回滚操作方法如下
```
transaction(callback, errorCallback, successCallback);
```
这里的3个参数代表的含义如下
1. 处理事务的回调函数必选在回调函数中可以执行SQL语句会使用到ExecuteSQL方法
1. 执行失败时的回调函数(可选);
1. 执行成功时的回调函数(可选)。
如果我们进行了一个事务处理包括创建heros数据表想要插入一条数据方法如下
```
db.transaction(function (tx) {
tx.executeSql('CREATE TABLE IF NOT EXISTS heros (id unique, name, hp_max, mp_max, role_main)');
tx.executeSql('INSERT INTO heros (id, name, hp_max, mp_max, role_main) VALUES (10000, &quot;夏侯惇&quot;, 7350, 1746, &quot;坦克&quot;)');
});
```
这里执行的事务就是一个方法包括两条SQL语句。tx表示的是回调函数的接收参数也就是transaction对象的引用方便我们在方法中进行使用。
### SQL执行executeSql()
ExecuteSQL命令用来执行SQL语句即增删改查。方法如下
```
tx.executeSql(sql, [], callback, errorCallback);
```
这里包括了4个参数它们代表的含义如下所示
1. 要执行的sql语句。
1. SQL语句中的占位符?)所对应的参数。
1. 执行SQL成功时的回调函数。
1. 执行SQL失败时的回调函数。
假如我们想要创建一个heros数据表可以使用如下命令
```
tx.executeSql('CREATE TABLE IF NOT EXISTS heros (id unique, name, hp_max, mp_max, role_main)');
```
假如我们想要对刚创建的heros数据表插入一条数据可以使用
```
tx.executeSql('INSERT INTO heros (id, name, hp_max, mp_max, role_main) VALUES (10000, &quot;夏侯惇&quot;, 7350, 1746, &quot;坦克&quot;)');
```
## 在浏览器端做一个王者荣耀英雄的查询页面
刚才我讲解了WebSQL的基本语法现在我们就来用刚学到的东西做一个小练习在浏览器端做一个王者荣耀英雄的创建和查询页面。
具体步骤如下:
1. 初始化数据我们需要在HTML中设置一个id为datatable的table表格然后在JavaScript中创建init()函数获取id为datatable的元素。
1. 创建showData方法参数为查询出来的数据rowshowData方法可以方便地展示查询出来的一行数据我们在数据表中的字段为id、name、hp_max、mp_max和role_main因此我们可以使用row.id、row.name、row.hp_max、row.mp_max和row.role_main来获取这些字段的数值并且创建相应的标签将这5个字段放到一个里面。
1. 使用openDatabase方法打开数据库这里我们定义的数据库名为wucai版本号为1.0数据库描述为“王者荣耀英雄数据”大小为1024 * 1024。
1. 使用transaction方法执行两个事务第一个事务是创建heros数据表并且插入5条数据。第二个事务是对heros数据表进行查询并且对查询出来的数据行使用showData方法进行展示。
完整代码如下(也可以通过[GitHub]((https://github.com/cystanford/WebSQL))下载):
```
&lt;!DOCTYPE HTML&gt;
&lt;html&gt;
&lt;head&gt;
&lt;meta charset=&quot;UTF-8&quot;&gt;
&lt;title&gt;SQL必知必会&lt;/title&gt;
&lt;script type=&quot;text/javascript&quot;&gt;
// 初始化
function init() {
datatable = document.getElementById(&quot;datatable&quot;);
}
// 显示每个英雄的数据
function showData(row){
var tr = document.createElement(&quot;tr&quot;);
var td1 = document.createElement(&quot;td&quot;);
var td2 = document.createElement(&quot;td&quot;);
var td3 = document.createElement(&quot;td&quot;);
var td4 = document.createElement(&quot;td&quot;);
var td5 = document.createElement(&quot;td&quot;);
td1.innerHTML = row.id;
td2.innerHTML = row.name;
td3.innerHTML = row.hp_max;
td4.innerHTML = row.mp_max;
td5.innerHTML = row.role_main;
tr.appendChild(td1);
tr.appendChild(td2);
tr.appendChild(td3);
tr.appendChild(td4);
tr.appendChild(td5);
datatable.appendChild(tr);
}
// 设置数据库信息
var db = openDatabase('wucai', '1.0', '王者荣耀英雄数据', 1024 * 1024);
var msg;
// 插入数据
db.transaction(function (tx) {
tx.executeSql('CREATE TABLE IF NOT EXISTS heros (id unique, name, hp_max, mp_max, role_main)');
tx.executeSql('INSERT INTO heros (id, name, hp_max, mp_max, role_main) VALUES (10000, &quot;夏侯惇&quot;, 7350, 1746, &quot;坦克&quot;)');
tx.executeSql('INSERT INTO heros (id, name, hp_max, mp_max, role_main) VALUES (10001, &quot;钟无艳&quot;, 7000, 1760, &quot;战士&quot;)');
tx.executeSql('INSERT INTO heros (id, name, hp_max, mp_max, role_main) VALUES (10002, &quot;张飞&quot;, 8341, 100, &quot;坦克&quot;)');
tx.executeSql('INSERT INTO heros (id, name, hp_max, mp_max, role_main) VALUES (10003, &quot;牛魔&quot;, 8476, 1926, &quot;坦克&quot;)');
tx.executeSql('INSERT INTO heros (id, name, hp_max, mp_max, role_main) VALUES (10004, &quot;吕布&quot;, 7344, 0, &quot;战士&quot;)');
msg = '&lt;p&gt;heros数据表创建成功一共插入5条数据。&lt;/p&gt;';
document.querySelector('#status').innerHTML = msg;
});
// 查询数据
db.transaction(function (tx) {
tx.executeSql('SELECT * FROM heros', [], function (tx, data) {
var len = data.rows.length;
msg = &quot;&lt;p&gt;查询记录条数: &quot; + len + &quot;&lt;/p&gt;&quot;;
document.querySelector('#status').innerHTML += msg;
// 将查询的英雄数据放到 datatable中
for (i = 0; i &lt; len; i++){
showData(data.rows.item(i));
}
});
});
&lt;/script&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;div id=&quot;status&quot; name=&quot;status&quot;&gt;状态信息&lt;/div&gt;
&lt;table border=&quot;1&quot; id=&quot;datatable&quot;&gt;&lt;/table&gt;
&lt;/body&gt;
&lt;/html&gt;
```
演示结果如下:
<img src="https://static001.geekbang.org/resource/image/3e/33/3e7e08ea2c5d768ed0bdbd757c6b8f33.png" alt=""><br>
你能看到使用WebSQL来操作本地存储还是很方便的。
刚才我们讲的是创建本地存储那么如何删除呢你可以直接通过浏览器来删除比如在Chrome浏览器中找到Application中的Clear storage然后使用Clear site data即可
<img src="https://static001.geekbang.org/resource/image/0e/db/0eec0b6cd8a11e6e52af6595b02ac9db.png" alt="">
## 总结
今天我讲解了如何在浏览器中通过WebSQL来操作本地存储如果想使用SQL来管理和查询本地存储我们可以使用WebSQL通过三个核心的方法就可以方便让我们对数据库的连接事务处理以及SQL语句的执行来进行操作。我在Github上提供了操作的HTML代码如果还没有使用过WebSQL就快来使用下吧。
<img src="https://static001.geekbang.org/resource/image/df/b0/dfdfe930267465e4ec99adc4f73aefb0.png" alt=""><br>
我今天讲到了本地存储在浏览器中包括了Cookies、Local Storage、Session Storage、WebSQL和IndexedDB这5种形式的本地存储你能说下它们之间的区别么
最后是一道动手题请你使用WebSQL创建数据表heros并且插入5个以上的英雄数据字段为id、name、hp_max、mp_max、role_main。在HTML中添加一个输入框可以输入英雄的姓名并对该英雄的数据进行查询如下图所示
<img src="https://static001.geekbang.org/resource/image/dc/08/dcdefa40424e4e9910dbef9dd6938d08.png" alt=""><br>
欢迎你在评论区写下你的答案,我会和你一起交流,也欢迎把这篇文章分享给你的朋友或者同事,与他们一起交流一下。

View File

@@ -0,0 +1,211 @@
<audio id="audio" title="40丨SQLite为什么微信用SQLite存储聊天记录" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/47/e1/47738cbc6adc70f6ac54bb1eb1113be1.mp3"></audio>
我在上一篇文章中讲了WebSQL当我们在Chrome、Safari和Firefox等浏览器客户端中使用WebSQL时会直接操作SQLite。实际上SQLite本身是一个嵌入式的开源数据库引擎大小只有3M左右可以将整个SQLite嵌入到应用中而不用采用传统的客户端服务器Client/Server的架构。这样做的好处就是非常轻便在许多智能设备和应用中都可以使用SQLite比如微信就采用了SQLite作为本地聊天记录的存储。
今天我们就来深入了解一下SQLite今天的内容主要包括以下几方面
1. SQLite是什么它有哪些优点和不足
1. 如何在Python中使用SQLite
1. 如何编写SQL通过SQLite查找微信的聊天记录
## SQLite是什么
SQLite是在2000年发布的到目前为止已经有19年了。一直采用C语言编写采用C语言而非C++面向对象的方式可以提升代码底层的执行效率。但SQLite也有一些优势与不足。
它的优势在于非常轻量级存储数据非常高效查询和操作数据简单方便。此外SQLite不需要安装和配置有很好的迁移性能够嵌入到很多应用程序中与托管在服务器上的RDBMS相比约束少易操作可以有效减少服务器的压力。
不足在于SQLite常用于小到中型的数据存储不适用高并发的情况。比如在微信本地可以使用SQLite即使是几百M的数据文件使用SQLite也可以很方便地查找数据和管理但是微信本身的服务器就不能使用SQLite了因为SQLite同一时间只允许一个写操作吞吐量非常有限。
作为简化版的数据库SQLite没有用户管理功能在语法上也有一些自己的“方言”。比如在SQL中的SELECT语句SQLite可以使用一个特殊的操作符来拼接两个列。在MySQL中会使用函数concat而在SQLite、PostgreSQL、Oracle和Db2中使用||号比如SELECT `MesLocalID || Message FROM "Chat_1234"`
这个语句代表的是从Chat_1234数据表中查询MesLocalID和Message字段并且将他们拼接起来。
但是在SQLite中不支持RIGHT JOIN因此你需要将右外连接转换为左外连接也就是LEFT JOIN写成下面这样
```
SELECT * FROM team LEFT JOIN player ON player.team_id = team.team_id
```
除此以外SQLite仅支持只读视图也就是说我们只能创建和读取视图不能对它们的内容进行修改。
总的来说支持SQL标准的RDBMS语法都相似只是不同的DBMS会有一些属于自己的“方言”我们使用不同的DBMS的时候需要注意。
## 在Python中使用SQLite
我之前介绍过如何在Python中使用MySQL其中会使用到DB API规范如下图所示。基于DB API规范我们可以对数据库进行连接、交互以及异常的处理。
<img src="https://static001.geekbang.org/resource/image/ef/a4/efd39186177ed0537e6e75dccaf3cba4.png" alt=""><br>
在Python中使用SQLite也会使用到DB API规范与使用MySQL的交互方式一样也会用到connection、cursor和exceptions。在Python中集成了SQLite3直接加载相应的工具包就可以直接使用。下面我们就来看下如何在Python中使用SQLite。
在使用之前我们需要进行引用SQLite使用
```
import sqlite3
```
然后我们可以使用SQLite3创建数据库连接
```
conn = sqlite3.connect(&quot;wucai.db&quot;)
```
这里我们连接的是wucai.db这个文件如果没有这个文件存储上面的调用会自动在相应的工程路径里进行创建然后我们可以使用conn操作连接通过会话连接conn来创建游标
```
cur = conn.cursor()
```
通过这一步我们得到了游标cur然后可以使用execute()方法来执行各种DML比如插入删除更新等当然我们也可以进行SQL查询用的同样是execute()方法。
比如我们想要创建heros数据表以及相应的字段id、name、hp_max、mp_max、role_main可以写成下面这样
```
cur.execute(&quot;CREATE TABLE IF NOT EXISTS heros (id int primary key, name text, hp_max real, mp_max real, role_main text)&quot;)
```
在创建之后我们可以使用execute()方法来添加一条数据:
```
cur.execute('insert into heros values(?, ?, ?, ?, ?)', (10000, '夏侯惇', 7350, 1746, '坦克'))
```
需要注意的是一条一条插入数据太麻烦我们也可以批量插入这里会使用到executemany方法这时我们传入的参数就是一个元组比如
```
cur.executemany('insert into heros values(?, ?, ?, ?, ?)',
((10000, '夏侯惇', 7350, 1746, '坦克'),
(10001, '钟无艳', 7000, 1760, '战士'),
(10002, '张飞', 8341, 100, '坦克'),
(10003, '牛魔', 8476, 1926, '坦克'),
(10004, '吕布', 7344, 0, '战士')))
```
如果我们想要对heros数据表进行查询同样使用execute执行SQL语句
```
cur.execute(&quot;SELECT id, name, hp_max, mp_max, role_main FROM heros&quot;)
```
这时cur会指向查询结果集的第一个位置如果我们想要获取数据有以下几种方法
1. cur.fetchone()方法,获取一条记录;
1. cur.fetchmany(n) 方法获取n条记录
1. cur.fetchall()方法,获取全部数据行。
比如我想获取全部的结果集,可以写成这样:
```
result = cur.fetchall()
```
如果我们对事务操作完了,可以提交事务,使用`conn.commit()`即可。
同样,如果游标和数据库的连接都操作完了,可以对它们进行关闭:
```
cur.close()
conn.close()
```
上面这个过程的完整代码如下:
```
import sqlite3
# 创建数据库连接
conn = sqlite3.connect(&quot;wucai.db&quot;)
# 获取游标
cur = conn.cursor()
# 创建数据表
cur.execute(&quot;CREATE TABLE IF NOT EXISTS heros (id int primary key, name text, hp_max real, mp_max real, role_main text)&quot;)
# 插入英雄数据
cur.executemany('insert into heros values(?, ?, ?, ?, ?)',
((10000, '夏侯惇', 7350, 1746, '坦克'),
(10001, '钟无艳', 7000, 1760, '战士'),
(10002, '张飞', 8341, 100, '坦克'),
(10003, '牛魔', 8476, 1926, '坦克'),
(10004, '吕布', 7344, 0, '战士')))
cur.execute(&quot;SELECT id, name, hp_max, mp_max, role_main FROM heros&quot;)
result = cur.fetchall()
print(result)
# 提交事务
conn.commit()
# 关闭游标
cur.close()
# 关闭数据库连接
conn.close()
```
除了使用Python操作SQLite之外在整个操作过程中我们同样可以使用navicat数据库可视化工具来查看和管理SQLite。
<img src="https://static001.geekbang.org/resource/image/f2/fe/f2cb51591733d386239f843bd83c8afe.png" alt="">
## 通过SQLite查询微信的聊天记录
刚才我们提到很多应用都会集成SQLite作为客户端本地的数据库这样就可以避免通过数据库服务器进行交互减少服务器的压力。
如果你是iPhone手机不妨跟着我执行以下的步骤来查找下微信中的SQLite文件的位置吧。
第一步使用iTunes备份iPhone第二步在电脑中查找备份文件。
当我们备份好数据之后需要在本地找到备份的文件如果是windows可以在C:\Users\XXXX\AppData\Roaming\Apple Computer\MobileSync\Backup 这个路径中找到备份文件夹。
第三步查找Manifest.db。
在备份文件夹中会存在Manifest.db文件这个文件定义了苹果系统中各种备份所在的文件位置。
第四步查找MM.sqlite。
Manifest.db本身是SQLite数据文件通过SQLite我们能看到文件中包含了Files数据表这张表中有fileID、domain和relativePath等字段。
微信的聊天记录文件为MM.sqlite我们可以直接通过SQL语句来查询下它的位置也就是fileID
```
SELECT * FROM Files WHERE relativePath LIKE '%MM.sqlite'
```
<img src="https://static001.geekbang.org/resource/image/d1/52/d11e7857bbf1da3a6b4db9c34d561e52.png" alt="">
你能看到在我的微信备份中有2个MM.sqlite文件这些都是微信的聊天记录。
第五步分析找到的MM.sqlite。
这里我们需要在备份文件夹中查找相关的fileID比如f71743874d7b858a01e3ddb933ce13a9a01f79aa。
找到这个文件后我们可以复制一份取名为weixin.db这样就可以使用navicat对这个数据库进行可视化管理如下图所示
<img src="https://static001.geekbang.org/resource/image/64/27/6401a3e7bbcf7757b27233b777934327.png" alt=""><br>
微信会把你与每一个人的聊天记录都保存成一张数据表在数据表中会有MesLocalID、Message、Status等相应的字段它们分别代表在当前对话中的ID、聊天内容和聊天内容的状态
如果聊天对象很多的话,数据表也会有很多,如果想知道都有哪些聊天对象的数据表,可以使用:
```
SELECT name FROM sqlite_master WHERE type = 'table' AND name LIKE 'Chat\_%' escape '\'
```
这里需要说明的是sqlite_master是SQLite的系统表数据表是只读的里面保存了数据库中的数据表的名称。聊天记录的数据表都是以Chat_开头的因为`_`属于特殊字符在LIKE语句中会将`_`作为通配符。所以如果我们想要对开头为Chat_的文件名进行匹配就需要用escape对这个特殊字符做转义。
## 总结
我今天讲了有关SQLite的内容。在使用SQLite的时候需要注意SQLite有自己的方言比如在进行表连接查询的时候不支持RIGHT JOIN需要将其转换成LEFT JOIN等。同时我们在使用execute()方法的时候尽量采用带有参数的SQL语句以免被SQL注入攻击。
学习完今天的内容后不如试试用SQL查询来查找本地的聊天记录吧。
<img src="https://static001.geekbang.org/resource/image/b9/91/b932d2e3a31f908cf1de9b80f29e8891.png" alt=""><br>
最后留一道思考题吧。请你使用SQL查询对微信聊天记录中和“作业”相关的记录进行查找。不论是iPhone还是Android手机都可以找到相应的SQLite文件你可以使用Python对SQLite进行操作并输出结果。
欢迎你在评论区写下你的答案,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。

View File

@@ -0,0 +1,224 @@
<audio id="audio" title="41丨初识RedisRedis为什么会这么快" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/e9/50/e91e477ce3138808edf4e03508690250.mp3"></audio>
之前我们讲解了一些RDBMS的使用比如MySQL、Oracle、SQL Server和SQLite等实际上在日常工作中我们还会接触到一些NoSQL类型的数据库。如果对比RDBMS和NoSQL数据库你会发现RDBMS建立在关系模型基础上强调数据的一致性和各种约束条件而NoSQL的规则是“只提供你想要的”数据模型灵活查询效率高成本低。但同时相比RDBMSNoSQL数据库没有统一的架构和标准语言每种数据库之间差异较大各有所长。
今天我们要讲解的Redis属于键值key-value数据库键值数据库会使用哈希表存储键值和数据其中key作为唯一的标识而且key和value可以是任何的内容不论是简单的对象还是复杂的对象都可以存储。键值数据库的查询性能高易于扩展。
今天我们就来了解下Redis具体的内容包括以下几个方面
1. Redis是什么为什么使用Redis会非常快
1. Redis支持的数据类型都有哪些
1. 如何通过Python和Redis进行交互
## Redis是什么为什么这么快
Redis全称是REmote DIctionary Server从名字中你也能看出来它用字典结构存储数据也就是key-value类型的数据。
Redis的查询效率非常高根据官方提供的数据Redis每秒最多处理的请求可以达到10万次。
为什么这么快呢?
Redis采用ANSI C语言编写它和SQLite一样。采用C语言进行编写的好处是底层代码执行效率高依赖性低因为使用C语言开发的库没有太多运行时Runtime依赖而且系统的兼容性好稳定性高。
此外Redis是基于内存的数据库我们之前讲到过这样可以避免磁盘I/O因此Redis也被称为缓存工具。
其次数据结构结构简单Redis采用Key-Value方式进行存储也就是使用Hash结构进行操作数据的操作复杂度为O(1)。
但Redis快的原因还不止这些它采用单进程单线程模型这样做的好处就是避免了上下文切换和不必要的线程之间引起的资源竞争。
在技术上Redis还采用了多路I/O复用技术。这里的多路指的是多个socket网络连接复用指的是复用同一个线程。采用多路I/O复用技术的好处是可以在同一个线程中处理多个I/O请求尽量减少网络I/O的消耗提升使用效率。
## Redis的数据类型
相比MemcachedRedis有一个非常大的优势就是支持多种数据类型。Redis支持的数据类型包括字符串、哈希、列表、集合、有序集合等。
字符串类型是Redis提供的最基本的数据类型对应的结构是key-value。
如果我们想要设置某个键的值,使用方法为`set key value`比如我们想要给name这个键设置值为zhangfei可以写成`set name zhangfei`。如果想要取某个键的值,可以使用`get key`比如想取name的值写成get name即可。
<img src="https://static001.geekbang.org/resource/image/55/30/554243f80e4029e82ffd60c4b0303030.png" alt=""><br>
哈希hash提供了字段和字段值的映射对应的结构是key-field-value。
如果我们想要设置某个键的哈希值,可以使用`hset key field value`如果想要给user1设置username为zhangfeiage为28可以写成下面这样
```
hset user1 username zhangfei
hset user1 age 28
```
如果我们想要同时将多个field-value设置给某个键key的时候可以使用`hmset key field value [field value...]`,比如上面这个可以写成:
```
Hmset user1 username zhangfei age 28
```
如果想要取某个键的某个field字段值可以使用`hget key field`比如想要取user1的username那么写成`hget user1 username`即可。
如果想要一次获取某个键的多个field字段值可以使用`hmget key field[field...]`比如想要取user1的username和age可以写成`hmget user1 username age`
<img src="https://static001.geekbang.org/resource/image/4a/3c/4aac95f8536f67f97f1c913a3633aa3c.png" alt=""><br>
字符串列表list的底层是一个双向链表结构所以我们可以向列表的两端添加元素时间复杂度都为O(1),同时我们也可以获取列表中的某个片段。
如果想要向列表左侧增加元素可以使用:`LPUSH key value [...]`比如我们给heroList列表向左侧添加zhangfei、guanyu和liubei这三个元素可以写成
```
LPUSH heroList zhangfei guanyu liubei
```
同样,我们也可以使用`RPUSH key value [...]`向列表右侧添加元素比如我们给heroList列表向右侧添加dianwei、lvbu这两个元素可以写成下面这样
```
RPUSH heroList dianwei lvbu
```
如果我们想要获取列表中某一片段的内容,使用`LRANGE key start stop`即可比如我们想要获取heroList从0到4位置的数据写成`LRANGE heroList 0 4`即可。
<img src="https://static001.geekbang.org/resource/image/d8/26/d87d2a6b0f858b0a5c88aa4417707a26.png" alt=""><br>
字符串集合set是字符串类型的无序集合与列表list的区别在于集合中的元素是无序的同时元素不能重复。
如果想要在集合中添加元素,可以使用`SADD key member [...]`比如我们给heroSet集合添加zhangfei、guanyu、liubei、dianwei和lvbu这五个元素可以写成
```
SADD heroSet zhangfei guanyu liubei dianwei lvbu
```
如果想要在集合中删除某元素,可以使用`SREM key member [...]`比如我们从heroSet集合中删除liubei和lvbu这两个元素可以写成
```
SREM heroSet liubei lvbu
```
如果想要获取集合中所有的元素,可以使用`SMEMBERS key`比如我们想要获取heroSet集合中的所有元素写成`SMEMBERS heroSet`即可。
如果想要判断集合中是否存在某个元素,可以使用`SISMEMBER key member`比如我们想要判断heroSet集合中是否存在zhangfei和liubei就可以写成下面这样
```
SISMEMBER heroSet zhangfei
SISMEMBER heroSet liubei
```
<img src="https://static001.geekbang.org/resource/image/e6/0f/e69e7249b48194e0f76a8f351287390f.png" alt=""><br>
我们可以把有序字符串集合SortedSet简称ZSET理解成集合的升级版。实际上ZSET是在集合的基础上增加了一个分数属性这个属性在添加修改元素的时候可以被指定。每次指定后ZSET都会按照分数来进行自动排序也就是说我们在给集合key添加member的时候可以指定score。
有序集合与列表有一定的相似性比如这两种数据类型都是有序的都可以获得某一范围的元素。但它俩在数据结构上有很大的不同首先列表list是通过双向链表实现的在操作左右两侧的数据时会非常快而对于中间的数据操作则相对较慢。有序集合采用hash表的结构来实现读取排序在中间部分的数据也会很快。同时有序集合可以通过score来完成元素位置的调整但如果我们想要对列表进行元素位置的调整则会比较麻烦。
如果我们想要在有序集合中添加元素和分数,使用`ZADD key score member [...]`比如我们给heroScore集合添加下面5个英雄的hp_max数值如下表所示
<img src="https://static001.geekbang.org/resource/image/2b/54/2b8db8619d37452b4608e8dbe91cba54.png" alt=""><br>
那么我们可以写成下面这样:
```
ZADD heroScore 8341 zhangfei 7107 guanyu 6900 liubei 7516 dianwei 7344 lvbu
```
如果我们想要获取某个元素的分数,可以使用`ZSCORE key member`比如我们想要获取guanyu的分数写成`ZSCORE heroScore guanyu`即可。
如果我们想要删除一个或多元素可以使用ZREM key member [member …]比如我们想要删除guanyu这个元素使用`ZREM heroScore guanyu`即可。
我们也可以获取某个范围的元素列表。如果想要分数从小到大进行排序,使用`ZRANGE key start stop [WITHSCORES]`,如果分数从大到小进行排序,使用`ZREVRANGE key start stop [WITHSCORES]`。需要注意的是WITHSCORES是个可选项如果使用WITHSCORES会将分数一同显示出来比如我们想要查询heroScore这个有序集合中分数排名前3的英雄及数值写成`ZREVRANGE heroScore 0 2 WITHSCORES`即可。
<img src="https://static001.geekbang.org/resource/image/10/85/106083c4b4872fadb6b91f46b3e74485.png" alt=""><br>
除了这5种数据类型以外Redis还支持位图Bitmaps数据结构在2.8版本之后增加了基数统计HyperLogLog3.2版本之后加入了地理空间Geospatial以及索引半径查询的功能在5.0版本引用了数据流Streams数据类型。
## 如何使用Redis
我们可以在Python中直接操作Redis在使用前需要使用`pip install redis`安装工具包安装好之后在使用前我们需要使用import redis进行引用。
在Python中提供了两种连接Redis的方式第一种是直接连接使用下面这行命令即可。
```
r = redis.Redis(host='localhost', port= 6379)
```
第二种是连接池方式。
```
pool = redis.ConnectionPool(host='localhost', port=6379)
r = redis.Redis(connection_pool=pool)
```
你可能会有疑问这两种连接方式有什么不同直接连接可能会耗费掉很多资源。通常情况下我们在连接Redis的时候可以创建一个Redis连接通过它来完成Redis操作完成之后再释放掉。但是在高并发的情况下这样做非常不经济因为每次连接和释放都需要消耗非常多的资源。
### 为什么采用连接池机制
基于直接连接的弊端Redis提供了连接池的机制这个机制可以让我们事先创建好多个连接将其放到连接池中当我们需要进行Redis操作的时候就直接从连接池中获取完成之后也不会直接释放掉连接而是将它返回到连接池中。
连接池机制可以避免频繁创建和释放连接,提升整体的性能。
### 连接池机制的原理
在连接池的实例中会有两个list保存的是`_available_connections``_in_use_connections`,它们分别代表连接池中可以使用的连接集合和正在使用的连接集合。当我们想要创建连接的时候,可以从`_available_connections`中获取一个连接进行使用,并将其放到`_in_use_connections`中。如果没有可用的连接,才会创建一个新连接,再将其放到`_in_use_connections`中。如果连接使用完毕,会从`_in_use_connections`中删除,添加到`_available_connections`中,供后续使用。
Redis库提供了Redis和StrictRedis类它们都可以实现Redis命令不同之处在于Redis是StrictRedis的子类可以对旧版本进行兼容。如果我们想要使用连接池机制然后用StrictRedis进行实例化可以写成下面这样
```
import redis
pool = redis.ConnectionPool(host='localhost', port=6379)
r = redis.StrictRedis(connection_pool=pool)
```
### 实验使用Python统计Redis进行1万次写请求和1万次读请求的时间
了解了如何使用Python创建Redis连接之后我们再来看下怎样使用Python对Redis进行数据的写入和读取。这里我们使用HMSET函数同时将多个`field-value`值存入到键中。模拟1万次的写请求里设置了不同的key和相同的`field-value`然后在1万次读请求中将这些不同的key中保存的`field-value`值读取出来。具体代码如下:
```
import redis
import time
# 创建redis连接
pool = redis.ConnectionPool(host='localhost', port=6379)
r = redis.StrictRedis(connection_pool=pool)
# 记录当前时间
time1 = time.time()
# 1万次写
for i in range(10000):
data = {'username': 'zhangfei', 'age':28}
r.hmset(&quot;users&quot;+str(i), data)
# 统计写时间
delta_time = time.time()-time1
print(delta_time)
# 统计当前时间
time1 = time.time()
# 1万次读
for i in range(10000):
result = r.hmget(&quot;users&quot;+str(i), ['username', 'age'])
# 统计读时间
delta_time = time.time()-time1
print(delta_time)
```
运行结果:
```
2.0041146278381348
0.9920568466186523
```
你能看到1万次写请求差不多用时2秒钟而1万次读请求用时不到1秒钟读写效率还是很高的。
## 总结
NoSQL数据库种类非常多了解Redis是非常有必要的在实际工作中我们也经常将RDBMS和Redis一起使用优势互补。
作为常见的NoSQL数据库Redis支持的数据类型比Memcached丰富得多在I/O性能上Redis采用的是单线程I/O复用模型而Memcached是多线程可以利用多核优势。而且在持久化上Redis提供了两种持久化的模式可以让数据永久保存这是Memcached不具备的。
<img src="https://static001.geekbang.org/resource/image/a1/67/a15c68e276f962169336ffd26bd22867.png" alt=""><br>
你不妨思考一下为什么Redis采用了单线程工作模式有哪些机制可以保证Redis即使采用单线程模式效率也很高呢
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。

View File

@@ -0,0 +1,168 @@
<audio id="audio" title="42丨如何使用Redis来实现多用户抢票问题" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/b8/36/b80b8d71b133cacfdd5e7d924d345236.mp3"></audio>
在上一篇文章中我们已经对Redis有了初步的认识了解到Redis采用Key-Value的方式进行存储在Redis内部使用的是redisObject对象来表示所有的key和value。同时我们还了解到Redis本身用的是单线程的机制采用了多路I/O复用的技术在处理多个I/O请求的时候效率很高。
今天我们来更加深入地了解一下Redis的原理内容包括以下几方面
1. Redis的事务处理机制是怎样的与RDBMS有何不同
1. Redis的事务处理的命令都有哪些如何使用它们完成事务操作
1. 如何使用Python的多线程机制和Redis的事务命令模拟多用户抢票
## Redis的事务处理机制
在此之前让我们先来回忆下RDBMS中事务满足的4个特性ACID它们分别代表原子性、一致性、隔离性和持久性。
Redis的事务处理与RDBMS的事务有一些不同。
首先Redis不支持事务的回滚机制Rollback这也就意味着当事务发生了错误只要不是语法错误整个事务依然会继续执行下去直到事务队列中所有命令都执行完毕。在[Redis官方文档](https://redis.io/topics/transactions)中说明了为什么Redis不支持事务回滚。
只有当编程语法错误的时候Redis命令执行才会失败。这种错误通常出现在开发环境中而很少出现在生产环境中没有必要开发事务回滚功能。
另外Redis是内存数据库与基于文件的RDBMS不同通常只进行内存计算和操作无法保证持久性。不过Redis也提供了两种持久化的模式分别是RDB和AOF模式。
RDBRedis DataBase持久化可以把当前进程的数据生成快照保存到磁盘上触发RDB持久化的方式分为手动触发和自动触发。因为持久化操作与命令操作不是同步进行的所以无法保证事务的持久性。
AOFAppend Only File持久化采用日志的形式记录每个写操作弥补了RDB在数据一致性上的不足但是采用AOF模式就意味着每条执行命令都需要写入文件中会大大降低Redis的访问性能。启用AOF模式需要手动开启有3种不同的配置方式默认为everysec也就是每秒钟同步一次。其次还有always和no模式分别代表只要有数据发生修改就会写入AOF文件以及由操作系统决定什么时候记录到AOF文件中。
虽然Redis提供了两种持久化的机制但是作为内存数据库持久性并不是它的擅长。
Redis是单线程程序在事务执行时不会中断事务其他客户端提交的各种操作都无法执行因此你可以理解为Redis的事务处理是串行化的方式总是具有隔离性的。
## Redis的事务处理命令
了解了Redis的事务处理机制之后我们来看下Redis的事务处理都包括哪些命令。
1. MULTI开启一个事务
1. EXEC事务执行将一次性执行事务内的所有命令
1. DISCARD取消事务
1. WATCH监视一个或多个键如果事务执行前某个键发生了改动那么事务也会被打断
1. UNWATCH取消WATCH命令对所有键的监视。
需要说明的是Redis实现事务是基于COMMAND队列如果Redis没有开启事务那么任何的COMMAND都会立即执行并返回结果。如果Redis开启了事务COMMAND命令会放到队列中并且返回排队的状态QUEUED只有调用EXEC才会执行COMMAND队列中的命令。
比如我们使用事务的方式存储5名玩家所选英雄的信息代码如下
```
MULTI
hmset user:001 hero 'zhangfei' hp_max 8341 mp_max 100
hmset user:002 hero 'guanyu' hp_max 7107 mp_max 10
hmset user:003 hero 'liubei' hp_max 6900 mp_max 1742
hmset user:004 hero 'dianwei' hp_max 7516 mp_max 1774
hmset user:005 hero 'diaochan' hp_max 5611 mp_max 1960
EXEC
```
你能看到在MULTI和EXEC之间的COMMAND命令都会被放到COMMAND队列中并返回排队的状态只有当EXEC调用时才会一次性全部执行。
<img src="https://static001.geekbang.org/resource/image/4a/06/4aa62797167f41599b9e514d77fc0a06.png" alt=""><br>
我们经常使用Redis的WATCH和MULTI命令来处理共享资源的并发操作比如秒杀抢票等。实际上WATCH+MULTI实现的是乐观锁。下面我们用两个Redis客户端来模拟下抢票的流程。
<img src="https://static001.geekbang.org/resource/image/95/41/95e294bfb6843ef65beff61ca0bc3a41.png" alt=""><br>
我们启动Redis客户端1执行上面的语句然后在执行EXEC前等待客户端2先完成上面的执行客户端2的结果如下
<img src="https://static001.geekbang.org/resource/image/eb/1b/ebbadb4698e80d81dbf7c62a21dbec1b.png" alt=""><br>
然后客户端1执行EXEC结果如下
<img src="https://static001.geekbang.org/resource/image/6b/f8/6b23c9efcdbe1f349299fc32d41ab0f8.png" alt=""><br>
你能看到实际上最后一张票被客户端2抢到了这是因为客户端1WATCH的票的变量在EXEC之前发生了变化整个事务就被打断返回空回复nil
需要说明的是MULTI后不能再执行WATCH命令否则会返回WATCH inside MULTI is not allowed错误因为WATCH代表的就是在执行事务前观察变量是否发生了改变如果变量改变了就将事务打断所以在事务执行之前也就是MULTI之前使用WATCH。同时如果在执行命令过程中有语法错误Redis也会报错整个事务也不会被执行Redis会忽略运行时发生的错误不会影响到后面的执行。
## 模拟多用户抢票
我们刚才讲解了Redis的事务命令并且使用Redis客户端的方式模拟了两个用户抢票的流程。下面我们使用Python继续模拟一下这个过程这里需要注意三点。
在Python中Redis事务是通过pipeline封装而实现的因此在创建Redis连接后需要获取管道pipeline然后通过pipeline使用WATCH、MULTI和EXEC命令。
其次用户是并发操作的因此我们需要使用到Python的多线程这里使用threading库来创建多线程。
对于用户的抢票我们设置了sell函数用于模拟用户i的抢票。在执行MULTI前我们需要先使用pipe.watch(KEY)监视票数如果票数不大于0则说明票卖完了用户抢票失败如果票数大于0证明可以抢票再执行MULTI将票数减1并进行提交。不过在提交执行的时候可能会失败这是因为如果监视的KEY发生了改变则会产生异常我们可以通过捕获异常来提示用户抢票失败重试一次。如果成功执行事务则提示用户抢票成功显示当前的剩余票数。
具体代码如下:
```
import redis
import threading
# 创建连接池
pool = redis.ConnectionPool(host = '127.0.0.1', port=6379, db=0)
# 初始化 redis
r = redis.StrictRedis(connection_pool = pool)
# 设置KEY
KEY=&quot;ticket_count&quot;
# 模拟第i个用户进行抢票
def sell(i):
# 初始化 pipe
pipe = r.pipeline()
while True:
try:
# 监视票数
pipe.watch(KEY)
# 查看票数
c = int(pipe.get(KEY))
if c &gt; 0:
# 开始事务
pipe.multi()
c = c - 1
pipe.set(KEY, c)
pipe.execute()
print('用户 {} 抢票成功,当前票数 {}'.format(i, c))
break
else:
print('用户 {} 抢票失败,票卖完了'.format(i))
break
except Exception as e:
print('用户 {} 抢票失败,重试一次'.format(i))
continue
finally:
pipe.unwatch()
if __name__ == &quot;__main__&quot;:
# 初始化5张票
r.set(KEY, 5)
# 设置8个人抢票
for i in range(8):
t = threading.Thread(target=sell, args=(i,))
t.start()
```
运行结果:
```
用户 0 抢票成功,当前票数 4
用户 4 抢票失败,重试一次
用户 1 抢票成功,当前票数 3
用户 2 抢票成功,当前票数 2
用户 4 抢票失败,重试一次
用户 5 抢票失败,重试一次
用户 6 抢票成功,当前票数 1
用户 4 抢票成功,当前票数 0
用户 5 抢票失败,重试一次
用户 3 抢票失败,重试一次
用户 7 抢票失败,票卖完了
用户 5 抢票失败,票卖完了
用户 3 抢票失败,票卖完了
```
在Redis中不存在悲观锁事务处理要考虑到并发请求的情况我们需要通过WATCH+MULTI的方式来实现乐观锁如果监视的KEY没有发生变化则可以顺利执行事务否则说明事务的安全性已经受到了破坏服务器就会放弃执行这个事务直接向客户端返回空回复nil事务执行失败后我们可以重新进行尝试。
## 总结
今天我讲解了Redis的事务机制Redis事务是一系列Redis命令的集合事务中的所有命令都会按照顺序进行执行并且在执行过程中不会受到其他客户端的干扰。不过在事务的执行中Redis可能会遇到下面两种错误的情况
首先是语法错误也就是在Redis命令入队时发生的语法错误。Redis在事务执行前不允许有语法错误如果出现则会导致事务执行失败。如官方文档所说通常这种情况在生产环境中很少出现一般会发生在开发环境中如果遇到了这种语法错误就需要开发人员自行纠错。
第二个是执行时错误也就是在事务执行时发生的错误比如处理了错误类型的键等这种错误并非语法错误Redis只有在实际执行中才能判断出来。不过Redis不提供回滚机制因此当发生这类错误时Redis会继续执行下去保证其他命令的正常执行。
在事务处理中我们需要通过锁的机制来解决共享资源并发访问的情况。在Redis中提供了WATCH+MULTI的乐观锁方式。我们之前了解过乐观锁是一种思想它是通过程序实现的锁机制在数据更新的时候进行判断成功就执行不成功就失败不需要等待其他事务来释放锁。事实上在在Redis的设计中处处体现了这种乐观、简单的设计理念。
<img src="https://static001.geekbang.org/resource/image/e3/83/e3ae78a3220320015cb3e43c642ea683.png" alt=""><br>
最后我们一起思考两个问题吧。Redis既然是单线程程序在执行事务过程中按照顺序执行为什么还会用WATCH+MULTI的方式来实现乐观锁的并发控制呢
我们在进行抢票模拟的时候列举了两个Redis客户端的例子当WATCH的键ticket发生改变的时候事务就会被打断。这里我将客户端2的SET ticket设置为1也就是ticket的数值没有发生变化请问此时客户端1和客户端2的执行结果是怎样的为什么
<img src="https://static001.geekbang.org/resource/image/d4/44/d4bb30f5d415ea93980c465e4f110544.png" alt=""><br>
欢迎你在评论区写下你的思考,我会和你一起交流,也欢迎你把这篇文章分享给你的朋友或者同事,一起交流一下。

View File

@@ -0,0 +1,305 @@
<audio id="audio" title="43丨如何使用Redis搭建玩家排行榜" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/cc/2f/cc79c6347073a267a68c4bac092b9a2f.mp3"></audio>
上一篇文章中我们使用Redis模拟了多用户抢票的问题这里再回顾一下原理。我们通过使用WATCH+MULTI的方式实现乐观锁机制对ticket_count这个键进行监视当这个键发生变化的时候事务就会被打断重新请求这样做的好处就是可以保证事务对键进行操作的原子性当然我们也可以使用Redis的incr和decr来实现键的原子性递增或递减。
今天我们用Redis搭建一个玩家的排行榜假设一个服务器存储了10万名玩家的数据我们想给这个区这台服务器上的玩家做个全区的排名该如何用Redis实现呢
不妨一起来思考下面几个问题:
1. MySQL是如何实现玩家排行榜的有哪些难题需要解决
1. 如何用Redis模拟10万名玩家数据Redis里的Lua又是什么
1. Redis如何搭建玩家排行榜和MySQL相比有什么优势
## 使用MySQL搭建玩家排行榜
我们如果用MySQL搭建玩家排行榜的话首先需要生成10万名玩家的数据这里我们使用之前学习过的存储过程来模拟。
为了简化玩家排行榜主要包括3个字段user_id、score、和create_time它们分别代表玩家的用户ID、玩家的积分和玩家的创建时间。
### 王者荣耀英雄等级说明
这里我们可以模拟王者荣耀的英雄等级,具体等级标准如下:
<img src="https://static001.geekbang.org/resource/image/f1/4d/f10fb49dde602525a65270c6b2af884d.png" alt=""><br>
如果想要英雄要达到最强王者的段位那么之前需要积累112颗9+12+16+25+25+25星星而达到最强王者之后还可以继续积累无上限的星星。在随机数模拟上我们也分成两个阶段第一个阶段模拟英雄的段位我们使用随机数来模拟score数值范围是1-112之间当score=112的时候再模拟最强王者等级中的星星个数。如果我们只用一个随机数进行模拟会出现最强王者的比例变大的情况显然不符合实际情况。
### 使用存储过程模拟10万名玩家数据
这里我们使用存储过程,具体代码如下:
```
CREATE DEFINER=`root`@`localhost` PROCEDURE `insert_many_user_scores`(IN START INT(10), IN max_num INT(10))
BEGIN
DECLARE i INT DEFAULT 0;
-- 模拟玩家英雄的星星数
DECLARE score INT;
DECLARE score2 INT;
-- 初始注册时间
DECLARE date_start DATETIME DEFAULT ('2017-01-01 00:00:00');
-- 每个玩家的注册时间
DECLARE date_temp DATETIME;
SET date_temp = date_start;
SET autocommit=0;
REPEAT
SET i=i+1;
SET date_temp = date_add(date_temp, interval RAND()*60 second);
-- 1-112随机数
SET score = CEIL(RAND()*112);
-- 如果达到了王者,继续模拟王者的星星数
IF score = 112 THEN
SET score2 = FLOOR(RAND()*100);
SET score = score + score2;
END IF;
-- 插入新玩家
INSERT INTO user_score(user_id, score, create_time) VALUES((START+i), score, date_temp);
UNTIL i = max_num
END REPEAT;
COMMIT;
END
```
然后我们使用`call insert_many_user_scores(10000,100000);`模拟生成10万名玩家的得分数据。注意在insert之前需要先设置`autocommit=0`也就是关闭了自动提交然后在批量插入结束之后再手动进行COMMIT这样做的好处是可以进行批量提交提升插入效率。你可以看到整体的用时为5.2秒。
<img src="https://static001.geekbang.org/resource/image/2d/5f/2d0a227b72da92e0ab79da59be28915f.png" alt="">
如上代码所示我用score来模拟第一阶段的星星数如果score达到了112再来模拟score2的分数这里我限定最强王者阶段的星星个数上限为100。同时我们还模拟了用户注册的时间这是因为排行榜可以有两种表示方式第二种方式需要用到这个时间。
第一种表示方式为并列排行榜,也就是分数相同的情况下,允许排名并列,如下所示:
<img src="https://static001.geekbang.org/resource/image/6e/2e/6edf318c5f4cbe7b48f6da3b65d2982e.png" alt=""><br>
第二种为严格排行榜。当分数相同的时候,会按照第二条件来进行排序,比如按照注册时间的长短,注册时间越长的排名越靠前。这样的话,上面那个排行榜就会变成如下所示的严格排行榜。
<img src="https://static001.geekbang.org/resource/image/6e/7e/6ec6befb0ffbc2a7a8682d309f96217e.png" alt=""><br>
你能看到当10013和10015得分相同的时候如果按照注册时间来进行排名的话会将10013排到10015前面。
上面的数据仅仅为示意下面我们用实际的10万条数据做一个严格排行榜你可以点击[下载地址](https://github.com/cystanford/mysql_user_scores)下载这10万条数据 也可以自己使用上面的存储过程来进行模拟首先使用SQL语句进行查询
```
SELECT (@rownum := @rownum + 1) AS user_rank, user_id, score, create_time
FROM user_score, (SELECT @rownum := 0) b
ORDER BY score DESC, create_time ASC
```
运行结果如下10万条数据用时0.17s
<img src="https://static001.geekbang.org/resource/image/14/f8/14d88fd5293ac312e585ec05529473f8.png" alt=""><br>
这里有几点需要说明。
MySQL不像Oracle一样自带rownum统计行编号的功能所以这里我们需要自己来实现rownum功能也就是设置MySQL的变量`@rownum`,初始化为`@rownum :=0`然后每次SELECT一条数据的时候都自动加1。
通过开发程序比如Python、PHP和Java等统计排名会更方便这里同样需要初始化一个变量比如`rownum=0`然后每次fetch一条数据的时候都将该变量加1作为记录的排名。同时开发程序也可以很方便地实现并列排名因为程序可以进行上下文的统计当两名玩家得分相同时排名相同否则排名会顺序加1。
如果想要通过SQL来实现可以写成下面这样
```
SELECT user_id, score,
IFNULL((SELECT COUNT(*) FROM user_score WHERE score &gt; t.score), 0) + 1 AS user_rank
FROM user_score t
ORDER BY user_rank ASC
```
这样做的原理是查找比当前分数大的数据行数然后加1但是这样执行效率会很低相当于需要对每个玩家都统计一遍排名。
## Lua是什么如何在Redis中使用
知道如何用MySQL模拟数据后我们再来看下如何在Redis中完成这一步。事实上Redis本身不提供存储过程的功能不过在2.6版本之后集成了Lua语言可以很方便地实现类似存储过程的函数调用方式。
Lua是一个小巧的脚本语言采用标准C语言编写一个完整的Lua解析器大小只有200K。我们之前讲到过采用标准C语言编写的好处就在于执行效率高依懒性低同时兼容性好稳定性高。这些特性同样Lua也有它可以嵌入到各种应用程序中提供灵活的扩展和定制功能。
### 如何在Redis中使用Lua
在Redis中使用Lua脚本的命令格式如下
```
EVAL script numkeys key [key ...] arg [arg ...]
```
我来说明下这些命令中各个参数代表的含义。
1. script代表的是Lua的脚本内容。
1. numkeys代表后续参数key的个数。
1. key就是我们要操作的键可以是多个键。我们在Lua脚本中可以直接使用这些key直接通过`KEYS[1]``KEYS[2]`来获取默认下标是从1开始。
1. arg表示传入到Lua脚本中的参数就像调用函数传入的参数一样。在Lua脚本中我们可以通过`ARGV[1]``ARGV[2]`来进行获取同样默认下标从1开始。
下面我们通过2个例子来体会下比如我们使用eval `"return {ARGV[1], ARGV[2]}" 0 cy 123`代表的是传入的key的个数为0后面有两个arg分别为cy和123。在Lua脚本中我们直接返回这两个参数`ARGV[1]`, `ARGV[2]`,执行结果如下:
<img src="https://static001.geekbang.org/resource/image/f8/76/f8b92089f5eed056fdf49eae53c2d576.png" alt=""><br>
比如我们要用这一条语句:
```
eval &quot;math.randomseed(ARGV[1]); local temp = math.random(1,112); redis.call('SET', KEYS[1], temp); return 'ok';&quot; 1 score 30
```
这条语句代表的意思是我们传入KEY的个数为1参数是scorearg参数为30。在Lua脚本中使用`ARGV[1]`也就是30作为随机数的种子然后创建本地变量temp等于1到112之间的随机数再使用SET方法对KEY也就是用刚才创建的随机数对score这个字段进行赋值结果如下
<img src="https://static001.geekbang.org/resource/image/b5/be/b569fe5c81ae82bd8e38cc2a93df72be.png" alt=""><br>
然后我们在Redis中使用`GET score`对刚才设置的随机数进行读取结果为34。
另外我们还可以在命令中调用Lua脚本使用的命令格式
```
redis-cli --eval lua_file key1 key2 , arg1 arg2 arg3
```
使用redis-cli的命令格式不需要输入key的个数在key和arg参数之间采用了逗号进行分割注意逗号前后都需要有空格。同时在eval后面可以带一个lua文件以.lua结尾
## 使用Lua创建10万名玩家数据
如果我们想要通过Lua脚本创建10万名玩家的数据文件名为`insert_user_scores.lua`,代码如下:
```
--设置时间种子
math.randomseed(ARGV[1])
-- 设置初始的生成时间
local create_time = 1567769563 - 3600*24*365*2.0
local num = ARGV[2]
local user_id = ARGV[3]
for i=1, num do
--生成1到60之间的随机数
local interval = math.random(1, 60)
--产生1到112之间的随机数
local temp = math.random(1, 112)
if (temp == 112) then
--产生0到100之间的随机数
temp = temp + math.random(0, 100)
end
create_time = create_time + interval
temp = temp + create_time / 10000000000
redis.call('ZADD', KEYS[1], temp, user_id+i-1)
end
return 'Generation Completed'
```
上面这段代码可以实现严格排行榜的排名具体方式是将score进行了改造score 为浮点数。整数部分为得分,小数部分为时间差。
在调用的时候,我们通过`ARGV[1]`获取时间种子的参数,传入的`KEYS[1]``user_score`,也就是创建有序集合`user_score`。然后通过num来设置生成玩家的数量通过`user_id`获取初始的`user_id`。最后调用如下命令完成玩家数据的创建:
```
redis-cli -h localhost -p 6379 --eval insert_user_scores.lua user_score , 30 100000 10000
```
<img src="https://static001.geekbang.org/resource/image/ba/94/ba8fcef75a2ea951dd31d9f7e266d094.png" alt="">
### 使用Redis实现玩家排行榜
我们通过Lua脚本模拟完成10万名玩家数据并将其存储在了Redis的有序集合`user_score`下面我们就来使用Redis来统计玩家排行榜的数据。
首先我们需要思考的是,一个典型的游戏排行榜都包括哪些功能呢?
1. 统计全部玩家的排行榜
1. 按名次查询排名前N名的玩家
1. 查询某个玩家的分数
1. 查询某个玩家的排名
1. 对玩家的分数和排名进行更新
1. 查询指定玩家前后M名的玩家
1. 增加或移除某个玩家,并对排名进行更新
在Redis中实现上面的功能非常简单只需要使用Redis我们提供的方法即可针对上面的排行榜功能需求我们分别来看下Redis是如何实现的。
### 统计全部玩家的排行榜
在Redis里统计全部玩家的排行榜的命令格式为`ZREVRANGE 排行榜名称 起始位置 结束位置 [WITHSCORES]`
我们使用这行命令即可:
```
ZREVRANGE user_score 0 -1 WITHSCORES
```
我们对玩家排行榜`user_score`进行统计,其中-1代表的是全部的玩家数据`WITHSCORES`代表的是输出排名的同时也输出分数。
### 按名次查询排名前N名的玩家
同样我们可以使用`ZREVRANGE`完成前N名玩家的排名比如我们想要统计前10名玩家可以使用`ZREVRANGE user_score 0 9`
<img src="https://static001.geekbang.org/resource/image/2e/3c/2e87be9528653b388f8a3dabedfcf23c.png" alt="">
### 查询某个玩家的分数
命令格式为`ZSCORE 排行榜名称 玩家标识`
时间复杂度为`O(1)`
如果我们想要查询玩家10001的分数可以使用`ZSCORE user_score 10001`
<img src="https://static001.geekbang.org/resource/image/74/95/742b794aa98bb9cf100cce8070a8f295.png" alt="">
### 查询某个玩家的排名
命令格式为`ZREVRANK 排行榜名称 玩家标识`
时间复杂度为`O(log(N))`
如果我们想要查询玩家10001的排名可以使用`ZREVRANK user_score 10001`
<img src="https://static001.geekbang.org/resource/image/c8/1f/c8a13f56fc4e151dd9804a9e317da91f.png" alt="">
### 对玩家的分数进行更新,同时排名进行更新
如果我们想要对玩家的分数进行增减,命令格式为`ZINCRBY 排行榜名称 分数变化 玩家标识`
时间复杂度为`O(log(N))`
比如我们想对玩家10001的分数减1可以使用`ZINCRBY user_score -1 10001`
<img src="https://static001.geekbang.org/resource/image/a6/14/a644a5aa7019b1cb3b7602bca4749614.png" alt=""><br>
然后我们再来查看下玩家10001的排名使用`ZREVRANK user_score 10001`
<img src="https://static001.geekbang.org/resource/image/51/54/51256aa0c27547ae508fc05049b2d554.png" alt=""><br>
你能看到排名由17153降到了18036名。
### 查询指定玩家前后M名的玩家
比如我们想要查询玩家10001前后5名玩家都是谁当前已知玩家10001的排名是18036那么可以使用`ZREVRANGE user_score 18031 18041`
<img src="https://static001.geekbang.org/resource/image/54/91/54eae0a13dea61d15752469c9d42e591.png" alt=""><br>
这样就可以得到玩家10001前后5名玩家的信息。
**增加或删除某个玩家,并对排名进行更新**
如果我们想要删除某个玩家,命令格式为`ZREM 排行榜名称 玩家标识`
时间复杂度为`O(log(N))`
比如我们想要删除玩家10001可以使用`ZREM user_score 10001`
<img src="https://static001.geekbang.org/resource/image/fe/d4/fee0dc8d42ca239427fe136375bda0d4.png" alt=""><br>
这样我们再来查询下排名在18031到18041的玩家是谁使用`ZREVRANGE user_score 18031 18041`
<img src="https://static001.geekbang.org/resource/image/c0/85/c06a95d1ef82cd215698585a40d91b85.png" alt=""><br>
你能看到玩家10001的信息被删除同时后面的玩家排名都向前移了一位。
如果我们想要增加某个玩家的数据,命令格式为`ZADD 排行榜名称 分数 玩家标识`
时间复杂度为`O(log(N))`
这里我们把玩家10001的信息再增加回来使用`ZADD user_score 93.1504697596 10001`
<img src="https://static001.geekbang.org/resource/image/a3/37/a3586cd0a7819d01226e5daaf2234d37.png" alt=""><br>
然后我们再来看下排名在18031到18041的玩家是谁使用`ZREVRANGE user_score 18031 18041`
<img src="https://static001.geekbang.org/resource/image/18/ec/18de0c19c2dfb777632c8d0edd6e3bec.png" alt=""><br>
你能看到插入了玩家10001的数据之后排名又回来了。
## 总结
今天我们使用MySQL和Redis搭建了排行榜根据相同分数的处理方式我们可以把排行榜分成并列排行榜和严格排行榜。虽然MySQL和Redis都可以搭建排行榜但两者还是有区别的。MySQL擅长存储数据而对于数据的运算来说则效率不高比如统计排行榜的排名通常还是需要使用后端语言比如Python、PHP、Java等再进行统计。而Redis本身提供了丰富的排行榜统计功能不论是增加、删除玩家还是对某个玩家的分数进行调整Redis都可以对排行榜实时更新对于游戏的实时排名来说这还是很重要的。
在Redis中还集成了Lua脚本语言通过Lua我们可以更加灵活地扩展Redis的功能同时在Redis中使用Lua语言还可以对Lua脚本进行复用减少网络开销编写代码也更具有模块化。此外Redis在调用Lua脚本的时候会将它作为一个整体也就是说中间如果有其他的Redis命令是不会被插入进去的也保证了Lua脚本执行过程中不会被其他命令所干扰。
<img src="https://static001.geekbang.org/resource/image/9d/ed/9dddcb0e41e56fff740a1ddaec8e05ed.png" alt=""><br>
我们今天使用Redis对10万名玩家的数据进行了排行榜的统计相比于用RDBMS实现排行榜来说使用Redis进行统计都有哪些优势呢
我们使用了Lua脚本模拟了10万名玩家的数据其中玩家的分数score分成了两个部分整数部分为实际的得分小数部分为注册时间。例子中给出的严格排行榜是在分数相同的情况下按照注册时间的长短进行的排名注册时间长的排名靠前。如果我们将规则进行调整同样是在分数相同的情况下如果注册时间长的排名靠后又该如何编写代码呢
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。

View File

@@ -0,0 +1,207 @@
<audio id="audio" title="44丨DBMS篇总结和答疑用SQLite做词云" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/18/02/1860a27760780ddeceee9635fdc4d002.mp3"></audio>
在认识DBMS篇中我们讲解了Excel+SQL、WebSQL、SQLite以及Redis的使用这些DBMS有自己适用的领域我们可以根据需求选择适合的DBMS。我总结了一些大家常见的问题希望能对你有所帮助。
## 关于Excel+SQL
### 答疑1关于mysql-for-excel的安装
Excel是我们常用的办公软件使用SQL做数据分析的同学也可以使用Excel+SQL作为报表工具通过它们提取一些指定条件的数据形成数据透视表或者数据透视图。
但是有同学在安装mysql-for-excel-1.3.8.msi 时报错,这里感谢**同学莫弹弹**给出了解答。解决这个问题的办法是在安装时需要Visual Studio 2010 Tools for Office Runtime 才能运行。
它的下载链接在这里: [https://www.microsoft.com/zh-CN/download/confirmation.aspx?id=56961](https://www.microsoft.com/zh-CN/download/confirmation.aspx?id=56961)
## 关于WebSQL
我在讲解WebSQL操作本地存储时可以使用浏览器中的Clear Storage功能。有同学问到这里只能用户手动删除才可以吗
事实上除了在浏览器里手动删除以外我们完全可以通过程序来控制本地的SQLite。
使用executeSql函数即可在executeSql函数后面有两个function分别代表成功之后的调用以及执行失败的调用。比如想要删除本地SQLite的heros数据表可以写成下面这样
```
tx.executeSql(&quot;DROP TABLE heros&quot;,[],
function(tx, result) {alert('Drop 成功');},
function(tx, error) {alert('Drop 失败' + error.message);});
```
第二个问题是Session是什么概念呢HTTP请求不是无状态的吗
我在文章中讲到过SessionStorage这里的Session指的就是一个会话周期的数据当我们关闭浏览器窗口的时候SessionStorage存储的数据就会被清空。相比之下localStorage存储的时间没有限制一年之后数据依然可以存在。
HTTP本身是一个无状态的连接协议想要保持客户端与服务器之间的交互可以使用两种交互存储方式即Cookie和Session。
Cookie是通过客户端保存的数据也就是可以保存服务器发送给客户端的信息存储在浏览器中。一般来说在服务器上也存在一个Session这个是通过服务器来存储的状态信息这时会将浏览器与服务器之间的一系列交互称为一个Session。这种情况下Session会存储在服务器端。
不过我们讲解的sessionStorage是本地存储的解决方式它存放在浏览器里借用了session会话的概念它指的是在本地存储过程中的一种临时存储方案数据只有在同一个session会话中的页面才能访问而且当session结束后数据也会释放掉。
## 关于SQLite
第一个问题关于SQLite查找微信本地的聊天记录有同学说可以导出聊天记录做个词云。
这是个不错的idea我们既然有了SQLite完全可以动手做个数据分析做个词云展示。
我在《数据分析45讲》里讲到过词云的制作方法这里使用Python+SQLite查询将微信的聊天记录做个词云具体代码如下
```
import sqlite3
from wordcloud import WordCloud
import matplotlib.pyplot as plt
import jieba
import os
import re
# 去掉停用词
def remove_stop_words(f):
stop_words = ['你好', '已添加', '现在', '可以', '开始', '聊天', '当前', '群聊', '人数', '过多', '显示', '群成员', '昵称', '信息页', '关闭', '参与人', '还有', '嗯']
for stop_word in stop_words:
f = f.replace(stop_word, '')
return f
# 生成词云
def create_word_cloud(f):
print('根据微信聊天记录,生成词云!')
# 设置本地的simhei字体文件位置
FONT_PATH = os.environ.get(&quot;FONT_PATH&quot;, os.path.join(os.path.dirname(__file__), &quot;simhei.ttf&quot;))
f = remove_stop_words(f)
cut_text = &quot; &quot;.join(jieba.cut(f,cut_all=False, HMM=True))
wc = WordCloud(
font_path=FONT_PATH,
max_words=100,
width=2000,
height=1200,
)
wordcloud = wc.generate(cut_text)
# 写词云图片
wordcloud.to_file(&quot;wordcloud.jpg&quot;)
# 显示词云文件
plt.imshow(wordcloud)
plt.axis(&quot;off&quot;)
plt.show()
def get_content_from_weixin():
# 创建数据库连接
conn = sqlite3.connect(&quot;weixin.db&quot;)
# 获取游标
cur = conn.cursor()
# 创建数据表
# 查询当前数据库中的所有数据表
sql = &quot;SELECT name FROM sqlite_master WHERE type = 'table' AND name LIKE 'Chat\_%' escape '\\\'&quot;
cur.execute(sql)
tables = cur.fetchall()
content = ''
for table in tables:
sql = &quot;SELECT Message FROM &quot; + table[0]
print(sql)
cur.execute(sql)
temp_result = cur.fetchall()
for temp in temp_result:
content = content + str(temp)
# 提交事务
conn.commit()
# 关闭游标
cur.close()
# 关闭数据库连接
conn.close()
return content
content = get_content_from_weixin()
# 去掉HTML标签里的内容
pattern = re.compile(r'&lt;[^&gt;]+&gt;',re.S)
content = pattern.sub('', content)
# 将聊天记录生成词云
create_word_cloud(content)
```
运行结果:
<img src="https://static001.geekbang.org/resource/image/c0/6b/c01ef48d13e80b5742248b9cf58cfb6b.png" alt=""><br>
你在[Github](https://github.com/cystanford/SQLite)上也可以找到相应的代码,这个结果图是我运行自己的微信聊天记录得出的。
我来讲解下代码中相关模块的作用。
首先是`create_word_cloud`函数通过聊天内容f展示出词云。这里会用到WordCloud类通过它配置本地的simhei字体因为需要显示中文设置显示的最大词数`max_words=100`图片的尺寸width和height。
第二个是`remove_stop_words`函数,用来设置停用词,也就是不需要统计的单词,这里我设置了一些,不过从结果中,你能看到我们需要更多的停用词,要不会统计出一些没有意义的词汇,比如“撤回”“一条”等。
第三个是`get_content_from_weixin`函数。这里我们通过访问SQLite来访问微信聊天记录首先需要查询数据表都有哪些在微信的本地存储里每个数据表对应着一个聊天对象然后我们对这些数据表中的message字段进行提取。
最后因为统计出来的聊天记录会包括大量的HTML标签这里我们还需要采用正则表达式匹配的方式将content中的HTML标签去掉然后调用`create_word_cloud`函数生成词云,结果就是文稿中的图片所示啦。
第二个问题是Navicat如何导入`weixin.db`呢?
事实上使用Navicat导入`weixin.db`非常简单。首先我们需要创建SQLite连接然后从本地选择数据库文件这里选中`weixin.db`
然后就导入到Navicat中了你在左侧可以看到weixin的连接然后打开main数据库就可以看到聊天记录的数据表了。
我制作了演示视频,可以看下。
<video preload="none" controls=""><source src="https://media001.geekbang.org/customerTrans/fe4a99b62946f2c31c2095c167b26f9c/5366112e-16d490a2068-0000-0000-01d-dbacd.mp4" type="video/mp4"><source src="https://media001.geekbang.org/0b5bd0228e9149cdb6965ef48f35b681/145dd2dd317949798ff8d62a9cafa6a6-a5a4cad5b88948f5722740a7b6a03596-sd.m3u8" type="application/x-mpegURL"><source src="https://media001.geekbang.org/0b5bd0228e9149cdb6965ef48f35b681/145dd2dd317949798ff8d62a9cafa6a6-560f782e2a1ba0c7731de809a5248c73-hd.m3u8" type="application/x-mpegURL"></video>
## 关于Redis
第一个问题MongoDB、Redis之间有什么区别实际使用时应该怎么选择呢
Redis是Key-Value数据库数据存放在内存中查询和写入都是在内存中进行操作。当然Redis也支持持久化但持久化只是Redis的功能之一并不是Redis的强项。通常你可以把Redis称之为缓存它支持的数据类型丰富包括字符串、哈希、列表、集合、有序集合同时还支持基数统计、地理空间以及索引半径查询、数据流等。
MongoDB面向文档数据库功能强大是非关系型数据库中最像RDBMS的处理增删改查也可以增加条件。
在存储方式上Redis将数据放在内存中通过RDB或者AOF方式进行持久化。而MongoDB实际上是将数据存放在磁盘上的只是通过mmap调用将数据映射到内存中你可以将mmap理解为加速的方式。mmap调用可以使得对普通文件的操作像是在内存中进行读写一样这是因为它将文件映射到调用进程的地址空间中实现了文件所在的磁盘物理地址与进程空间的虚拟地址一一映射的关系这样就可以直接在内存中进行操作然后写完之后同步一下就可以存放到文件中效率非常高。
不过在使用选择的时候,我们还是将 MongoDB 归为数据库而将Redis归为缓存。
总的来说Redis就像一架飞机查询以及写入性能极佳但是存储的数据规模有限。MongoDB就像高铁在处理货物数据的功能上强于Redis同时能承载的数据量远高于Redis但是查询及写入的效率不及Redis。
第三个问题是我们能否用Redis中的DECR实现多用户抢票问题
当然是可以的在专栏文章中我使用了WATCH+MULTI的乐观锁方式主要是讲解这种乐观锁的实现方式。我们也可以使用Redis中的DECR命令对相应的KEY值进行减1操作是原子性的然后我们判断下DECR之后的数值即可当减1之后大于等于0证明抢票成功否则小于0则说明抢票失败。
这里我给出了相应的代码,你也可以在[Github](https://github.com/cystanford/Redis)上下载。
```
# 抢票模拟使用DECR原子操作
import redis
import threading
# 创建连接池
pool = redis.ConnectionPool(host = '127.0.0.1', port=6379, db=0)
# 初始化 redis
r = redis.StrictRedis(connection_pool = pool)
# 设置KEY
KEY=&quot;ticket_count&quot;
# 模拟第i个用户进行抢购
def sell(i):
# 使用decr对KEY减1
temp = r.decr(KEY)
if temp &gt;= 0:
print('用户 {} 抢票成功,当前票数 {}'.format(i, temp))
else:
print('用户 {} 抢票失败,票卖完了'.format(i))
if __name__ == &quot;__main__&quot;:
# 初始化5张票
r.set(KEY, 5)
# 设置8个人抢票
for i in range(8):
t = threading.Thread(target=sell, args=(i,))
t.start()
```
最后有些同学感觉用Redis最终还是需要结合程序以及MySQL来处理因为排行榜展示在前端还是需要用户名的光给个用户id不知道是谁除非Redis有序集合的member包含了用户id和name。
这里排行榜中如果要显示用户名称需要放到有序集合中这样就不需要再通过MySQL查询一次。这种需要实时排名计算的通过Redis解决更适合。如果是排行榜生成之后用户想看某一个用户具体的信息比如地区、战绩、使用英雄情况等可以通过MySQL来进行查询。而对于热点数据使用Redis进行缓存可以解决高并发情况下的数据库读压力。
所以你能看到Redis通常可以作为MySQL的缓存它存储的数据量有限适合存储热点数据可以解决读写效率要求很高的请求。而MySQL则作为数据库提供持久化功能并通过主从架构提高数据库服务的高可用性。
最后留两个思考题。
我在文稿中使用SQLite对于微信聊天记录进行查询使用wordcloud词云工具对聊天记录进行词云展示。同时我将聊天记录文本保存下来一共4.82M不包括HTML标签内容你可以使用SQLite读取微信聊天记录然后看下纯文本大小有多少
第二个问题是我们使用Redis作为MySQL的缓存假设MySQL存储了1000万的数据Redis只保存有限的数据比如10万数据量如何保证Redis存储的数据都是热点数据呢
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。