mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-16 14:13:46 +08:00
del
This commit is contained in:
157
极客时间专栏/geek/SQL必知必会/第三章:认识DBMS/38丨如何在Excel中使用SQL语言?.md
Normal file
157
极客时间专栏/geek/SQL必知必会/第三章:认识DBMS/38丨如何在Excel中使用SQL语言?.md
Normal 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>
|
||||
第二步。选择可用的表和列,在左侧面板中勾选我们想要导入的数据表及相应的列,点击 (>) 按钮导入到右侧的面板中,然后点击下一步。
|
||||
|
||||
<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>
|
||||
然后我们在右侧面板中选择“数据透视表字段”,以便对数据透视表中的字段进行管理,比如我们勾选num,role_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 > 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。
|
||||
|
||||
欢迎你在评论区写下你的体会与思考,也欢迎把这篇文章分享给你的朋友或者同事,一起来交流。
|
||||
238
极客时间专栏/geek/SQL必知必会/第三章:认识DBMS/39丨WebSQL:如何在H5中存储一个本地数据库?.md
Normal file
238
极客时间专栏/geek/SQL必知必会/第三章:认识DBMS/39丨WebSQL:如何在H5中存储一个本地数据库?.md
Normal 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');
|
||||
}
|
||||
|
||||
完整代码如下:
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>SQL必知必会</title>
|
||||
<script type="text/javascript">
|
||||
if (!window.openDatabase) {
|
||||
alert('浏览器不支持WebSQL');
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="status" name="status">WebSQL Test</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
```
|
||||
|
||||
如果浏览器不支持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, "夏侯惇", 7350, 1746, "坦克")');
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
这里执行的事务就是一个方法,包括两条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, "夏侯惇", 7350, 1746, "坦克")');
|
||||
|
||||
```
|
||||
|
||||
## 在浏览器端做一个王者荣耀英雄的查询页面
|
||||
|
||||
刚才我讲解了WebSQL的基本语法,现在我们就来用刚学到的东西做一个小练习:在浏览器端做一个王者荣耀英雄的创建和查询页面。
|
||||
|
||||
具体步骤如下:
|
||||
|
||||
1. 初始化数据:我们需要在HTML中设置一个id为datatable的table表格,然后在JavaScript中创建init()函数,获取id为datatable的元素。
|
||||
1. 创建showData方法:参数为查询出来的数据row,showData方法可以方便地展示查询出来的一行数据我们在数据表中的字段为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))下载):
|
||||
|
||||
```
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>SQL必知必会</title>
|
||||
<script type="text/javascript">
|
||||
// 初始化
|
||||
function init() {
|
||||
datatable = document.getElementById("datatable");
|
||||
}
|
||||
// 显示每个英雄的数据
|
||||
function showData(row){
|
||||
var tr = document.createElement("tr");
|
||||
var td1 = document.createElement("td");
|
||||
var td2 = document.createElement("td");
|
||||
var td3 = document.createElement("td");
|
||||
var td4 = document.createElement("td");
|
||||
var td5 = document.createElement("td");
|
||||
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, "夏侯惇", 7350, 1746, "坦克")');
|
||||
tx.executeSql('INSERT INTO heros (id, name, hp_max, mp_max, role_main) VALUES (10001, "钟无艳", 7000, 1760, "战士")');
|
||||
tx.executeSql('INSERT INTO heros (id, name, hp_max, mp_max, role_main) VALUES (10002, "张飞", 8341, 100, "坦克")');
|
||||
tx.executeSql('INSERT INTO heros (id, name, hp_max, mp_max, role_main) VALUES (10003, "牛魔", 8476, 1926, "坦克")');
|
||||
tx.executeSql('INSERT INTO heros (id, name, hp_max, mp_max, role_main) VALUES (10004, "吕布", 7344, 0, "战士")');
|
||||
msg = '<p>heros数据表创建成功,一共插入5条数据。</p>';
|
||||
document.querySelector('#status').innerHTML = msg;
|
||||
});
|
||||
// 查询数据
|
||||
db.transaction(function (tx) {
|
||||
tx.executeSql('SELECT * FROM heros', [], function (tx, data) {
|
||||
var len = data.rows.length;
|
||||
msg = "<p>查询记录条数: " + len + "</p>";
|
||||
document.querySelector('#status').innerHTML += msg;
|
||||
// 将查询的英雄数据放到 datatable中
|
||||
for (i = 0; i < len; i++){
|
||||
showData(data.rows.item(i));
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="status" name="status">状态信息</div>
|
||||
<table border="1" id="datatable"></table>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
```
|
||||
|
||||
演示结果如下:
|
||||
|
||||
<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>
|
||||
欢迎你在评论区写下你的答案,我会和你一起交流,也欢迎把这篇文章分享给你的朋友或者同事,与他们一起交流一下。
|
||||
211
极客时间专栏/geek/SQL必知必会/第三章:认识DBMS/40丨SQLite:为什么微信用SQLite存储聊天记录?.md
Normal file
211
极客时间专栏/geek/SQL必知必会/第三章:认识DBMS/40丨SQLite:为什么微信用SQLite存储聊天记录?.md
Normal 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("wucai.db")
|
||||
|
||||
```
|
||||
|
||||
这里我们连接的是wucai.db这个文件,如果没有这个文件存储,上面的调用会自动在相应的工程路径里进行创建,然后我们可以使用conn操作连接,通过会话连接conn来创建游标:
|
||||
|
||||
```
|
||||
cur = conn.cursor()
|
||||
|
||||
```
|
||||
|
||||
通过这一步,我们得到了游标cur,然后可以使用execute()方法来执行各种DML,比如插入,删除,更新等,当然我们也可以进行SQL查询,用的同样是execute()方法。
|
||||
|
||||
比如我们想要创建heros数据表,以及相应的字段id、name、hp_max、mp_max、role_main,可以写成下面这样:
|
||||
|
||||
```
|
||||
cur.execute("CREATE TABLE IF NOT EXISTS heros (id int primary key, name text, hp_max real, mp_max real, role_main text)")
|
||||
|
||||
```
|
||||
|
||||
在创建之后,我们可以使用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("SELECT id, name, hp_max, mp_max, role_main FROM heros")
|
||||
|
||||
```
|
||||
|
||||
这时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("wucai.db")
|
||||
# 获取游标
|
||||
cur = conn.cursor()
|
||||
# 创建数据表
|
||||
cur.execute("CREATE TABLE IF NOT EXISTS heros (id int primary key, name text, hp_max real, mp_max real, role_main text)")
|
||||
# 插入英雄数据
|
||||
cur.executemany('insert into heros values(?, ?, ?, ?, ?)',
|
||||
((10000, '夏侯惇', 7350, 1746, '坦克'),
|
||||
(10001, '钟无艳', 7000, 1760, '战士'),
|
||||
(10002, '张飞', 8341, 100, '坦克'),
|
||||
(10003, '牛魔', 8476, 1926, '坦克'),
|
||||
(10004, '吕布', 7344, 0, '战士')))
|
||||
cur.execute("SELECT id, name, hp_max, mp_max, role_main FROM heros")
|
||||
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进行操作,并输出结果。
|
||||
|
||||
欢迎你在评论区写下你的答案,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。
|
||||
|
||||
|
||||
224
极客时间专栏/geek/SQL必知必会/第三章:认识DBMS/41丨初识Redis:Redis为什么会这么快?.md
Normal file
224
极客时间专栏/geek/SQL必知必会/第三章:认识DBMS/41丨初识Redis:Redis为什么会这么快?.md
Normal file
@@ -0,0 +1,224 @@
|
||||
<audio id="audio" title="41丨初识Redis:Redis为什么会这么快?" 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的规则是“只提供你想要的”,数据模型灵活,查询效率高,成本低。但同时,相比RDBMS,NoSQL数据库没有统一的架构和标准语言,每种数据库之间差异较大,各有所长。
|
||||
|
||||
今天我们要讲解的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的数据类型
|
||||
|
||||
相比Memcached,Redis有一个非常大的优势,就是支持多种数据类型。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为zhangfei,age为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版本之后,增加了基数统计(HyperLogLog),3.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("users"+str(i), data)
|
||||
# 统计写时间
|
||||
delta_time = time.time()-time1
|
||||
print(delta_time)
|
||||
# 统计当前时间
|
||||
time1 = time.time()
|
||||
# 1万次读
|
||||
for i in range(10000):
|
||||
result = r.hmget("users"+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即使采用单线程模式效率也很高呢?
|
||||
|
||||
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。
|
||||
|
||||
|
||||
168
极客时间专栏/geek/SQL必知必会/第三章:认识DBMS/42丨如何使用Redis来实现多用户抢票问题.md
Normal file
168
极客时间专栏/geek/SQL必知必会/第三章:认识DBMS/42丨如何使用Redis来实现多用户抢票问题.md
Normal 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模式。
|
||||
|
||||
RDB(Redis DataBase)持久化可以把当前进程的数据生成快照保存到磁盘上,触发RDB持久化的方式分为手动触发和自动触发。因为持久化操作与命令操作不是同步进行的,所以无法保证事务的持久性。
|
||||
|
||||
AOF(Append 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="ticket_count"
|
||||
# 模拟第i个用户进行抢票
|
||||
def sell(i):
|
||||
# 初始化 pipe
|
||||
pipe = r.pipeline()
|
||||
while True:
|
||||
try:
|
||||
# 监视票数
|
||||
pipe.watch(KEY)
|
||||
# 查看票数
|
||||
c = int(pipe.get(KEY))
|
||||
if c > 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__ == "__main__":
|
||||
# 初始化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>
|
||||
欢迎你在评论区写下你的思考,我会和你一起交流,也欢迎你把这篇文章分享给你的朋友或者同事,一起交流一下。
|
||||
305
极客时间专栏/geek/SQL必知必会/第三章:认识DBMS/43丨如何使用Redis搭建玩家排行榜?.md
Normal file
305
极客时间专栏/geek/SQL必知必会/第三章:认识DBMS/43丨如何使用Redis搭建玩家排行榜?.md
Normal 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 > 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 "math.randomseed(ARGV[1]); local temp = math.random(1,112); redis.call('SET', KEYS[1], temp); return 'ok';" 1 score 30
|
||||
|
||||
```
|
||||
|
||||
这条语句代表的意思是,我们传入KEY的个数为1,参数是score,arg参数为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分成了两个部分,整数部分为实际的得分,小数部分为注册时间。例子中给出的严格排行榜是在分数相同的情况下,按照注册时间的长短进行的排名,注册时间长的排名靠前。如果我们将规则进行调整,同样是在分数相同的情况下,如果注册时间长的排名靠后,又该如何编写代码呢?
|
||||
|
||||
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。
|
||||
207
极客时间专栏/geek/SQL必知必会/第三章:认识DBMS/44丨DBMS篇总结和答疑:用SQLite做词云.md
Normal file
207
极客时间专栏/geek/SQL必知必会/第三章:认识DBMS/44丨DBMS篇总结和答疑:用SQLite做词云.md
Normal 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("DROP TABLE heros",[],
|
||||
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("FONT_PATH", os.path.join(os.path.dirname(__file__), "simhei.ttf"))
|
||||
f = remove_stop_words(f)
|
||||
cut_text = " ".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("wordcloud.jpg")
|
||||
# 显示词云文件
|
||||
plt.imshow(wordcloud)
|
||||
plt.axis("off")
|
||||
plt.show()
|
||||
|
||||
def get_content_from_weixin():
|
||||
# 创建数据库连接
|
||||
conn = sqlite3.connect("weixin.db")
|
||||
# 获取游标
|
||||
cur = conn.cursor()
|
||||
# 创建数据表
|
||||
# 查询当前数据库中的所有数据表
|
||||
sql = "SELECT name FROM sqlite_master WHERE type = 'table' AND name LIKE 'Chat\_%' escape '\\\'"
|
||||
cur.execute(sql)
|
||||
tables = cur.fetchall()
|
||||
content = ''
|
||||
for table in tables:
|
||||
sql = "SELECT Message FROM " + 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'<[^>]+>',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="ticket_count"
|
||||
# 模拟第i个用户进行抢购
|
||||
def sell(i):
|
||||
# 使用decr对KEY减1
|
||||
temp = r.decr(KEY)
|
||||
if temp >= 0:
|
||||
print('用户 {} 抢票成功,当前票数 {}'.format(i, temp))
|
||||
else:
|
||||
print('用户 {} 抢票失败,票卖完了'.format(i))
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 初始化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存储的数据都是热点数据呢?
|
||||
|
||||
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user