diff --git a/.editorconfig b/.editorconfig
index 0b8d35f..c7ca29b 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -1,32 +1,25 @@
-# EditorConfig helps developers define and maintain consistent
-# coding styles between different editors and IDEs
-# http://editorconfig.org
-# 所有文件换行使用 Unix like 风格(LF),bat 文件使用 win 风格(CRLF)
-# 缩进 java 4 个空格,其他所有文件 2 个空格
+# EditorConfig 用于在 IDE 中检查代码的基本 Code Style
+# @see: https://editorconfig.org/
+
+# 配置说明:
+# 所有文件换行使用 Unix 风格(LF),*.bat 文件使用 Windows 风格(CRLF)
+# java / sh 文件缩进 4 个空格,其他所有文件缩进 2 个空格
root = true
[*]
-# Unix-style newlines with a newline ending every file
end_of_line = lf
-
-# Change these settings to your own preference
indent_size = 2
-indent_style = space
+indent_style = tab
max_line_length = 120
-
-# We recommend you to keep these unchanged
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
-[*.bat]
+[*.{bat, cmd}]
end_of_line = crlf
-[*.java]
-indent_size = 4
-
-[*.sql]
+[*.{java, groovy, kt, sh}]
indent_size = 4
[*.md]
diff --git a/codes/javadb/javadb-h2/pom.xml b/codes/javadb/javadb-h2/pom.xml
index d229e4c..da00858 100644
--- a/codes/javadb/javadb-h2/pom.xml
+++ b/codes/javadb/javadb-h2/pom.xml
@@ -1,56 +1,57 @@
- 4.0.0
- io.github.dunwu
- javadb-h2
- 1.0.0
- jar
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
+ xmlns="http://maven.apache.org/POM/4.0.0">
+ 4.0.0
+ io.github.dunwu
+ javadb-h2
+ 1.0.0
+ jar
-
- UTF-8
- 1.8
- ${java.version}
- ${java.version}
+
+ UTF-8
+ 1.8
+ ${java.version}
+ ${java.version}
- 4.12
-
+ 4.12
+
-
-
-
- com.h2database
- h2
-
-
+
+
+
+ com.h2database
+ h2
+
+
-
-
- junit
- junit
-
-
-
+
+
+ junit
+ junit
+
+
+
-
-
-
-
- com.h2database
- h2
- 1.4.197
- test
-
-
+
+
+
+
+ com.h2database
+ h2
+ 1.4.197
+ test
+
+
-
-
- junit
- junit
- ${junit.version}
- test
-
-
-
-
+
+
+ junit
+ junit
+ ${junit.version}
+ test
+
+
+
+
diff --git a/codes/javadb/javadb-h2/src/test/java/io/github/dunwu/javadb/H2JdbcTest01.java b/codes/javadb/javadb-h2/src/test/java/io/github/dunwu/javadb/H2JdbcTest01.java
index 8944a18..885e449 100644
--- a/codes/javadb/javadb-h2/src/test/java/io/github/dunwu/javadb/H2JdbcTest01.java
+++ b/codes/javadb/javadb-h2/src/test/java/io/github/dunwu/javadb/H2JdbcTest01.java
@@ -43,8 +43,7 @@ public class H2JdbcTest01 {
CONNECTION = DriverManager.getConnection(JDBC_URL3, USER, PASSWORD);
// 创建sql声明
STATEMENT = CONNECTION.createStatement();
- }
- catch (ClassNotFoundException | SQLException e) {
+ } catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
}
}
@@ -56,8 +55,7 @@ public class H2JdbcTest01 {
STATEMENT.close();
// 关闭连接
CONNECTION.close();
- }
- catch (SQLException e) {
+ } catch (SQLException e) {
e.printStackTrace();
}
}
@@ -85,8 +83,7 @@ public class H2JdbcTest01 {
while (rs.next()) {
System.out.println(rs.getString("id") + "," + rs.getString("name") + "," + rs.getString("sex"));
}
- }
- catch (SQLException e) {
+ } catch (SQLException e) {
Assert.assertTrue(e.getMessage(), true);
}
}
diff --git a/codes/javadb/javadb-hbase/pom.xml b/codes/javadb/javadb-hbase/pom.xml
index c74f393..df4764d 100644
--- a/codes/javadb/javadb-hbase/pom.xml
+++ b/codes/javadb/javadb-hbase/pom.xml
@@ -1,62 +1,63 @@
- 4.0.0
- io.github.dunwu
- javadb-hbase
- 1.0.0
- jar
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
+ xmlns="http://maven.apache.org/POM/4.0.0">
+ 4.0.0
+ io.github.dunwu
+ javadb-hbase
+ 1.0.0
+ jar
-
- UTF-8
- 1.8
- ${java.version}
- ${java.version}
+
+ UTF-8
+ 1.8
+ ${java.version}
+ ${java.version}
- 1.3.1
- 4.12
- 0.4.1
-
+ 1.3.1
+ 4.12
+ 0.4.1
+
-
-
- org.apache.hbase
- hbase-client
-
-
- io.github.dunwu
- dunwu-common
-
+
+
+ org.apache.hbase
+ hbase-client
+
+
+ io.github.dunwu
+ dunwu-common
+
-
-
- junit
- junit
-
-
-
+
+
+ junit
+ junit
+
+
+
-
-
-
- org.apache.hbase
- hbase-client
- ${hbase.version}
-
-
- io.github.dunwu
- dunwu-common
- ${dunwu.version}
-
+
+
+
+ org.apache.hbase
+ hbase-client
+ ${hbase.version}
+
+
+ io.github.dunwu
+ dunwu-common
+ ${dunwu.version}
+
-
-
- junit
- junit
- ${junit.version}
- test
-
-
-
-
+
+
+ junit
+ junit
+ ${junit.version}
+ test
+
+
+
+
diff --git a/codes/javadb/javadb-hbase/src/main/java/io/github/dunwu/javadb/HBaseConstant.java b/codes/javadb/javadb-hbase/src/main/java/io/github/dunwu/javadb/HBaseConstant.java
index 9978afc..996fba4 100644
--- a/codes/javadb/javadb-hbase/src/main/java/io/github/dunwu/javadb/HBaseConstant.java
+++ b/codes/javadb/javadb-hbase/src/main/java/io/github/dunwu/javadb/HBaseConstant.java
@@ -3,14 +3,14 @@ package io.github.dunwu.javadb;
public enum HBaseConstant {
HBASE_ZOOKEEPER_QUORUM("hbase.zookeeper.quorum"), HBASE_ENABLE("hbase.enable"), HBASE_MASTER(
- "hbase.master"), HBASE_ZOOKEEPER_PROPERTY_CLIENTPORT(
- "hbase.zookeeper.property.clientPort"), HBASE_HCONNECTION_THREADS_MAX(
- "hbase.hconnection.threads.max"), HBASE_HCONNECTION_THREADS_CORE(
- "hbase.hconnection.threads.core"), ZOOKEEPER_ZNODE_PARENT(
- "zookeeper.znode.parent"), HBASE_COLUMN_FAMILY(
- "hbase.column.family"), HBASE_EXECUTOR_NUM(
- "hbase.executor.num"), HBASE_IPC_POOL_SIZE(
- "hbase.client.ipc.pool.size");
+ "hbase.master"), HBASE_ZOOKEEPER_PROPERTY_CLIENTPORT(
+ "hbase.zookeeper.property.clientPort"), HBASE_HCONNECTION_THREADS_MAX(
+ "hbase.hconnection.threads.max"), HBASE_HCONNECTION_THREADS_CORE(
+ "hbase.hconnection.threads.core"), ZOOKEEPER_ZNODE_PARENT(
+ "zookeeper.znode.parent"), HBASE_COLUMN_FAMILY(
+ "hbase.column.family"), HBASE_EXECUTOR_NUM(
+ "hbase.executor.num"), HBASE_IPC_POOL_SIZE(
+ "hbase.client.ipc.pool.size");
private String key;
@@ -21,5 +21,4 @@ public enum HBaseConstant {
public String key() {
return key;
}
-
}
diff --git a/codes/javadb/javadb-hbase/src/main/java/io/github/dunwu/javadb/HbaseCellEntity.java b/codes/javadb/javadb-hbase/src/main/java/io/github/dunwu/javadb/HbaseCellEntity.java
index 25a1a31..d01f680 100644
--- a/codes/javadb/javadb-hbase/src/main/java/io/github/dunwu/javadb/HbaseCellEntity.java
+++ b/codes/javadb/javadb-hbase/src/main/java/io/github/dunwu/javadb/HbaseCellEntity.java
@@ -4,7 +4,7 @@ package io.github.dunwu.javadb;
* HBase Cell 实体
*
* @author Zhang Peng
- * @date 2019-03-04
+ * @since 2019-03-04
*/
public class HbaseCellEntity {
@@ -79,7 +79,7 @@ public class HbaseCellEntity {
@Override
public String toString() {
return "HbaseCellEntity{" + "table='" + table + '\'' + ", row='" + row + '\'' + ", colFamily='" + colFamily
- + '\'' + ", col='" + col + '\'' + ", val='" + val + '\'' + '}';
+ + '\'' + ", col='" + col + '\'' + ", val='" + val + '\'' + '}';
}
}
diff --git a/codes/javadb/javadb-hbase/src/main/java/io/github/dunwu/javadb/HbaseHelper.java b/codes/javadb/javadb-hbase/src/main/java/io/github/dunwu/javadb/HbaseHelper.java
index 1968aef..19074a0 100644
--- a/codes/javadb/javadb-hbase/src/main/java/io/github/dunwu/javadb/HbaseHelper.java
+++ b/codes/javadb/javadb-hbase/src/main/java/io/github/dunwu/javadb/HbaseHelper.java
@@ -16,13 +16,16 @@ import java.util.Properties;
* HBase 服务实现类
*
* @author Zhang Peng
- * @date 2019-03-01
+ * @since 2019-03-01
*/
public class HbaseHelper {
private static final String FIRST_CONFIG = "classpath://config//hbase.properties";
+
private static final String SECOND_CONFIG = "classpath://application.properties";
+
private HbaseProperties hbaseProperties;
+
private Connection connection;
public HbaseHelper() throws Exception {
@@ -38,22 +41,17 @@ public class HbaseHelper {
String quorum = PropertiesUtil.getString(properties, HBaseConstant.HBASE_ZOOKEEPER_QUORUM.key(), "");
String hbaseMaster = PropertiesUtil.getString(properties, HBaseConstant.HBASE_MASTER.key(), "");
String clientPort = PropertiesUtil.getString(properties,
- HBaseConstant.HBASE_ZOOKEEPER_PROPERTY_CLIENTPORT.key(), "");
+ HBaseConstant.HBASE_ZOOKEEPER_PROPERTY_CLIENTPORT.key(), "");
String znodeParent = PropertiesUtil.getString(properties, HBaseConstant.ZOOKEEPER_ZNODE_PARENT.key(), "");
String maxThreads = PropertiesUtil.getString(properties, HBaseConstant.HBASE_HCONNECTION_THREADS_MAX.key(), "");
String coreThreads = PropertiesUtil.getString(properties, HBaseConstant.HBASE_HCONNECTION_THREADS_CORE.key(),
- "");
+ "");
String columnFamily = PropertiesUtil.getString(properties, HBaseConstant.HBASE_COLUMN_FAMILY.key(), "");
String hbaseExecutorsNum = PropertiesUtil.getString(properties, HBaseConstant.HBASE_EXECUTOR_NUM.key(), "10");
String ipcPoolSize = PropertiesUtil.getString(properties, HBaseConstant.HBASE_IPC_POOL_SIZE.key(), "1");
hbaseProperties = new HbaseProperties(hbaseMaster, quorum, clientPort, znodeParent, maxThreads, coreThreads,
- columnFamily, hbaseExecutorsNum, ipcPoolSize);
- init(hbaseProperties);
- }
-
- public HbaseHelper(HbaseProperties hbaseProperties) throws Exception {
- this.hbaseProperties = hbaseProperties;
+ columnFamily, hbaseExecutorsNum, ipcPoolSize);
init(hbaseProperties);
}
@@ -61,16 +59,14 @@ public class HbaseHelper {
Properties properties = null;
try {
properties = PropertiesUtil.loadFromFile(FIRST_CONFIG);
- }
- catch (Exception e) {
+ } catch (Exception e) {
e.printStackTrace();
}
if (properties == null) {
try {
properties = PropertiesUtil.loadFromFile(SECOND_CONFIG);
- }
- catch (Exception e) {
+ } catch (Exception e) {
e.printStackTrace();
return null;
}
@@ -81,32 +77,35 @@ public class HbaseHelper {
private void init(HbaseProperties hbaseProperties) throws Exception {
try {
// @formatter:off
- Configuration configuration = HBaseConfiguration.create();
- configuration.set(HBaseConstant.HBASE_ZOOKEEPER_QUORUM.key(), hbaseProperties.getQuorum());
- configuration.set(HBaseConstant.HBASE_MASTER.key(), hbaseProperties.getHbaseMaster());
- configuration.set(HBaseConstant.HBASE_ZOOKEEPER_PROPERTY_CLIENTPORT.key(),
- hbaseProperties.getClientPort());
- configuration.set(HBaseConstant.HBASE_HCONNECTION_THREADS_MAX.key(),
- hbaseProperties.getMaxThreads());
- configuration.set(HBaseConstant.HBASE_HCONNECTION_THREADS_CORE.key(),
- hbaseProperties.getCoreThreads());
- configuration.set(HBaseConstant.ZOOKEEPER_ZNODE_PARENT.key(), hbaseProperties.getZnodeParent());
- configuration.set(HBaseConstant.HBASE_COLUMN_FAMILY.key(), hbaseProperties.getColumnFamily());
- configuration.set(HBaseConstant.HBASE_IPC_POOL_SIZE.key(), hbaseProperties.getIpcPoolSize());
- // @formatter:on
+ Configuration configuration = HBaseConfiguration.create();
+ configuration.set(HBaseConstant.HBASE_ZOOKEEPER_QUORUM.key(), hbaseProperties.getQuorum());
+ configuration.set(HBaseConstant.HBASE_MASTER.key(), hbaseProperties.getHbaseMaster());
+ configuration.set(HBaseConstant.HBASE_ZOOKEEPER_PROPERTY_CLIENTPORT.key(),
+ hbaseProperties.getClientPort());
+ configuration.set(HBaseConstant.HBASE_HCONNECTION_THREADS_MAX.key(),
+ hbaseProperties.getMaxThreads());
+ configuration.set(HBaseConstant.HBASE_HCONNECTION_THREADS_CORE.key(),
+ hbaseProperties.getCoreThreads());
+ configuration.set(HBaseConstant.ZOOKEEPER_ZNODE_PARENT.key(), hbaseProperties.getZnodeParent());
+ configuration.set(HBaseConstant.HBASE_COLUMN_FAMILY.key(), hbaseProperties.getColumnFamily());
+ configuration.set(HBaseConstant.HBASE_IPC_POOL_SIZE.key(), hbaseProperties.getIpcPoolSize());
+ // @formatter:on
connection = ConnectionFactory.createConnection(configuration);
- }
- catch (Exception e) {
+ } catch (Exception e) {
throw new Exception("hbase链接未创建", e);
}
}
+ public HbaseHelper(HbaseProperties hbaseProperties) throws Exception {
+ this.hbaseProperties = hbaseProperties;
+ init(hbaseProperties);
+ }
+
public void destory() {
if (connection != null) {
try {
connection.close();
- }
- catch (IOException e) {
+ } catch (IOException e) {
e.printStackTrace();
}
}
@@ -125,12 +124,10 @@ public class HbaseHelper {
try {
if (StringUtils.isEmpty(tableName)) {
hTableDescriptors = connection.getAdmin().listTables();
- }
- else {
+ } else {
hTableDescriptors = connection.getAdmin().listTables(tableName);
}
- }
- catch (IOException e) {
+ } catch (IOException e) {
throw new Exception("执行失败", e);
}
return hTableDescriptors;
@@ -145,7 +142,7 @@ public class HbaseHelper {
*
*/
public void createTable(String tableName) throws Exception {
- createTable(tableName, new String[] { hbaseProperties.getColumnFamily() });
+ createTable(tableName, new String[] {hbaseProperties.getColumnFamily()});
}
/**
@@ -173,8 +170,7 @@ public class HbaseHelper {
}
connection.getAdmin().createTable(tableDescriptor);
- }
- catch (IOException e) {
+ } catch (IOException e) {
e.printStackTrace();
}
}
@@ -187,6 +183,7 @@ public class HbaseHelper {
*
disable 'tablename'
* drop 't1'
*
+ *
* @param name
*/
public void dropTable(String name) throws Exception {
@@ -203,8 +200,7 @@ public class HbaseHelper {
admin.disableTable(tableName);
admin.deleteTable(tableName);
}
- }
- catch (IOException e) {
+ } catch (IOException e) {
e.printStackTrace();
}
}
@@ -216,7 +212,7 @@ public class HbaseHelper {
Put put = new Put(Bytes.toBytes(hBaseTableDTO.getRow()));
put.addColumn(Bytes.toBytes(hBaseTableDTO.getColFamily()), Bytes.toBytes(hBaseTableDTO.getCol()),
- Bytes.toBytes(hBaseTableDTO.getVal()));
+ Bytes.toBytes(hBaseTableDTO.getVal()));
return put;
}
@@ -230,8 +226,7 @@ public class HbaseHelper {
table = connection.getTable(TableName.valueOf(tableName));
Delete delete = new Delete(Bytes.toBytes(rowKey));
table.delete(delete);
- }
- catch (IOException e) {
+ } catch (IOException e) {
e.printStackTrace();
throw new Exception("delete失败");
}
@@ -279,14 +274,12 @@ public class HbaseHelper {
if (StringUtils.isNotEmpty(colFamily)) {
if (StringUtils.isNotEmpty(qualifier)) {
get.addColumn(Bytes.toBytes(colFamily), Bytes.toBytes(qualifier));
- }
- else {
+ } else {
get.addFamily(Bytes.toBytes(colFamily));
}
}
result = table.get(get);
- }
- catch (IOException e) {
+ } catch (IOException e) {
throw new Exception("查询时发生异常");
}
return result;
@@ -301,7 +294,7 @@ public class HbaseHelper {
}
public Result[] scan(String tableName, String colFamily, String qualifier, String startRow, String stopRow)
- throws Exception {
+ throws Exception {
if (connection == null) {
throw new Exception("hbase链接未创建");
}
@@ -333,11 +326,9 @@ public class HbaseHelper {
list.add(result);
result = resultScanner.next();
}
- }
- catch (IOException e) {
+ } catch (IOException e) {
e.printStackTrace();
- }
- finally {
+ } finally {
if (resultScanner != null) {
resultScanner.close();
}
@@ -366,8 +357,7 @@ public class HbaseHelper {
list.add(result);
result = resultScanner.next();
}
- }
- catch (IOException e) {
+ } catch (IOException e) {
e.printStackTrace();
}
return list;
diff --git a/codes/javadb/javadb-hbase/src/main/java/io/github/dunwu/javadb/HbaseProperties.java b/codes/javadb/javadb-hbase/src/main/java/io/github/dunwu/javadb/HbaseProperties.java
index 8bad534..e2a1ad5 100644
--- a/codes/javadb/javadb-hbase/src/main/java/io/github/dunwu/javadb/HbaseProperties.java
+++ b/codes/javadb/javadb-hbase/src/main/java/io/github/dunwu/javadb/HbaseProperties.java
@@ -33,7 +33,7 @@ public class HbaseProperties implements Serializable {
}
public HbaseProperties(String hbaseMaster, String quorum, String clientPort, String znodeParent, String maxThreads,
- String coreThreads, String columnFamily, String hbaseExecutorsNum, String ipcPoolSize) {
+ String coreThreads, String columnFamily, String hbaseExecutorsNum, String ipcPoolSize) {
this.hbaseMaster = hbaseMaster;
this.quorum = quorum;
this.clientPort = clientPort;
@@ -120,9 +120,9 @@ public class HbaseProperties implements Serializable {
@Override
public String toString() {
return "HbaseProperties{" + "quorum='" + quorum + '\'' + ", clientPort='" + clientPort + '\''
- + ", znodeParent='" + znodeParent + '\'' + ", maxThreads='" + maxThreads + '\'' + ", coreThreads='"
- + coreThreads + '\'' + ", columnFamily='" + columnFamily + '\'' + ", hbaseExecutorsNum='"
- + hbaseExecutorsNum + '\'' + '}';
+ + ", znodeParent='" + znodeParent + '\'' + ", maxThreads='" + maxThreads + '\'' + ", coreThreads='"
+ + coreThreads + '\'' + ", columnFamily='" + columnFamily + '\'' + ", hbaseExecutorsNum='"
+ + hbaseExecutorsNum + '\'' + '}';
}
}
diff --git a/codes/javadb/javadb-hbase/src/test/java/io/github/dunwu/javadb/HbaseHelperTest.java b/codes/javadb/javadb-hbase/src/test/java/io/github/dunwu/javadb/HbaseHelperTest.java
index 70326bd..e52447f 100644
--- a/codes/javadb/javadb-hbase/src/test/java/io/github/dunwu/javadb/HbaseHelperTest.java
+++ b/codes/javadb/javadb-hbase/src/test/java/io/github/dunwu/javadb/HbaseHelperTest.java
@@ -8,7 +8,7 @@ import org.junit.Test;
/**
* @author Zhang Peng
- * @date 2019-03-29
+ * @since 2019-03-29
*/
public class HbaseHelperTest {
@@ -18,8 +18,7 @@ public class HbaseHelperTest {
public static void BeforeClass() {
try {
hbaseHelper = new HbaseHelper();
- }
- catch (Exception e) {
+ } catch (Exception e) {
e.printStackTrace();
}
}
@@ -39,13 +38,13 @@ public class HbaseHelperTest {
@Test
public void createTable() throws Exception {
- hbaseHelper.createTable("table1", new String[] { "columnFamliy1", "columnFamliy2" });
+ hbaseHelper.createTable("table1", new String[] {"columnFamliy1", "columnFamliy2"});
HTableDescriptor[] table1s = hbaseHelper.listTables("table1");
if (table1s == null || table1s.length <= 0) {
Assert.fail();
}
- hbaseHelper.createTable("table2", new String[] { "columnFamliy1", "columnFamliy2" });
+ hbaseHelper.createTable("table2", new String[] {"columnFamliy1", "columnFamliy2"});
table1s = hbaseHelper.listTables("table2");
if (table1s == null || table1s.length <= 0) {
Assert.fail();
diff --git a/codes/javadb/javadb-mysql/pom.xml b/codes/javadb/javadb-mysql/pom.xml
index da0a2ef..8fb2a7d 100644
--- a/codes/javadb/javadb-mysql/pom.xml
+++ b/codes/javadb/javadb-mysql/pom.xml
@@ -1,113 +1,114 @@
- 4.0.0
- io.github.dunwu
- javadb-mysql
- 1.0.0
- jar
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
+ xmlns="http://maven.apache.org/POM/4.0.0">
+ 4.0.0
+ io.github.dunwu
+ javadb-mysql
+ 1.0.0
+ jar
-
- UTF-8
- 1.8
- ${java.version}
- ${java.version}
+
+ UTF-8
+ 1.8
+ ${java.version}
+ ${java.version}
- 4.3.13.RELEASE
- 1.2.3
- 4.12
-
+ 4.3.13.RELEASE
+ 1.2.3
+ 4.12
+
-
-
-
- mysql
- mysql-connector-java
- 5.1.45
-
-
- org.apache.commons
- commons-pool2
- 2.5.0
-
-
+
+
+
+ mysql
+ mysql-connector-java
+ 5.1.45
+
+
+ org.apache.commons
+ commons-pool2
+ 2.5.0
+
+
-
-
- ch.qos.logback
- logback-classic
-
-
+
+
+ ch.qos.logback
+ logback-classic
+
+
-
-
- org.springframework
- spring-context-support
-
-
- org.springframework
- spring-test
- test
-
-
+
+
+ org.springframework
+ spring-context-support
+
+
+ org.springframework
+ spring-test
+ test
+
+
-
-
- junit
- junit
-
-
-
+
+
+ junit
+ junit
+
+
+
-
-
-
- org.springframework
- spring-framework-bom
- ${spring.version}
- pom
- import
-
+
+
+
+ org.springframework
+ spring-framework-bom
+ ${spring.version}
+ pom
+ import
+
-
-
- redis.clients
- jedis
- ${jedis.version}
-
-
+
+
+ redis.clients
+ jedis
+ ${jedis.version}
+
+
-
-
- ch.qos.logback
- logback-parent
- ${logback.version}
- pom
- import
-
-
+
+
+ ch.qos.logback
+ logback-parent
+ ${logback.version}
+ pom
+ import
+
+
-
-
- junit
- junit
- ${junit.version}
- test
-
-
-
-
+
+
+ junit
+ junit
+ ${junit.version}
+ test
+
+
+
+
-
- ${project.artifactId}
-
-
- true
- src/main/resources
-
- logback.xml
-
-
-
-
+
+ ${project.artifactId}
+
+
+ true
+ src/main/resources
+
+ logback.xml
+
+
+
+
diff --git a/codes/javadb/javadb-mysql/src/test/java/io/github/dunwu/javadb/MysqlDemoTest.java b/codes/javadb/javadb-mysql/src/test/java/io/github/dunwu/javadb/MysqlDemoTest.java
index 416af87..ebdd009 100644
--- a/codes/javadb/javadb-mysql/src/test/java/io/github/dunwu/javadb/MysqlDemoTest.java
+++ b/codes/javadb/javadb-mysql/src/test/java/io/github/dunwu/javadb/MysqlDemoTest.java
@@ -6,8 +6,6 @@ import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.sql.*;
-
/**
* Mysql 测试例
*
@@ -17,11 +15,17 @@ import java.sql.*;
public class MysqlDemoTest {
private static final String DB_HOST = "localhost";
+
private static final String DB_PORT = "3306";
+
private static final String DB_SCHEMA = "sakila";
+
private static final String DB_USER = "root";
+
private static final String DB_PASSWORD = "root";
+
private static Logger logger = LoggerFactory.getLogger(MysqlDemoTest.class);
+
private static Statement statement;
private static Connection connection;
@@ -35,8 +39,7 @@ public class MysqlDemoTest {
// DriverManager.getConnection("jdbc:mysql://localhost:3306/sakila?" +
// "user=root&password=root");
statement = connection.createStatement();
- }
- catch (SQLException e) {
+ } catch (SQLException e) {
e.printStackTrace();
}
}
@@ -47,8 +50,7 @@ public class MysqlDemoTest {
if (connection != null) {
connection.close();
}
- }
- catch (SQLException e) {
+ } catch (SQLException e) {
e.printStackTrace();
}
}
@@ -67,10 +69,9 @@ public class MysqlDemoTest {
Date lastUpdate = rs.getDate("last_update");
// 输出数据
logger.debug("actor_id: {}, first_name: {}, last_name: {}, last_update: {}", id, firstName, lastName,
- lastUpdate.toLocalDate());
+ lastUpdate.toLocalDate());
}
- }
- catch (SQLException e) {
+ } catch (SQLException e) {
e.printStackTrace();
}
}
diff --git a/codes/javadb/javadb-mysql/src/test/resources/logback.xml b/codes/javadb/javadb-mysql/src/test/resources/logback.xml
index 143ac56..d782516 100644
--- a/codes/javadb/javadb-mysql/src/test/resources/logback.xml
+++ b/codes/javadb/javadb-mysql/src/test/resources/logback.xml
@@ -3,43 +3,43 @@
-
+
-
-
-
- %d{HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n
-
-
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n
+
+
-
-
-
-
- ${user.dir}/logs/${FILE_NAME}-all.%d{yyyy-MM-dd}.log
- 30
-
+
+
+
+
+ ${user.dir}/logs/${FILE_NAME}-all.%d{yyyy-MM-dd}.log
+ 30
+
-
-
- 30MB
-
+
+
+ 30MB
+
-
- %d{HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n
-
-
-
+
+ %d{HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n
+
+
+
-
-
-
-
-
+
+
+
+
+
-
-
-
-
+
+
+
+
diff --git a/codes/javadb/javadb-redis/pom.xml b/codes/javadb/javadb-redis/pom.xml
index 8c24db6..37c0578 100644
--- a/codes/javadb/javadb-redis/pom.xml
+++ b/codes/javadb/javadb-redis/pom.xml
@@ -1,126 +1,127 @@
- 4.0.0
- io.github.dunwu
- javadb-redis
- 1.0.0
- jar
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
+ xmlns="http://maven.apache.org/POM/4.0.0">
+ 4.0.0
+ io.github.dunwu
+ javadb-redis
+ 1.0.0
+ jar
-
- UTF-8
- 1.8
- ${java.version}
- ${java.version}
+
+ UTF-8
+ 1.8
+ ${java.version}
+ ${java.version}
- 4.3.13.RELEASE
- 1.2.3
- 2.9.0
- 3.7.2
- 4.12
-
+ 4.3.13.RELEASE
+ 1.2.3
+ 2.9.0
+ 3.7.2
+ 4.12
+
-
-
-
- redis.clients
- jedis
-
-
- org.redisson
- redisson
-
-
+
+
+
+ redis.clients
+ jedis
+
+
+ org.redisson
+ redisson
+
+
-
-
- ch.qos.logback
- logback-classic
-
-
+
+
+ ch.qos.logback
+ logback-classic
+
+
-
-
- org.springframework
- spring-beans
-
-
- org.springframework
- spring-context-support
-
-
- org.springframework
- spring-core
-
-
- org.springframework
- spring-test
- test
-
-
+
+
+ org.springframework
+ spring-beans
+
+
+ org.springframework
+ spring-context-support
+
+
+ org.springframework
+ spring-core
+
+
+ org.springframework
+ spring-test
+ test
+
+
-
-
- junit
- junit
-
-
-
+
+
+ junit
+ junit
+
+
+
-
-
-
- org.springframework
- spring-framework-bom
- ${spring.version}
- pom
- import
-
+
+
+
+ org.springframework
+ spring-framework-bom
+ ${spring.version}
+ pom
+ import
+
-
-
- redis.clients
- jedis
- ${jedis.version}
-
-
- org.redisson
- redisson
- ${redisson.version}
-
-
+
+
+ redis.clients
+ jedis
+ ${jedis.version}
+
+
+ org.redisson
+ redisson
+ ${redisson.version}
+
+
-
-
- ch.qos.logback
- logback-parent
- ${logback.version}
- pom
- import
-
-
+
+
+ ch.qos.logback
+ logback-parent
+ ${logback.version}
+ pom
+ import
+
+
-
-
- junit
- junit
- ${junit.version}
- test
-
-
-
-
+
+
+ junit
+ junit
+ ${junit.version}
+ test
+
+
+
+
-
- ${project.artifactId}
-
-
- true
- src/main/resources
-
- logback.xml
-
-
-
-
+
+ ${project.artifactId}
+
+
+ true
+ src/main/resources
+
+ logback.xml
+
+
+
+
diff --git a/codes/javadb/javadb-redis/src/test/java/io/github/dunwu/javadb/JedisDemoTest.java b/codes/javadb/javadb-redis/src/test/java/io/github/dunwu/javadb/JedisDemoTest.java
index 24ed749..31ffc98 100644
--- a/codes/javadb/javadb-redis/src/test/java/io/github/dunwu/javadb/JedisDemoTest.java
+++ b/codes/javadb/javadb-redis/src/test/java/io/github/dunwu/javadb/JedisDemoTest.java
@@ -38,8 +38,7 @@ public class JedisDemoTest {
try {
jedis.ping();
logger.debug("jedis 连接成功。");
- }
- catch (JedisConnectionException e) {
+ } catch (JedisConnectionException e) {
e.printStackTrace();
}
}
diff --git a/codes/javadb/javadb-redis/src/test/java/io/github/dunwu/javadb/JedisPoolDemoTest.java b/codes/javadb/javadb-redis/src/test/java/io/github/dunwu/javadb/JedisPoolDemoTest.java
index 5c5cf8c..316359d 100644
--- a/codes/javadb/javadb-redis/src/test/java/io/github/dunwu/javadb/JedisPoolDemoTest.java
+++ b/codes/javadb/javadb-redis/src/test/java/io/github/dunwu/javadb/JedisPoolDemoTest.java
@@ -20,7 +20,7 @@ import java.util.Set;
*/
@ActiveProfiles("test")
@RunWith(SpringJUnit4ClassRunner.class)
-@ContextConfiguration(locations = { "classpath:/applicationContext.xml" })
+@ContextConfiguration(locations = {"classpath:/applicationContext.xml"})
public class JedisPoolDemoTest {
private static Logger logger = LoggerFactory.getLogger(JedisPoolDemoTest.class);
diff --git a/codes/javadb/javadb-redis/src/test/java/io/github/dunwu/javadb/RedissonStandaloneTest.java b/codes/javadb/javadb-redis/src/test/java/io/github/dunwu/javadb/RedissonStandaloneTest.java
index 43150fc..014f030 100644
--- a/codes/javadb/javadb-redis/src/test/java/io/github/dunwu/javadb/RedissonStandaloneTest.java
+++ b/codes/javadb/javadb-redis/src/test/java/io/github/dunwu/javadb/RedissonStandaloneTest.java
@@ -7,7 +7,7 @@ import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* @author Zhang Peng
- * @date 2018/6/19
+ * @since 2018/6/19
*/
public class RedissonStandaloneTest {
diff --git a/codes/javadb/javadb-redis/src/test/resources/applicationContext.xml b/codes/javadb/javadb-redis/src/test/resources/applicationContext.xml
index 3e29ca9..14c081d 100644
--- a/codes/javadb/javadb-redis/src/test/resources/applicationContext.xml
+++ b/codes/javadb/javadb-redis/src/test/resources/applicationContext.xml
@@ -1,12 +1,12 @@
+ xmlns="http://www.springframework.org/schema/beans"
+ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"
+ default-lazy-init="false">
- Spring基础配置
+ Spring基础配置
-
-
+
+
diff --git a/codes/javadb/javadb-redis/src/test/resources/config.xml b/codes/javadb/javadb-redis/src/test/resources/config.xml
index a117988..0a0008c 100644
--- a/codes/javadb/javadb-redis/src/test/resources/config.xml
+++ b/codes/javadb/javadb-redis/src/test/resources/config.xml
@@ -1,19 +1,20 @@
-
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
diff --git a/codes/javadb/javadb-redis/src/test/resources/logback.xml b/codes/javadb/javadb-redis/src/test/resources/logback.xml
index a343b51..54865e8 100644
--- a/codes/javadb/javadb-redis/src/test/resources/logback.xml
+++ b/codes/javadb/javadb-redis/src/test/resources/logback.xml
@@ -3,43 +3,43 @@
-
+
-
-
-
- %d{HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n
-
-
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n
+
+
-
-
-
-
- ${user.dir}/logs/${FILE_NAME}.%d{yyyy-MM-dd}.log
- 30
-
+
+
+
+
+ ${user.dir}/logs/${FILE_NAME}.%d{yyyy-MM-dd}.log
+ 30
+
-
-
- 30MB
-
+
+
+ 30MB
+
-
- %d{HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n
-
-
-
+
+ %d{HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n
+
+
+
-
-
-
-
-
+
+
+
+
+
-
-
-
-
+
+
+
+
diff --git a/codes/javadb/javadb-redis/src/test/resources/properties/application-dev.properties b/codes/javadb/javadb-redis/src/test/resources/properties/application-dev.properties
index c65d0af..bcafc92 100644
--- a/codes/javadb/javadb-redis/src/test/resources/properties/application-dev.properties
+++ b/codes/javadb/javadb-redis/src/test/resources/properties/application-dev.properties
@@ -4,5 +4,4 @@ redis.port = 6379
redis.timeout = 3000
redis.password = zp
redis.database = 0
-
log.path = ./
diff --git a/codes/javadb/javadb-redis/src/test/resources/properties/application-test.properties b/codes/javadb/javadb-redis/src/test/resources/properties/application-test.properties
index 0b06816..cc341fc 100644
--- a/codes/javadb/javadb-redis/src/test/resources/properties/application-test.properties
+++ b/codes/javadb/javadb-redis/src/test/resources/properties/application-test.properties
@@ -4,5 +4,4 @@ redis.port = 6379
redis.timeout = 3000
redis.password = zp
redis.database = 0
-
log.path = /home/zp/log
diff --git a/codes/javadb/javadb-redis/src/test/resources/redis.xml b/codes/javadb/javadb-redis/src/test/resources/redis.xml
index 884256b..468b1cd 100644
--- a/codes/javadb/javadb-redis/src/test/resources/redis.xml
+++ b/codes/javadb/javadb-redis/src/test/resources/redis.xml
@@ -1,21 +1,21 @@
+ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
- redis configuration
+ redis configuration
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/codes/javadb/javadb-redis/src/test/resources/redisson-standalone.xml b/codes/javadb/javadb-redis/src/test/resources/redisson-standalone.xml
index 84f04e7..04a334f 100644
--- a/codes/javadb/javadb-redis/src/test/resources/redisson-standalone.xml
+++ b/codes/javadb/javadb-redis/src/test/resources/redisson-standalone.xml
@@ -1,21 +1,21 @@
-
-
-
-
+
+
+
+
diff --git a/codes/javadb/javadb-sqlite/pom.xml b/codes/javadb/javadb-sqlite/pom.xml
index b1efd26..d40a71c 100644
--- a/codes/javadb/javadb-sqlite/pom.xml
+++ b/codes/javadb/javadb-sqlite/pom.xml
@@ -1,60 +1,60 @@
- 4.0.0
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ 4.0.0
-
- org.springframework.boot
- spring-boot-starter-parent
- 2.1.9.RELEASE
-
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.1.9.RELEASE
+
- io.github.dunwu
- javadb-sqlite
- 1.0.0
- jar
+ io.github.dunwu
+ javadb-sqlite
+ 1.0.0
+ jar
-
- UTF-8
- UTF-8
- 1.8
-
+
+ UTF-8
+ UTF-8
+ 1.8
+
-
-
- org.springframework.boot
- spring-boot-starter
-
-
- org.springframework.boot
- spring-boot-starter-test
- test
-
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
-
- org.xerial
- sqlite-jdbc
- 3.25.2
-
-
+
+ org.xerial
+ sqlite-jdbc
+ 3.25.2
+
+
-
- ${project.artifactId}
-
-
- org.springframework.boot
- spring-boot-maven-plugin
-
- io.github.dunwu.db.SqliteApplication
-
-
-
-
- repackage
-
-
-
-
-
-
+
+ ${project.artifactId}
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+ io.github.dunwu.db.SqliteApplication
+
+
+
+
+ repackage
+
+
+
+
+
+
diff --git a/codes/javadb/javadb-sqlite/src/main/java/io/github/dunwu/db/SqliteApplication.java b/codes/javadb/javadb-sqlite/src/main/java/io/github/dunwu/db/SqliteApplication.java
index 48085cb..e0069c1 100644
--- a/codes/javadb/javadb-sqlite/src/main/java/io/github/dunwu/db/SqliteApplication.java
+++ b/codes/javadb/javadb-sqlite/src/main/java/io/github/dunwu/db/SqliteApplication.java
@@ -5,7 +5,7 @@ import org.springframework.boot.builder.SpringApplicationBuilder;
/**
* @author Zhang Peng
- * @date 2019-03-05
+ * @since 2019-03-05
*/
public class SqliteApplication implements CommandLineRunner {
diff --git a/codes/javadb/javadb-sqlite/src/main/java/io/github/dunwu/db/SqliteDemo.java b/codes/javadb/javadb-sqlite/src/main/java/io/github/dunwu/db/SqliteDemo.java
index ec7bda6..9030b59 100644
--- a/codes/javadb/javadb-sqlite/src/main/java/io/github/dunwu/db/SqliteDemo.java
+++ b/codes/javadb/javadb-sqlite/src/main/java/io/github/dunwu/db/SqliteDemo.java
@@ -7,28 +7,19 @@ import java.sql.Statement;
/**
* @author Zhang Peng
- * @date 2019-03-05
+ * @since 2019-03-05
*/
public class SqliteDemo {
- public static void createTable() {
- try {
- Class.forName("org.sqlite.JDBC");
- Connection connection = DriverManager.getConnection("jdbc:sqlite:test.db");
-
- Statement statement = connection.createStatement();
- String sql = new StringBuilder().append("CREATE TABLE COMPANY ").append("(ID INT PRIMARY KEY NOT NULL,")
- .append(" NAME TEXT NOT NULL, ").append(" AGE INT NOT NULL, ")
- .append(" ADDRESS CHAR(50), ").append(" SALARY REAL)").toString();
- statement.executeUpdate(sql);
- statement.close();
- connection.close();
- }
- catch (Exception e) {
- System.err.println(e.getClass().getName() + ": " + e.getMessage());
- System.exit(0);
- }
- System.out.println("Create table successfully.");
+ public static void main(String[] args) {
+ SqliteDemo.dropTable();
+ SqliteDemo.createTable();
+ SqliteDemo.insert();
+ SqliteDemo.select();
+ SqliteDemo.delete();
+ SqliteDemo.select();
+ SqliteDemo.update();
+ SqliteDemo.select();
}
public static void dropTable() {
@@ -41,14 +32,32 @@ public class SqliteDemo {
statement.executeUpdate(sql);
statement.close();
connection.close();
- }
- catch (Exception e) {
+ } catch (Exception e) {
System.err.println(e.getClass().getName() + ": " + e.getMessage());
System.exit(0);
}
System.out.println("Drop table successfully.");
}
+ public static void createTable() {
+ try {
+ Class.forName("org.sqlite.JDBC");
+ Connection connection = DriverManager.getConnection("jdbc:sqlite:test.db");
+
+ Statement statement = connection.createStatement();
+ String sql = new StringBuilder().append("CREATE TABLE COMPANY ").append("(ID INT PRIMARY KEY NOT NULL,")
+ .append(" NAME TEXT NOT NULL, ").append(" AGE INT NOT NULL, ")
+ .append(" ADDRESS CHAR(50), ").append(" SALARY REAL)").toString();
+ statement.executeUpdate(sql);
+ statement.close();
+ connection.close();
+ } catch (Exception e) {
+ System.err.println(e.getClass().getName() + ": " + e.getMessage());
+ System.exit(0);
+ }
+ System.out.println("Create table successfully.");
+ }
+
public static void insert() {
try {
Class.forName("org.sqlite.JDBC");
@@ -57,7 +66,7 @@ public class SqliteDemo {
Statement statement = connection.createStatement();
String sql = "INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) "
- + "VALUES (1, 'Paul', 32, 'California', 20000.00 );";
+ + "VALUES (1, 'Paul', 32, 'California', 20000.00 );";
statement.executeUpdate(sql);
sql = "INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) " + "VALUES (2, 'Allen', 25, 'Texas', 15000.00 );";
@@ -67,20 +76,46 @@ public class SqliteDemo {
statement.executeUpdate(sql);
sql = "INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) "
- + "VALUES (4, 'Mark', 25, 'Rich-Mond ', 65000.00 );";
+ + "VALUES (4, 'Mark', 25, 'Rich-Mond ', 65000.00 );";
statement.executeUpdate(sql);
statement.close();
connection.commit();
connection.close();
- }
- catch (Exception e) {
+ } catch (Exception e) {
System.err.println(e.getClass().getName() + ": " + e.getMessage());
System.exit(0);
}
System.out.println("Insert table successfully.");
}
+ public static void select() {
+ try {
+ Class.forName("org.sqlite.JDBC");
+ Connection connection = DriverManager.getConnection("jdbc:sqlite:test.db");
+ connection.setAutoCommit(false);
+
+ Statement statement = connection.createStatement();
+ ResultSet resultSet = statement.executeQuery("SELECT * FROM COMPANY;");
+ while (resultSet.next()) {
+ int id = resultSet.getInt("id");
+ String name = resultSet.getString("name");
+ int age = resultSet.getInt("age");
+ String address = resultSet.getString("address");
+ float salary = resultSet.getFloat("salary");
+ String format = String.format("ID = %s, NAME = %s, AGE = %d, ADDRESS = %s, SALARY = %f", id, name, age,
+ address, salary);
+ System.out.println(format);
+ }
+ resultSet.close();
+ statement.close();
+ connection.close();
+ } catch (Exception e) {
+ System.err.println(e.getClass().getName() + ": " + e.getMessage());
+ System.exit(0);
+ }
+ }
+
public static void delete() {
try {
Class.forName("org.sqlite.JDBC");
@@ -100,8 +135,7 @@ public class SqliteDemo {
statement.close();
connection.close();
- }
- catch (Exception e) {
+ } catch (Exception e) {
System.err.println(e.getClass().getName() + ": " + e.getMessage());
System.exit(0);
}
@@ -121,51 +155,11 @@ public class SqliteDemo {
statement.close();
connection.close();
- }
- catch (Exception e) {
+ } catch (Exception e) {
System.err.println(e.getClass().getName() + ": " + e.getMessage());
System.exit(0);
}
System.out.println("Update table successfully.");
}
- public static void select() {
- try {
- Class.forName("org.sqlite.JDBC");
- Connection connection = DriverManager.getConnection("jdbc:sqlite:test.db");
- connection.setAutoCommit(false);
-
- Statement statement = connection.createStatement();
- ResultSet resultSet = statement.executeQuery("SELECT * FROM COMPANY;");
- while (resultSet.next()) {
- int id = resultSet.getInt("id");
- String name = resultSet.getString("name");
- int age = resultSet.getInt("age");
- String address = resultSet.getString("address");
- float salary = resultSet.getFloat("salary");
- String format = String.format("ID = %s, NAME = %s, AGE = %d, ADDRESS = %s, SALARY = %f", id, name, age,
- address, salary);
- System.out.println(format);
- }
- resultSet.close();
- statement.close();
- connection.close();
- }
- catch (Exception e) {
- System.err.println(e.getClass().getName() + ": " + e.getMessage());
- System.exit(0);
- }
- }
-
- public static void main(String[] args) {
- SqliteDemo.dropTable();
- SqliteDemo.createTable();
- SqliteDemo.insert();
- SqliteDemo.select();
- SqliteDemo.delete();
- SqliteDemo.select();
- SqliteDemo.update();
- SqliteDemo.select();
- }
-
}
diff --git a/codes/javadb/javadb-sqlite/src/main/resources/logback.xml b/codes/javadb/javadb-sqlite/src/main/resources/logback.xml
index d1ae018..b100aee 100644
--- a/codes/javadb/javadb-sqlite/src/main/resources/logback.xml
+++ b/codes/javadb/javadb-sqlite/src/main/resources/logback.xml
@@ -1,5 +1,5 @@
-
-
+
+
diff --git a/codes/javadb/pom.xml b/codes/javadb/pom.xml
index d139397..c29d814 100644
--- a/codes/javadb/pom.xml
+++ b/codes/javadb/pom.xml
@@ -1,18 +1,18 @@
- 4.0.0
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ 4.0.0
- io.github.dunwu
- javadb
- 1.0.0
- pom
+ io.github.dunwu
+ javadb
+ 1.0.0
+ pom
-
- javadb-h2
- javadb-hbase
- javadb-mysql
- javadb-redis
- javadb-sqlite
-
+
+ javadb-h2
+ javadb-hbase
+ javadb-mysql
+ javadb-redis
+ javadb-sqlite
+
diff --git a/codes/middleware/flyway/pom.xml b/codes/middleware/flyway/pom.xml
index b1ab1a4..39129fd 100644
--- a/codes/middleware/flyway/pom.xml
+++ b/codes/middleware/flyway/pom.xml
@@ -1,53 +1,54 @@
- 4.0.0
- io.github.dunwu
- db-middleware-flyway
- 1.0.0
- jar
- DB :: Middleware :: Flyway
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
+ xmlns="http://maven.apache.org/POM/4.0.0">
+ 4.0.0
+ io.github.dunwu
+ db-middleware-flyway
+ 1.0.0
+ jar
+ DB :: Middleware :: Flyway
-
- UTF-8
- 1.8
- ${java.version}
- ${java.version}
-
+
+ UTF-8
+ 1.8
+ ${java.version}
+ ${java.version}
+
-
-
-
- org.flywaydb
- flyway-core
- 5.1.4
-
-
- com.h2database
- h2
- 1.4.197
-
-
-
+
+
+
+ org.flywaydb
+ flyway-core
+ 5.1.4
+
+
+ com.h2database
+ h2
+ 1.4.197
+
+
+
-
-
-
- org.flywaydb
- flyway-maven-plugin
- 5.1.4
-
- jdbc:h2:file:./target/io/github/dunwu/db/middleware
- sa
-
-
-
- com.h2database
- h2
- 1.4.197
-
-
-
-
-
+
+
+
+ org.flywaydb
+ flyway-maven-plugin
+ 5.1.4
+
+ jdbc:h2:file:./target/io/github/dunwu/db/middleware
+ sa
+
+
+
+ com.h2database
+ h2
+ 1.4.197
+
+
+
+
+
diff --git a/codes/redis/redis-in-action-py/ch01_listing_source.py b/codes/redis/redis-in-action-py/ch01_listing_source.py
index dc7a7fd..26be458 100644
--- a/codes/redis/redis-in-action-py/ch01_listing_source.py
+++ b/codes/redis/redis-in-action-py/ch01_listing_source.py
@@ -124,23 +124,23 @@ VOTE_SCORE = 432
def article_vote(conn, user, article):
- # 计算文章的投票截止时间。
- cutoff = time.time() - ONE_WEEK_IN_SECONDS
+ # 计算文章的投票截止时间。
+ cutoff = time.time() - ONE_WEEK_IN_SECONDS
- # 检查是否还可以对文章进行投票
- # (虽然使用散列也可以获取文章的发布时间,
- # 但有序集合返回的文章发布时间为浮点数,
- # 可以不进行转换直接使用)。
- if conn.zscore('time:', article) < cutoff:
- return
+ # 检查是否还可以对文章进行投票
+ # (虽然使用散列也可以获取文章的发布时间,
+ # 但有序集合返回的文章发布时间为浮点数,
+ # 可以不进行转换直接使用)。
+ if conn.zscore('time:', article) < cutoff:
+ return
- # 从article:id标识符(identifier)里面取出文章的ID。
- article_id = article.partition(':')[-1]
+ # 从article:id标识符(identifier)里面取出文章的ID。
+ article_id = article.partition(':')[-1]
- # 如果用户是第一次为这篇文章投票,那么增加这篇文章的投票数量和评分。
- if conn.sadd('voted:' + article_id, user):
- conn.zincrby('score:', article, VOTE_SCORE)
- conn.hincrby(article, 'votes', 1)
+ # 如果用户是第一次为这篇文章投票,那么增加这篇文章的投票数量和评分。
+ if conn.sadd('voted:' + article_id, user):
+ conn.zincrby('score:', article, VOTE_SCORE)
+ conn.hincrby(article, 'votes', 1)
#
@@ -149,31 +149,31 @@ def article_vote(conn, user, article):
# 代码清单 1-7
#
def post_article(conn, user, title, link):
- # 生成一个新的文章ID。
- article_id = str(conn.incr('article:'))
+ # 生成一个新的文章ID。
+ article_id = str(conn.incr('article:'))
- voted = 'voted:' + article_id
- # 将发布文章的用户添加到文章的已投票用户名单里面,
- # 然后将这个名单的过期时间设置为一周(第3章将对过期时间作更详细的介绍)。
- conn.sadd(voted, user)
- conn.expire(voted, ONE_WEEK_IN_SECONDS)
+ voted = 'voted:' + article_id
+ # 将发布文章的用户添加到文章的已投票用户名单里面,
+ # 然后将这个名单的过期时间设置为一周(第3章将对过期时间作更详细的介绍)。
+ conn.sadd(voted, user)
+ conn.expire(voted, ONE_WEEK_IN_SECONDS)
- now = time.time()
- article = 'article:' + article_id
- # 将文章信息存储到一个散列里面。
- conn.hmset(article, {
- 'title': title,
- 'link': link,
- 'poster': user,
- 'time': now,
- 'votes': 1,
- })
+ now = time.time()
+ article = 'article:' + article_id
+ # 将文章信息存储到一个散列里面。
+ conn.hmset(article, {
+ 'title': title,
+ 'link': link,
+ 'poster': user,
+ 'time': now,
+ 'votes': 1,
+ })
- # 将文章添加到根据发布时间排序的有序集合和根据评分排序的有序集合里面。
- conn.zadd('score:', article, now + VOTE_SCORE)
- conn.zadd('time:', article, now)
+ # 将文章添加到根据发布时间排序的有序集合和根据评分排序的有序集合里面。
+ conn.zadd('score:', article, now + VOTE_SCORE)
+ conn.zadd('time:', article, now)
- return article_id
+ return article_id
#
@@ -185,20 +185,20 @@ ARTICLES_PER_PAGE = 25
def get_articles(conn, page, order='score:'):
- # 设置获取文章的起始索引和结束索引。
- start = (page - 1) * ARTICLES_PER_PAGE
- end = start + ARTICLES_PER_PAGE - 1
+ # 设置获取文章的起始索引和结束索引。
+ start = (page - 1) * ARTICLES_PER_PAGE
+ end = start + ARTICLES_PER_PAGE - 1
- # 获取多个文章ID。
- ids = conn.zrevrange(order, start, end)
- articles = []
- # 根据文章ID获取文章的详细信息。
- for id in ids:
- article_data = conn.hgetall(id)
- article_data['id'] = id
- articles.append(article_data)
+ # 获取多个文章ID。
+ ids = conn.zrevrange(order, start, end)
+ articles = []
+ # 根据文章ID获取文章的详细信息。
+ for id in ids:
+ article_data = conn.hgetall(id)
+ article_data['id'] = id
+ articles.append(article_data)
- return articles
+ return articles
#
@@ -207,14 +207,14 @@ def get_articles(conn, page, order='score:'):
# 代码清单 1-9
#
def add_remove_groups(conn, article_id, to_add=[], to_remove=[]):
- # 构建存储文章信息的键名。
- article = 'article:' + article_id
- for group in to_add:
- # 将文章添加到它所属的群组里面。
- conn.sadd('group:' + group, article)
- for group in to_remove:
- # 从群组里面移除文章。
- conn.srem('group:' + group, article)
+ # 构建存储文章信息的键名。
+ article = 'article:' + article_id
+ for group in to_add:
+ # 将文章添加到它所属的群组里面。
+ conn.sadd('group:' + group, article)
+ for group in to_remove:
+ # 从群组里面移除文章。
+ conn.srem('group:' + group, article)
#
@@ -223,19 +223,19 @@ def add_remove_groups(conn, article_id, to_add=[], to_remove=[]):
# 代码清单 1-10
#
def get_group_articles(conn, group, page, order='score:'):
- # 为每个群组的每种排列顺序都创建一个键。
- key = order + group
- # 检查是否有已缓存的排序结果,如果没有的话就现在进行排序。
- if not conn.exists(key):
- # 根据评分或者发布时间,对群组文章进行排序。
- conn.zinterstore(key,
- ['group:' + group, order],
- aggregate='max',
- )
- # 让Redis在60秒钟之后自动删除这个有序集合。
- conn.expire(key, 60)
- # 调用之前定义的get_articles()函数来进行分页并获取文章数据。
- return get_articles(conn, page, key)
+ # 为每个群组的每种排列顺序都创建一个键。
+ key = order + group
+ # 检查是否有已缓存的排序结果,如果没有的话就现在进行排序。
+ if not conn.exists(key):
+ # 根据评分或者发布时间,对群组文章进行排序。
+ conn.zinterstore(key,
+ ['group:' + group, order],
+ aggregate='max',
+ )
+ # 让Redis在60秒钟之后自动删除这个有序集合。
+ conn.expire(key, 60)
+ # 调用之前定义的get_articles()函数来进行分页并获取文章数据。
+ return get_articles(conn, page, key)
#
@@ -243,58 +243,58 @@ def get_group_articles(conn, group, page, order='score:'):
# --------------- 以下是用于测试代码的辅助函数 --------------------------------
class TestCh01(unittest.TestCase):
- def setUp(self):
- import redis
- self.conn = redis.Redis(db=15)
+ def setUp(self):
+ import redis
+ self.conn = redis.Redis(db=15)
- def tearDown(self):
- del self.conn
- print
- print
+ def tearDown(self):
+ del self.conn
+ print
+ print
- def test_article_functionality(self):
- conn = self.conn
- import pprint
+ def test_article_functionality(self):
+ conn = self.conn
+ import pprint
- article_id = str(post_article(conn, 'username', 'A title', 'http://www.google.com'))
- print "We posted a new article with id:", article_id
- print
- self.assertTrue(article_id)
+ article_id = str(post_article(conn, 'username', 'A title', 'http://www.google.com'))
+ print "We posted a new article with id:", article_id
+ print
+ self.assertTrue(article_id)
- print "Its HASH looks like:"
- r = conn.hgetall('article:' + article_id)
- print r
- print
- self.assertTrue(r)
+ print "Its HASH looks like:"
+ r = conn.hgetall('article:' + article_id)
+ print r
+ print
+ self.assertTrue(r)
- article_vote(conn, 'other_user', 'article:' + article_id)
- print "We voted for the article, it now has votes:",
- v = int(conn.hget('article:' + article_id, 'votes'))
- print v
- print
- self.assertTrue(v > 1)
+ article_vote(conn, 'other_user', 'article:' + article_id)
+ print "We voted for the article, it now has votes:",
+ v = int(conn.hget('article:' + article_id, 'votes'))
+ print v
+ print
+ self.assertTrue(v > 1)
- print "The currently highest-scoring articles are:"
- articles = get_articles(conn, 1)
- pprint.pprint(articles)
- print
+ print "The currently highest-scoring articles are:"
+ articles = get_articles(conn, 1)
+ pprint.pprint(articles)
+ print
- self.assertTrue(len(articles) >= 1)
+ self.assertTrue(len(articles) >= 1)
- add_remove_groups(conn, article_id, ['new-group'])
- print "We added the article to a new group, other articles include:"
- articles = get_group_articles(conn, 'new-group', 1)
- pprint.pprint(articles)
- print
- self.assertTrue(len(articles) >= 1)
+ add_remove_groups(conn, article_id, ['new-group'])
+ print "We added the article to a new group, other articles include:"
+ articles = get_group_articles(conn, 'new-group', 1)
+ pprint.pprint(articles)
+ print
+ self.assertTrue(len(articles) >= 1)
- to_del = (
- conn.keys('time:*') + conn.keys('voted:*') + conn.keys('score:*') +
- conn.keys('article:*') + conn.keys('group:*')
- )
- if to_del:
- conn.delete(*to_del)
+ to_del = (
+ conn.keys('time:*') + conn.keys('voted:*') + conn.keys('score:*') +
+ conn.keys('article:*') + conn.keys('group:*')
+ )
+ if to_del:
+ conn.delete(*to_del)
if __name__ == '__main__':
- unittest.main()
+ unittest.main()
diff --git a/codes/redis/redis-in-action-py/ch02_listing_source.py b/codes/redis/redis-in-action-py/ch02_listing_source.py
index d757ad3..58c85ff 100644
--- a/codes/redis/redis-in-action-py/ch02_listing_source.py
+++ b/codes/redis/redis-in-action-py/ch02_listing_source.py
@@ -13,7 +13,7 @@ QUIT = False
# 代码清单 2-1
#
def check_token(conn, token):
- return conn.hget('login:', token) # 尝试获取并返回令牌对应的用户。
+ return conn.hget('login:', token) # 尝试获取并返回令牌对应的用户。
#
@@ -22,17 +22,17 @@ def check_token(conn, token):
# 代码清单 2-2
#
def update_token(conn, token, user, item=None):
- # 获取当前时间戳。
- timestamp = time.time()
- # 维持令牌与已登录用户之间的映射。
- conn.hset('login:', token, user)
- # 记录令牌最后一次出现的时间。
- conn.zadd('recent:', token, timestamp)
- if item:
- # 记录用户浏览过的商品。
- conn.zadd('viewed:' + token, item, timestamp)
- # 移除旧的记录,只保留用户最近浏览过的25个商品。
- conn.zremrangebyrank('viewed:' + token, 0, -26)
+ # 获取当前时间戳。
+ timestamp = time.time()
+ # 维持令牌与已登录用户之间的映射。
+ conn.hset('login:', token, user)
+ # 记录令牌最后一次出现的时间。
+ conn.zadd('recent:', token, timestamp)
+ if item:
+ # 记录用户浏览过的商品。
+ conn.zadd('viewed:' + token, item, timestamp)
+ # 移除旧的记录,只保留用户最近浏览过的25个商品。
+ conn.zremrangebyrank('viewed:' + token, 0, -26)
#
@@ -45,27 +45,27 @@ LIMIT = 10000000
def clean_sessions(conn):
- while not QUIT:
- # 找出目前已有令牌的数量。
- size = conn.zcard('recent:')
- # 令牌数量未超过限制,休眠并在之后重新检查。
- if size <= LIMIT:
- time.sleep(1)
- continue
+ while not QUIT:
+ # 找出目前已有令牌的数量。
+ size = conn.zcard('recent:')
+ # 令牌数量未超过限制,休眠并在之后重新检查。
+ if size <= LIMIT:
+ time.sleep(1)
+ continue
- # 获取需要移除的令牌ID。
- end_index = min(size - LIMIT, 100)
- tokens = conn.zrange('recent:', 0, end_index - 1)
+ # 获取需要移除的令牌ID。
+ end_index = min(size - LIMIT, 100)
+ tokens = conn.zrange('recent:', 0, end_index - 1)
- # 为那些将要被删除的令牌构建键名。
- session_keys = []
- for token in tokens:
- session_keys.append('viewed:' + token)
+ # 为那些将要被删除的令牌构建键名。
+ session_keys = []
+ for token in tokens:
+ session_keys.append('viewed:' + token)
- # 移除最旧的那些令牌。
- conn.delete(*session_keys)
- conn.hdel('login:', *tokens)
- conn.zrem('recent:', *tokens)
+ # 移除最旧的那些令牌。
+ conn.delete(*session_keys)
+ conn.hdel('login:', *tokens)
+ conn.zrem('recent:', *tokens)
#
@@ -74,35 +74,35 @@ def clean_sessions(conn):
# 代码清单 2-4
#
def add_to_cart(conn, session, item, count):
- if count <= 0:
- # 从购物车里面移除指定的商品。
- conn.hrem('cart:' + session, item)
- else:
- # 将指定的商品添加到购物车。
- conn.hset('cart:' + session, item, count)
- #
+ if count <= 0:
+ # 从购物车里面移除指定的商品。
+ conn.hrem('cart:' + session, item)
+ else:
+ # 将指定的商品添加到购物车。
+ conn.hset('cart:' + session, item, count)
+ #
# 代码清单 2-5
#
def clean_full_sessions(conn):
- while not QUIT:
- size = conn.zcard('recent:')
- if size <= LIMIT:
- time.sleep(1)
- continue
+ while not QUIT:
+ size = conn.zcard('recent:')
+ if size <= LIMIT:
+ time.sleep(1)
+ continue
- end_index = min(size - LIMIT, 100)
- sessions = conn.zrange('recent:', 0, end_index - 1)
+ end_index = min(size - LIMIT, 100)
+ sessions = conn.zrange('recent:', 0, end_index - 1)
- session_keys = []
- for sess in sessions:
- session_keys.append('viewed:' + sess)
- session_keys.append('cart:' + sess) # 新增加的这行代码用于删除旧会话对应用户的购物车。
+ session_keys = []
+ for sess in sessions:
+ session_keys.append('viewed:' + sess)
+ session_keys.append('cart:' + sess) # 新增加的这行代码用于删除旧会话对应用户的购物车。
- conn.delete(*session_keys)
- conn.hdel('login:', *sessions)
- conn.zrem('recent:', *sessions)
+ conn.delete(*session_keys)
+ conn.hdel('login:', *sessions)
+ conn.zrem('recent:', *sessions)
#
@@ -111,23 +111,23 @@ def clean_full_sessions(conn):
# 代码清单 2-6
#
def cache_request(conn, request, callback):
- # 对于不能被缓存的请求,直接调用回调函数。
- if not can_cache(conn, request):
- return callback(request)
+ # 对于不能被缓存的请求,直接调用回调函数。
+ if not can_cache(conn, request):
+ return callback(request)
- # 将请求转换成一个简单的字符串键,方便之后进行查找。
- page_key = 'cache:' + hash_request(request)
- # 尝试查找被缓存的页面。
- content = conn.get(page_key)
+ # 将请求转换成一个简单的字符串键,方便之后进行查找。
+ page_key = 'cache:' + hash_request(request)
+ # 尝试查找被缓存的页面。
+ content = conn.get(page_key)
- if not content:
- # 如果页面还没有被缓存,那么生成页面。
- content = callback(request)
- # 将新生成的页面放到缓存里面。
- conn.setex(page_key, content, 300)
+ if not content:
+ # 如果页面还没有被缓存,那么生成页面。
+ content = callback(request)
+ # 将新生成的页面放到缓存里面。
+ conn.setex(page_key, content, 300)
- # 返回页面。
- return content
+ # 返回页面。
+ return content
#
@@ -136,10 +136,10 @@ def cache_request(conn, request, callback):
# 代码清单 2-7
#
def schedule_row_cache(conn, row_id, delay):
- # 先设置数据行的延迟值。
- conn.zadd('delay:', row_id, delay)
- # 立即缓存数据行。
- conn.zadd('schedule:', row_id, time.time())
+ # 先设置数据行的延迟值。
+ conn.zadd('delay:', row_id, delay)
+ # 立即缓存数据行。
+ conn.zadd('schedule:', row_id, time.time())
#
@@ -148,44 +148,44 @@ def schedule_row_cache(conn, row_id, delay):
# 代码清单 2-8
#
def cache_rows(conn):
- while not QUIT:
- # 尝试获取下一个需要被缓存的数据行以及该行的调度时间戳,
- # 命令会返回一个包含零个或一个元组(tuple)的列表。
- next = conn.zrange('schedule:', 0, 0, withscores=True)
- now = time.time()
- if not next or next[0][1] > now:
- # 暂时没有行需要被缓存,休眠50毫秒后重试。
- time.sleep(.05)
- continue
+ while not QUIT:
+ # 尝试获取下一个需要被缓存的数据行以及该行的调度时间戳,
+ # 命令会返回一个包含零个或一个元组(tuple)的列表。
+ next = conn.zrange('schedule:', 0, 0, withscores=True)
+ now = time.time()
+ if not next or next[0][1] > now:
+ # 暂时没有行需要被缓存,休眠50毫秒后重试。
+ time.sleep(.05)
+ continue
- row_id = next[0][0]
- # 获取下一次调度前的延迟时间。
- delay = conn.zscore('delay:', row_id)
- if delay <= 0:
- # 不必再缓存这个行,将它从缓存中移除。
- conn.zrem('delay:', row_id)
- conn.zrem('schedule:', row_id)
- conn.delete('inv:' + row_id)
- continue
+ row_id = next[0][0]
+ # 获取下一次调度前的延迟时间。
+ delay = conn.zscore('delay:', row_id)
+ if delay <= 0:
+ # 不必再缓存这个行,将它从缓存中移除。
+ conn.zrem('delay:', row_id)
+ conn.zrem('schedule:', row_id)
+ conn.delete('inv:' + row_id)
+ continue
- # 读取数据行。
- row = Inventory.get(row_id)
- # 更新调度时间并设置缓存值。
- conn.zadd('schedule:', row_id, now + delay)
- conn.set('inv:' + row_id, json.dumps(row.to_dict()))
- #
+ # 读取数据行。
+ row = Inventory.get(row_id)
+ # 更新调度时间并设置缓存值。
+ conn.zadd('schedule:', row_id, now + delay)
+ conn.set('inv:' + row_id, json.dumps(row.to_dict()))
+ #
# 代码清单 2-9
#
def update_token(conn, token, user, item=None):
- timestamp = time.time()
- conn.hset('login:', token, user)
- conn.zadd('recent:', token, timestamp)
- if item:
- conn.zadd('viewed:' + token, item, timestamp)
- conn.zremrangebyrank('viewed:' + token, 0, -26)
- conn.zincrby('viewed:', item, -1) # 这行代码是新添加的。
+ timestamp = time.time()
+ conn.hset('login:', token, user)
+ conn.zadd('recent:', token, timestamp)
+ if item:
+ conn.zadd('viewed:' + token, item, timestamp)
+ conn.zremrangebyrank('viewed:' + token, 0, -26)
+ conn.zincrby('viewed:', item, -1) # 这行代码是新添加的。
#
@@ -194,28 +194,28 @@ def update_token(conn, token, user, item=None):
# 代码清单 2-10
#
def rescale_viewed(conn):
- while not QUIT:
- # 删除所有排名在20 000名之后的商品。
- conn.zremrangebyrank('viewed:', 20000, -1)
- # 将浏览次数降低为原来的一半
- conn.zinterstore('viewed:', {'viewed:': .5})
- # 5分钟之后再执行这个操作。
- time.sleep(300)
- #
+ while not QUIT:
+ # 删除所有排名在20 000名之后的商品。
+ conn.zremrangebyrank('viewed:', 20000, -1)
+ # 将浏览次数降低为原来的一半
+ conn.zinterstore('viewed:', {'viewed:': .5})
+ # 5分钟之后再执行这个操作。
+ time.sleep(300)
+ #
# 代码清单 2-11
#
def can_cache(conn, request):
- # 尝试从页面里面取出商品ID。
- item_id = extract_item_id(request)
- # 检查这个页面能否被缓存以及这个页面是否为商品页面。
- if not item_id or is_dynamic(request):
- return False
- # 取得商品的浏览次数排名。
- rank = conn.zrank('viewed:', item_id)
- # 根据商品的浏览次数排名来判断是否需要缓存这个页面。
- return rank is not None and rank < 10000
+ # 尝试从页面里面取出商品ID。
+ item_id = extract_item_id(request)
+ # 检查这个页面能否被缓存以及这个页面是否为商品页面。
+ if not item_id or is_dynamic(request):
+ return False
+ # 取得商品的浏览次数排名。
+ rank = conn.zrank('viewed:', item_id)
+ # 根据商品的浏览次数排名来判断是否需要缓存这个页面。
+ return rank is not None and rank < 10000
#
@@ -224,190 +224,190 @@ def can_cache(conn, request):
# --------------- 以下是用于测试代码的辅助函数 --------------------------------
def extract_item_id(request):
- parsed = urlparse.urlparse(request)
- query = urlparse.parse_qs(parsed.query)
- return (query.get('item') or [None])[0]
+ parsed = urlparse.urlparse(request)
+ query = urlparse.parse_qs(parsed.query)
+ return (query.get('item') or [None])[0]
def is_dynamic(request):
- parsed = urlparse.urlparse(request)
- query = urlparse.parse_qs(parsed.query)
- return '_' in query
+ parsed = urlparse.urlparse(request)
+ query = urlparse.parse_qs(parsed.query)
+ return '_' in query
def hash_request(request):
- return str(hash(request))
+ return str(hash(request))
class Inventory(object):
- def __init__(self, id):
- self.id = id
+ def __init__(self, id):
+ self.id = id
- @classmethod
- def get(cls, id):
- return Inventory(id)
+ @classmethod
+ def get(cls, id):
+ return Inventory(id)
- def to_dict(self):
- return {'id': self.id, 'data': 'data to cache...', 'cached': time.time()}
+ def to_dict(self):
+ return {'id': self.id, 'data': 'data to cache...', 'cached': time.time()}
class TestCh02(unittest.TestCase):
- def setUp(self):
- import redis
- self.conn = redis.Redis(db=15)
+ def setUp(self):
+ import redis
+ self.conn = redis.Redis(db=15)
- def tearDown(self):
- conn = self.conn
- to_del = (
- conn.keys('login:*') + conn.keys('recent:*') + conn.keys('viewed:*') +
- conn.keys('cart:*') + conn.keys('cache:*') + conn.keys('delay:*') +
- conn.keys('schedule:*') + conn.keys('inv:*'))
- if to_del:
- self.conn.delete(*to_del)
- del self.conn
- global QUIT, LIMIT
- QUIT = False
- LIMIT = 10000000
- print
- print
+ def tearDown(self):
+ conn = self.conn
+ to_del = (
+ conn.keys('login:*') + conn.keys('recent:*') + conn.keys('viewed:*') +
+ conn.keys('cart:*') + conn.keys('cache:*') + conn.keys('delay:*') +
+ conn.keys('schedule:*') + conn.keys('inv:*'))
+ if to_del:
+ self.conn.delete(*to_del)
+ del self.conn
+ global QUIT, LIMIT
+ QUIT = False
+ LIMIT = 10000000
+ print
+ print
- def test_login_cookies(self):
- conn = self.conn
- global LIMIT, QUIT
- token = str(uuid.uuid4())
+ def test_login_cookies(self):
+ conn = self.conn
+ global LIMIT, QUIT
+ token = str(uuid.uuid4())
- update_token(conn, token, 'username', 'itemX')
- print "We just logged-in/updated token:", token
- print "For user:", 'username'
- print
+ update_token(conn, token, 'username', 'itemX')
+ print "We just logged-in/updated token:", token
+ print "For user:", 'username'
+ print
- print "What username do we get when we look-up that token?"
- r = check_token(conn, token)
- print r
- print
- self.assertTrue(r)
+ print "What username do we get when we look-up that token?"
+ r = check_token(conn, token)
+ print r
+ print
+ self.assertTrue(r)
- print "Let's drop the maximum number of cookies to 0 to clean them out"
- print "We will start a thread to do the cleaning, while we stop it later"
+ print "Let's drop the maximum number of cookies to 0 to clean them out"
+ print "We will start a thread to do the cleaning, while we stop it later"
- LIMIT = 0
- t = threading.Thread(target=clean_sessions, args=(conn,))
- t.setDaemon(1) # to make sure it dies if we ctrl+C quit
- t.start()
- time.sleep(1)
- QUIT = True
- time.sleep(2)
- if t.isAlive():
- raise Exception("The clean sessions thread is still alive?!?")
+ LIMIT = 0
+ t = threading.Thread(target=clean_sessions, args=(conn,))
+ t.setDaemon(1) # to make sure it dies if we ctrl+C quit
+ t.start()
+ time.sleep(1)
+ QUIT = True
+ time.sleep(2)
+ if t.isAlive():
+ raise Exception("The clean sessions thread is still alive?!?")
- s = conn.hlen('login:')
- print "The current number of sessions still available is:", s
- self.assertFalse(s)
+ s = conn.hlen('login:')
+ print "The current number of sessions still available is:", s
+ self.assertFalse(s)
- def test_shoppping_cart_cookies(self):
- conn = self.conn
- global LIMIT, QUIT
- token = str(uuid.uuid4())
+ def test_shoppping_cart_cookies(self):
+ conn = self.conn
+ global LIMIT, QUIT
+ token = str(uuid.uuid4())
- print "We'll refresh our session..."
- update_token(conn, token, 'username', 'itemX')
- print "And add an item to the shopping cart"
- add_to_cart(conn, token, "itemY", 3)
- r = conn.hgetall('cart:' + token)
- print "Our shopping cart currently has:", r
- print
+ print "We'll refresh our session..."
+ update_token(conn, token, 'username', 'itemX')
+ print "And add an item to the shopping cart"
+ add_to_cart(conn, token, "itemY", 3)
+ r = conn.hgetall('cart:' + token)
+ print "Our shopping cart currently has:", r
+ print
- self.assertTrue(len(r) >= 1)
+ self.assertTrue(len(r) >= 1)
- print "Let's clean out our sessions and carts"
- LIMIT = 0
- t = threading.Thread(target=clean_full_sessions, args=(conn,))
- t.setDaemon(1) # to make sure it dies if we ctrl+C quit
- t.start()
- time.sleep(1)
- QUIT = True
- time.sleep(2)
- if t.isAlive():
- raise Exception("The clean sessions thread is still alive?!?")
+ print "Let's clean out our sessions and carts"
+ LIMIT = 0
+ t = threading.Thread(target=clean_full_sessions, args=(conn,))
+ t.setDaemon(1) # to make sure it dies if we ctrl+C quit
+ t.start()
+ time.sleep(1)
+ QUIT = True
+ time.sleep(2)
+ if t.isAlive():
+ raise Exception("The clean sessions thread is still alive?!?")
- r = conn.hgetall('cart:' + token)
- print "Our shopping cart now contains:", r
+ r = conn.hgetall('cart:' + token)
+ print "Our shopping cart now contains:", r
- self.assertFalse(r)
+ self.assertFalse(r)
- def test_cache_request(self):
- conn = self.conn
- token = str(uuid.uuid4())
+ def test_cache_request(self):
+ conn = self.conn
+ token = str(uuid.uuid4())
- def callback(request):
- return "content for " + request
+ def callback(request):
+ return "content for " + request
- update_token(conn, token, 'username', 'itemX')
- url = 'http://test.com/?item=itemX'
- print "We are going to cache a simple request against", url
- result = cache_request(conn, url, callback)
- print "We got initial content:", repr(result)
- print
+ update_token(conn, token, 'username', 'itemX')
+ url = 'http://test.com/?item=itemX'
+ print "We are going to cache a simple request against", url
+ result = cache_request(conn, url, callback)
+ print "We got initial content:", repr(result)
+ print
- self.assertTrue(result)
+ self.assertTrue(result)
- print "To test that we've cached the request, we'll pass a bad callback"
- result2 = cache_request(conn, url, None)
- print "We ended up getting the same response!", repr(result2)
+ print "To test that we've cached the request, we'll pass a bad callback"
+ result2 = cache_request(conn, url, None)
+ print "We ended up getting the same response!", repr(result2)
- self.assertEquals(result, result2)
+ self.assertEquals(result, result2)
- self.assertFalse(can_cache(conn, 'http://test.com/'))
- self.assertFalse(can_cache(conn, 'http://test.com/?item=itemX&_=1234536'))
+ self.assertFalse(can_cache(conn, 'http://test.com/'))
+ self.assertFalse(can_cache(conn, 'http://test.com/?item=itemX&_=1234536'))
- def test_cache_rows(self):
- import pprint
- conn = self.conn
- global QUIT
+ def test_cache_rows(self):
+ import pprint
+ conn = self.conn
+ global QUIT
- print "First, let's schedule caching of itemX every 5 seconds"
- schedule_row_cache(conn, 'itemX', 5)
- print "Our schedule looks like:"
- s = conn.zrange('schedule:', 0, -1, withscores=True)
- pprint.pprint(s)
- self.assertTrue(s)
+ print "First, let's schedule caching of itemX every 5 seconds"
+ schedule_row_cache(conn, 'itemX', 5)
+ print "Our schedule looks like:"
+ s = conn.zrange('schedule:', 0, -1, withscores=True)
+ pprint.pprint(s)
+ self.assertTrue(s)
- print "We'll start a caching thread that will cache the data..."
- t = threading.Thread(target=cache_rows, args=(conn,))
- t.setDaemon(1)
- t.start()
+ print "We'll start a caching thread that will cache the data..."
+ t = threading.Thread(target=cache_rows, args=(conn,))
+ t.setDaemon(1)
+ t.start()
- time.sleep(1)
- print "Our cached data looks like:"
- r = conn.get('inv:itemX')
- print repr(r)
- self.assertTrue(r)
- print
- print "We'll check again in 5 seconds..."
- time.sleep(5)
- print "Notice that the data has changed..."
- r2 = conn.get('inv:itemX')
- print repr(r2)
- print
- self.assertTrue(r2)
- self.assertTrue(r != r2)
+ time.sleep(1)
+ print "Our cached data looks like:"
+ r = conn.get('inv:itemX')
+ print repr(r)
+ self.assertTrue(r)
+ print
+ print "We'll check again in 5 seconds..."
+ time.sleep(5)
+ print "Notice that the data has changed..."
+ r2 = conn.get('inv:itemX')
+ print repr(r2)
+ print
+ self.assertTrue(r2)
+ self.assertTrue(r != r2)
- print "Let's force un-caching"
- schedule_row_cache(conn, 'itemX', -1)
- time.sleep(1)
- r = conn.get('inv:itemX')
- print "The cache was cleared?", not r
- print
- self.assertFalse(r)
+ print "Let's force un-caching"
+ schedule_row_cache(conn, 'itemX', -1)
+ time.sleep(1)
+ r = conn.get('inv:itemX')
+ print "The cache was cleared?", not r
+ print
+ self.assertFalse(r)
- QUIT = True
- time.sleep(2)
- if t.isAlive():
- raise Exception("The database caching thread is still alive?!?")
+ QUIT = True
+ time.sleep(2)
+ if t.isAlive():
+ raise Exception("The database caching thread is still alive?!?")
- # We aren't going to bother with the top 10k requests are cached, as
- # we already tested it as part of the cached requests test.
+ # We aren't going to bother with the top 10k requests are cached, as
+ # we already tested it as part of the cached requests test.
if __name__ == '__main__':
- unittest.main()
+ unittest.main()
diff --git a/codes/redis/redis-in-action-py/ch03_listing_source.py b/codes/redis/redis-in-action-py/ch03_listing_source.py
index 807f691..320538e 100644
--- a/codes/redis/redis-in-action-py/ch03_listing_source.py
+++ b/codes/redis/redis-in-action-py/ch03_listing_source.py
@@ -114,18 +114,18 @@ True #
#
def update_token(conn, token, user, item=None):
- timestamp = time.time()
- conn.hset('login:', token, user)
- conn.zadd('recent:', token, timestamp)
- if item:
- key = 'viewed:' + token
- # 如果指定的元素存在于列表当中,那么移除它
- conn.lrem(key, item)
- # 将元素推入到列表的右端,使得 ZRANGE 和 LRANGE 可以取得相同的结果
- conn.rpush(key, item)
- # 对列表进行修剪,让它最多只能保存 25 个元素
- conn.ltrim(key, -25, -1)
- conn.zincrby('viewed:', item, -1)
+ timestamp = time.time()
+ conn.hset('login:', token, user)
+ conn.zadd('recent:', token, timestamp)
+ if item:
+ key = 'viewed:' + token
+ # 如果指定的元素存在于列表当中,那么移除它
+ conn.lrem(key, item)
+ # 将元素推入到列表的右端,使得 ZRANGE 和 LRANGE 可以取得相同的结果
+ conn.rpush(key, item)
+ # 对列表进行修剪,让它最多只能保存 25 个元素
+ conn.ltrim(key, -25, -1)
+ conn.zincrby('viewed:', item, -1)
#
@@ -247,24 +247,24 @@ True #
def publisher(n):
- time.sleep(1)
- for i in xrange(n):
- conn.publish('channel', i)
- time.sleep(1)
+ time.sleep(1)
+ for i in xrange(n):
+ conn.publish('channel', i)
+ time.sleep(1)
def run_pubsub():
- threading.Thread(target=publisher, args=(3,)).start()
- pubsub = conn.pubsub()
- pubsub.subscribe(['channel'])
- count = 0
- for item in pubsub.listen():
- print item
- count += 1
- if count == 4:
- pubsub.unsubscribe()
- if count == 5:
- break
+ threading.Thread(target=publisher, args=(3,)).start()
+ pubsub = conn.pubsub()
+ pubsub.subscribe(['channel'])
+ count = 0
+ for item in pubsub.listen():
+ print item
+ count += 1
+ if count == 4:
+ pubsub.unsubscribe()
+ if count == 5:
+ break
# 代码清单 3-11
@@ -380,23 +380,23 @@ def run_pubsub():
#
def article_vote(conn, user, article):
- # 在进行投票之前,先检查这篇文章是否仍然处于可投票的时间之内
- cutoff = time.time() - ONE_WEEK_IN_SECONDS
- posted = conn.zscore('time:', article)
- if posted < cutoff:
- return
+ # 在进行投票之前,先检查这篇文章是否仍然处于可投票的时间之内
+ cutoff = time.time() - ONE_WEEK_IN_SECONDS
+ posted = conn.zscore('time:', article)
+ if posted < cutoff:
+ return
- article_id = article.partition(':')[-1]
- pipeline = conn.pipeline()
- pipeline.sadd('voted:' + article_id, user)
- # 为文章的投票设置过期时间
- pipeline.expire('voted:' + article_id, int(posted - cutoff))
- if pipeline.execute()[0]:
- # 因为客户端可能会在执行 SADD/EXPIRE 之间或者执行 ZINCRBY/HINCRBY 之间掉线
- # 所以投票可能会不被计数,但这总比在执行 ZINCRBY/HINCRBY 之间失败并导致不完整的计数要好
- pipeline.zincrby('score:', article, VOTE_SCORE)
- pipeline.hincrby(article, 'votes', 1)
- pipeline.execute()
+ article_id = article.partition(':')[-1]
+ pipeline = conn.pipeline()
+ pipeline.sadd('voted:' + article_id, user)
+ # 为文章的投票设置过期时间
+ pipeline.expire('voted:' + article_id, int(posted - cutoff))
+ if pipeline.execute()[0]:
+ # 因为客户端可能会在执行 SADD/EXPIRE 之间或者执行 ZINCRBY/HINCRBY 之间掉线
+ # 所以投票可能会不被计数,但这总比在执行 ZINCRBY/HINCRBY 之间失败并导致不完整的计数要好
+ pipeline.zincrby('score:', article, VOTE_SCORE)
+ pipeline.hincrby(article, 'votes', 1)
+ pipeline.execute()
#
@@ -406,48 +406,48 @@ def article_vote(conn, user, article):
# 这段代码里面用到了本书第 4 章才会介绍的技术
def article_vote(conn, user, article):
- cutoff = time.time() - ONE_WEEK_IN_SECONDS
- posted = conn.zscore('time:', article)
- article_id = article.partition(':')[-1]
- voted = 'voted:' + article_id
+ cutoff = time.time() - ONE_WEEK_IN_SECONDS
+ posted = conn.zscore('time:', article)
+ article_id = article.partition(':')[-1]
+ voted = 'voted:' + article_id
- pipeline = conn.pipeline()
- while posted > cutoff:
- try:
- pipeline.watch(voted)
- if not pipeline.sismember(voted, user):
- pipeline.multi()
- pipeline.sadd(voted, user)
- pipeline.expire(voted, int(posted - cutoff))
- pipeline.zincrby('score:', article, VOTE_SCORE)
- pipeline.hincrby(article, 'votes', 1)
- pipeline.execute()
- else:
- pipeline.unwatch()
- return
- except redis.exceptions.WatchError:
- cutoff = time.time() - ONE_WEEK_IN_SECONDS
+ pipeline = conn.pipeline()
+ while posted > cutoff:
+ try:
+ pipeline.watch(voted)
+ if not pipeline.sismember(voted, user):
+ pipeline.multi()
+ pipeline.sadd(voted, user)
+ pipeline.expire(voted, int(posted - cutoff))
+ pipeline.zincrby('score:', article, VOTE_SCORE)
+ pipeline.hincrby(article, 'votes', 1)
+ pipeline.execute()
+ else:
+ pipeline.unwatch()
+ return
+ except redis.exceptions.WatchError:
+ cutoff = time.time() - ONE_WEEK_IN_SECONDS
#
def get_articles(conn, page, order='score:'):
- start = max(page - 1, 0) * ARTICLES_PER_PAGE
- end = start + ARTICLES_PER_PAGE - 1
+ start = max(page - 1, 0) * ARTICLES_PER_PAGE
+ end = start + ARTICLES_PER_PAGE - 1
- ids = conn.zrevrangebyscore(order, start, end)
+ ids = conn.zrevrangebyscore(order, start, end)
- pipeline = conn.pipeline()
- # 将等待执行的多个 HGETALL 调用放入流水线
- map(pipeline.hgetall, ids) # A
+ pipeline = conn.pipeline()
+ # 将等待执行的多个 HGETALL 调用放入流水线
+ map(pipeline.hgetall, ids) # A
- articles = []
- # 执行被流水线包含的多个 HGETALL 命令,
- # 并将执行所得的多个 id 添加到 articles 变量里面
- for id, article_data in zip(ids, pipeline.execute()): # B
- article_data['id'] = id
- articles.append(article_data)
+ articles = []
+ # 执行被流水线包含的多个 HGETALL 命令,
+ # 并将执行所得的多个 id 添加到 articles 变量里面
+ for id, article_data in zip(ids, pipeline.execute()): # B
+ article_data['id'] = id
+ articles.append(article_data)
- return articles
+ return articles
#
@@ -477,31 +477,31 @@ THIRTY_DAYS = 30 * 86400
def check_token(conn, token):
- # 为了能够对登录令牌进行过期,我们将把它存储为字符串值
- return conn.get('login:' + token)
+ # 为了能够对登录令牌进行过期,我们将把它存储为字符串值
+ return conn.get('login:' + token)
def update_token(conn, token, user, item=None):
- # 在一次命令调用里面,同时为字符串键设置值和过期时间
- conn.setex('login:' + token, user, THIRTY_DAYS)
- key = 'viewed:' + token
- if item:
- conn.lrem(key, item)
- conn.rpush(key, item)
- conn.ltrim(key, -25, -1)
- # 跟字符串不一样,Redis 并没有提供能够在操作列表的同时,
- # 为列表设置过期时间的命令,
- # 所以我们需要在这里调用 EXPIRE 命令来为列表设置过期时间
- conn.expire(key, THIRTY_DAYS)
- conn.zincrby('viewed:', item, -1)
+ # 在一次命令调用里面,同时为字符串键设置值和过期时间
+ conn.setex('login:' + token, user, THIRTY_DAYS)
+ key = 'viewed:' + token
+ if item:
+ conn.lrem(key, item)
+ conn.rpush(key, item)
+ conn.ltrim(key, -25, -1)
+ # 跟字符串不一样,Redis 并没有提供能够在操作列表的同时,
+ # 为列表设置过期时间的命令,
+ # 所以我们需要在这里调用 EXPIRE 命令来为列表设置过期时间
+ conn.expire(key, THIRTY_DAYS)
+ conn.zincrby('viewed:', item, -1)
def add_to_cart(conn, session, item, count):
- key = 'cart:' + session
- if count <= 0:
- conn.hrem(key, item)
- else:
- conn.hset(key, item, count)
- # 散列也和列表一样,需要通过调用 EXPIRE 命令来设置过期时间
- conn.expire(key, THIRTY_DAYS)
+ key = 'cart:' + session
+ if count <= 0:
+ conn.hrem(key, item)
+ else:
+ conn.hset(key, item, count)
+ # 散列也和列表一样,需要通过调用 EXPIRE 命令来设置过期时间
+ conn.expire(key, THIRTY_DAYS)
#
diff --git a/codes/redis/redis-in-action-py/ch04_listing_source.py b/codes/redis/redis-in-action-py/ch04_listing_source.py
index 5e22f4a..df68ac0 100644
--- a/codes/redis/redis-in-action-py/ch04_listing_source.py
+++ b/codes/redis/redis-in-action-py/ch04_listing_source.py
@@ -30,56 +30,56 @@ dir ./ # 共享选项,这个选项决定了快照
# 这个回调函数接受一个Redis连接和一个日志行作为参数,
# 并通过调用流水线对象的方法来执行Redis命令。
def process_logs(conn, path, callback):
- # 获取文件当前的处理进度。
- current_file, offset = conn.mget(
- 'progress:file', 'progress:position')
+ # 获取文件当前的处理进度。
+ current_file, offset = conn.mget(
+ 'progress:file', 'progress:position')
- pipe = conn.pipeline()
+ pipe = conn.pipeline()
- # 通过使用闭包(closure)来减少重复代码
- def update_progress():
- # 更新正在处理的日志文件的名字和偏移量。
- pipe.mset({
- 'progress:file': fname,
- 'progress:position': offset
- })
- # 这个语句负责执行实际的日志更新操作,
- # 并将日志文件的名字和目前的处理进度记录到Redis里面。
- pipe.execute()
+ # 通过使用闭包(closure)来减少重复代码
+ def update_progress():
+ # 更新正在处理的日志文件的名字和偏移量。
+ pipe.mset({
+ 'progress:file': fname,
+ 'progress:position': offset
+ })
+ # 这个语句负责执行实际的日志更新操作,
+ # 并将日志文件的名字和目前的处理进度记录到Redis里面。
+ pipe.execute()
- # 有序地遍历各个日志文件。
- for fname in sorted(os.listdir(path)):
- # 略过所有已处理的日志文件。
- if fname < current_file:
- continue
+ # 有序地遍历各个日志文件。
+ for fname in sorted(os.listdir(path)):
+ # 略过所有已处理的日志文件。
+ if fname < current_file:
+ continue
- inp = open(os.path.join(path, fname), 'rb')
- # 在接着处理一个因为系统崩溃而未能完成处理的日志文件时,略过已处理的内容。
- if fname == current_file:
- inp.seek(int(offset, 10))
- else:
- offset = 0
+ inp = open(os.path.join(path, fname), 'rb')
+ # 在接着处理一个因为系统崩溃而未能完成处理的日志文件时,略过已处理的内容。
+ if fname == current_file:
+ inp.seek(int(offset, 10))
+ else:
+ offset = 0
- current_file = None
+ current_file = None
- # 枚举函数遍历一个由文件行组成的序列,
- # 并返回任意多个二元组,
- # 每个二元组包含了行号lno和行数据line,
- # 其中行号从0开始。
- for lno, line in enumerate(inp):
- # 处理日志行。
- callback(pipe, line)
- # 更新已处理内容的偏移量。
- offset += int(offset) + len(line)
+ # 枚举函数遍历一个由文件行组成的序列,
+ # 并返回任意多个二元组,
+ # 每个二元组包含了行号lno和行数据line,
+ # 其中行号从0开始。
+ for lno, line in enumerate(inp):
+ # 处理日志行。
+ callback(pipe, line)
+ # 更新已处理内容的偏移量。
+ offset += int(offset) + len(line)
- # 每当处理完1000个日志行或者处理完整个日志文件的时候,
- # 都更新一次文件的处理进度。
- if not (lno + 1) % 1000:
- update_progress()
+ # 每当处理完1000个日志行或者处理完整个日志文件的时候,
+ # 都更新一次文件的处理进度。
+ if not (lno + 1) % 1000:
+ update_progress()
- update_progress()
+ update_progress()
- inp.close()
+ inp.close()
#
@@ -88,29 +88,29 @@ def process_logs(conn, path, callback):
# 代码清单 4-3
#
def wait_for_sync(mconn, sconn):
- identifier = str(uuid.uuid4())
- # 将令牌添加至主服务器。
- mconn.zadd('sync:wait', identifier, time.time())
+ identifier = str(uuid.uuid4())
+ # 将令牌添加至主服务器。
+ mconn.zadd('sync:wait', identifier, time.time())
- # 如果有必要的话,等待从服务器完成同步。
- while sconn.info()['master_link_status'] != 'up':
- time.sleep(.001)
+ # 如果有必要的话,等待从服务器完成同步。
+ while sconn.info()['master_link_status'] != 'up':
+ time.sleep(.001)
- # 等待从服务器接收数据更新。
- while not sconn.zscore('sync:wait', identifier):
- time.sleep(.001)
+ # 等待从服务器接收数据更新。
+ while not sconn.zscore('sync:wait', identifier):
+ time.sleep(.001)
- # 最多只等待一秒钟。
- deadline = time.time() + 1.01
- while time.time() < deadline:
- # 检查数据更新是否已经被同步到了磁盘。
- if sconn.info()['aof_pending_bio_fsync'] == 0:
- break
- time.sleep(.001)
+ # 最多只等待一秒钟。
+ deadline = time.time() + 1.01
+ while time.time() < deadline:
+ # 检查数据更新是否已经被同步到了磁盘。
+ if sconn.info()['aof_pending_bio_fsync'] == 0:
+ break
+ time.sleep(.001)
- # 清理刚刚创建的新令牌以及之前可能留下的旧令牌。
- mconn.zrem('sync:wait', identifier)
- mconn.zremrangebyscore('sync:wait', 0, time.time() - 900)
+ # 清理刚刚创建的新令牌以及之前可能留下的旧令牌。
+ mconn.zrem('sync:wait', identifier)
+ mconn.zremrangebyscore('sync:wait', 0, time.time() - 900)
#
@@ -153,35 +153,35 @@ user@vpn-master ~:$
# 代码清单 4-5
#
def list_item(conn, itemid, sellerid, price):
- inventory = "inventory:%s" % sellerid
- item = "%s.%s" % (itemid, sellerid)
- end = time.time() + 5
- pipe = conn.pipeline()
+ inventory = "inventory:%s" % sellerid
+ item = "%s.%s" % (itemid, sellerid)
+ end = time.time() + 5
+ pipe = conn.pipeline()
- while time.time() < end:
- try:
- # 监视用户包裹发生的变化。
- pipe.watch(inventory)
- # 验证用户是否仍然持有指定的物品。
- if not pipe.sismember(inventory, itemid):
- # 如果指定的物品不在用户的包裹里面,
- # 那么停止对包裹键的监视并返回一个空值。
- pipe.unwatch()
- return None
+ while time.time() < end:
+ try:
+ # 监视用户包裹发生的变化。
+ pipe.watch(inventory)
+ # 验证用户是否仍然持有指定的物品。
+ if not pipe.sismember(inventory, itemid):
+ # 如果指定的物品不在用户的包裹里面,
+ # 那么停止对包裹键的监视并返回一个空值。
+ pipe.unwatch()
+ return None
- # 将指定的物品添加到物品买卖市场里面。
- pipe.multi()
- pipe.zadd("market:", item, price)
- pipe.srem(inventory, itemid)
- # 如果执行execute方法没有引发WatchError异常,
- # 那么说明事务执行成功,
- # 并且对包裹键的监视也已经结束。
- pipe.execute()
- return True
- # 用户的包裹已经发生了变化;重试。
- except redis.exceptions.WatchError:
- pass
- return False
+ # 将指定的物品添加到物品买卖市场里面。
+ pipe.multi()
+ pipe.zadd("market:", item, price)
+ pipe.srem(inventory, itemid)
+ # 如果执行execute方法没有引发WatchError异常,
+ # 那么说明事务执行成功,
+ # 并且对包裹键的监视也已经结束。
+ pipe.execute()
+ return True
+ # 用户的包裹已经发生了变化;重试。
+ except redis.exceptions.WatchError:
+ pass
+ return False
#
@@ -190,39 +190,39 @@ def list_item(conn, itemid, sellerid, price):
# 代码清单 4-6
#
def purchase_item(conn, buyerid, itemid, sellerid, lprice):
- buyer = "users:%s" % buyerid
- seller = "users:%s" % sellerid
- item = "%s.%s" % (itemid, sellerid)
- inventory = "inventory:%s" % buyerid
- end = time.time() + 10
- pipe = conn.pipeline()
+ buyer = "users:%s" % buyerid
+ seller = "users:%s" % sellerid
+ item = "%s.%s" % (itemid, sellerid)
+ inventory = "inventory:%s" % buyerid
+ end = time.time() + 10
+ pipe = conn.pipeline()
- while time.time() < end:
- try:
- # 对物品买卖市场以及买家账号信息的变化进行监视。
- pipe.watch("market:", buyer)
+ while time.time() < end:
+ try:
+ # 对物品买卖市场以及买家账号信息的变化进行监视。
+ pipe.watch("market:", buyer)
- # 检查指定物品的价格是否出现了变化,
- # 以及买家是否有足够的钱来购买指定的物品。
- price = pipe.zscore("market:", item)
- funds = int(pipe.hget(buyer, "funds"))
- if price != lprice or price > funds:
- pipe.unwatch()
- return None
+ # 检查指定物品的价格是否出现了变化,
+ # 以及买家是否有足够的钱来购买指定的物品。
+ price = pipe.zscore("market:", item)
+ funds = int(pipe.hget(buyer, "funds"))
+ if price != lprice or price > funds:
+ pipe.unwatch()
+ return None
- # 将买家支付的货款转移给卖家,并将卖家出售的物品移交给买家。
- pipe.multi()
- pipe.hincrby(seller, "funds", int(price))
- pipe.hincrby(buyer, "funds", int(-price))
- pipe.sadd(inventory, itemid)
- pipe.zrem("market:", item)
- pipe.execute()
- return True
- # 如果买家的账号或者物品买卖市场出现了变化,那么进行重试。
- except redis.exceptions.WatchError:
- pass
+ # 将买家支付的货款转移给卖家,并将卖家出售的物品移交给买家。
+ pipe.multi()
+ pipe.hincrby(seller, "funds", int(price))
+ pipe.hincrby(buyer, "funds", int(-price))
+ pipe.sadd(inventory, itemid)
+ pipe.zrem("market:", item)
+ pipe.execute()
+ return True
+ # 如果买家的账号或者物品买卖市场出现了变化,那么进行重试。
+ except redis.exceptions.WatchError:
+ pass
- return False
+ return False
#
@@ -231,36 +231,36 @@ def purchase_item(conn, buyerid, itemid, sellerid, lprice):
# 代码清单 4-7
#
def update_token(conn, token, user, item=None):
- # 获取时间戳。
- timestamp = time.time()
- # 创建令牌与已登录用户之间的映射。
- conn.hset('login:', token, user)
- # 记录令牌最后一次出现的时间。
- conn.zadd('recent:', token, timestamp)
- if item:
- # 把用户浏览过的商品记录起来。
- conn.zadd('viewed:' + token, item, timestamp)
- # 移除旧商品,只记录最新浏览的25件商品。
- conn.zremrangebyrank('viewed:' + token, 0, -26)
- # 更新给定商品的被浏览次数。
- conn.zincrby('viewed:', item, -1)
- #
+ # 获取时间戳。
+ timestamp = time.time()
+ # 创建令牌与已登录用户之间的映射。
+ conn.hset('login:', token, user)
+ # 记录令牌最后一次出现的时间。
+ conn.zadd('recent:', token, timestamp)
+ if item:
+ # 把用户浏览过的商品记录起来。
+ conn.zadd('viewed:' + token, item, timestamp)
+ # 移除旧商品,只记录最新浏览的25件商品。
+ conn.zremrangebyrank('viewed:' + token, 0, -26)
+ # 更新给定商品的被浏览次数。
+ conn.zincrby('viewed:', item, -1)
+ #
# 代码清单 4-8
#
def update_token_pipeline(conn, token, user, item=None):
- timestamp = time.time()
- # 设置流水线。
- pipe = conn.pipeline(False) # A
- pipe.hset('login:', token, user)
- pipe.zadd('recent:', token, timestamp)
- if item:
- pipe.zadd('viewed:' + token, item, timestamp)
- pipe.zremrangebyrank('viewed:' + token, 0, -26)
- pipe.zincrby('viewed:', item, -1)
- # 执行那些被流水线包裹的命令。
- pipe.execute() # B
+ timestamp = time.time()
+ # 设置流水线。
+ pipe = conn.pipeline(False) # A
+ pipe.hset('login:', token, user)
+ pipe.zadd('recent:', token, timestamp)
+ if item:
+ pipe.zadd('viewed:' + token, item, timestamp)
+ pipe.zremrangebyrank('viewed:' + token, 0, -26)
+ pipe.zincrby('viewed:', item, -1)
+ # 执行那些被流水线包裹的命令。
+ pipe.execute() # B
#
@@ -269,20 +269,20 @@ def update_token_pipeline(conn, token, user, item=None):
# 代码清单 4-9
#
def benchmark_update_token(conn, duration):
- # 测试会分别执行update_token()函数和update_token_pipeline()函数。
- for function in (update_token, update_token_pipeline):
- # 设置计数器以及测试结束的条件。
- count = 0 # B
- start = time.time() # B
- end = start + duration # B
- while time.time() < end:
- count += 1
- # 调用两个函数的其中一个。
- function(conn, 'token', 'user', 'item') # C
- # 计算函数的执行时长。
- delta = time.time() - start # D
- # 打印测试结果。
- print function.__name__, count, delta, count / delta # E
+ # 测试会分别执行update_token()函数和update_token_pipeline()函数。
+ for function in (update_token, update_token_pipeline):
+ # 设置计数器以及测试结束的条件。
+ count = 0 # B
+ start = time.time() # B
+ end = start + duration # B
+ while time.time() < end:
+ count += 1
+ # 调用两个函数的其中一个。
+ function(conn, 'token', 'user', 'item') # C
+ # 计算函数的执行时长。
+ delta = time.time() - start # D
+ # 打印测试结果。
+ print function.__name__, count, delta, count / delta # E
#
@@ -316,75 +316,75 @@ LRANGE (first 600 elements): 9041.59 requests per second
# --------------- 以下是用于测试代码的辅助函数 --------------------------------
class TestCh04(unittest.TestCase):
- def setUp(self):
- import redis
- self.conn = redis.Redis(db=15)
- self.conn.flushdb()
+ def setUp(self):
+ import redis
+ self.conn = redis.Redis(db=15)
+ self.conn.flushdb()
- def tearDown(self):
- self.conn.flushdb()
- del self.conn
- print
- print
+ def tearDown(self):
+ self.conn.flushdb()
+ del self.conn
+ print
+ print
- # We can't test process_logs, as that would require writing to disk, which
- # we don't want to do.
+ # We can't test process_logs, as that would require writing to disk, which
+ # we don't want to do.
- # We also can't test wait_for_sync, as we can't guarantee that there are
- # multiple Redis servers running with the proper configuration
+ # We also can't test wait_for_sync, as we can't guarantee that there are
+ # multiple Redis servers running with the proper configuration
- def test_list_item(self):
- import pprint
- conn = self.conn
+ def test_list_item(self):
+ import pprint
+ conn = self.conn
- print "We need to set up just enough state so that a user can list an item"
- seller = 'userX'
- item = 'itemX'
- conn.sadd('inventory:' + seller, item)
- i = conn.smembers('inventory:' + seller)
- print "The user's inventory has:", i
- self.assertTrue(i)
- print
+ print "We need to set up just enough state so that a user can list an item"
+ seller = 'userX'
+ item = 'itemX'
+ conn.sadd('inventory:' + seller, item)
+ i = conn.smembers('inventory:' + seller)
+ print "The user's inventory has:", i
+ self.assertTrue(i)
+ print
- print "Listing the item..."
- l = list_item(conn, item, seller, 10)
- print "Listing the item succeeded?", l
- self.assertTrue(l)
- r = conn.zrange('market:', 0, -1, withscores=True)
- print "The market contains:"
- pprint.pprint(r)
- self.assertTrue(r)
- self.assertTrue(any(x[0] == 'itemX.userX' for x in r))
+ print "Listing the item..."
+ l = list_item(conn, item, seller, 10)
+ print "Listing the item succeeded?", l
+ self.assertTrue(l)
+ r = conn.zrange('market:', 0, -1, withscores=True)
+ print "The market contains:"
+ pprint.pprint(r)
+ self.assertTrue(r)
+ self.assertTrue(any(x[0] == 'itemX.userX' for x in r))
- def test_purchase_item(self):
- self.test_list_item()
- conn = self.conn
+ def test_purchase_item(self):
+ self.test_list_item()
+ conn = self.conn
- print "We need to set up just enough state so a user can buy an item"
- buyer = 'userY'
- conn.hset('users:userY', 'funds', 125)
- r = conn.hgetall('users:userY')
- print "The user has some money:", r
- self.assertTrue(r)
- self.assertTrue(r.get('funds'))
- print
+ print "We need to set up just enough state so a user can buy an item"
+ buyer = 'userY'
+ conn.hset('users:userY', 'funds', 125)
+ r = conn.hgetall('users:userY')
+ print "The user has some money:", r
+ self.assertTrue(r)
+ self.assertTrue(r.get('funds'))
+ print
- print "Let's purchase an item"
- p = purchase_item(conn, 'userY', 'itemX', 'userX', 10)
- print "Purchasing an item succeeded?", p
- self.assertTrue(p)
- r = conn.hgetall('users:userY')
- print "Their money is now:", r
- self.assertTrue(r)
- i = conn.smembers('inventory:' + buyer)
- print "Their inventory is now:", i
- self.assertTrue(i)
- self.assertTrue('itemX' in i)
- self.assertEquals(conn.zscore('market:', 'itemX.userX'), None)
+ print "Let's purchase an item"
+ p = purchase_item(conn, 'userY', 'itemX', 'userX', 10)
+ print "Purchasing an item succeeded?", p
+ self.assertTrue(p)
+ r = conn.hgetall('users:userY')
+ print "Their money is now:", r
+ self.assertTrue(r)
+ i = conn.smembers('inventory:' + buyer)
+ print "Their inventory is now:", i
+ self.assertTrue(i)
+ self.assertTrue('itemX' in i)
+ self.assertEquals(conn.zscore('market:', 'itemX.userX'), None)
- def test_benchmark_update_token(self):
- benchmark_update_token(self.conn, 5)
+ def test_benchmark_update_token(self):
+ benchmark_update_token(self.conn, 5)
if __name__ == '__main__':
- unittest.main()
+ unittest.main()
diff --git a/codes/redis/redis-in-action-py/ch05_listing_source.py b/codes/redis/redis-in-action-py/ch05_listing_source.py
index 3dacbdd..11dbd69 100644
--- a/codes/redis/redis-in-action-py/ch05_listing_source.py
+++ b/codes/redis/redis-in-action-py/ch05_listing_source.py
@@ -23,30 +23,30 @@ config_connection = None
#
# 设置一个字典,它可以帮助我们将大部分日志的安全级别转换成某种一致的东西。
SEVERITY = {
- logging.DEBUG: 'debug',
- logging.INFO: 'info',
- logging.WARNING: 'warning',
- logging.ERROR: 'error',
- logging.CRITICAL: 'critical',
+ logging.DEBUG: 'debug',
+ logging.INFO: 'info',
+ logging.WARNING: 'warning',
+ logging.ERROR: 'error',
+ logging.CRITICAL: 'critical',
}
SEVERITY.update((name, name) for name in SEVERITY.values())
def log_recent(conn, name, message, severity=logging.INFO, pipe=None):
- # 尝试将日志的级别转换成简单的字符串。
- severity = str(SEVERITY.get(severity, severity)).lower()
- # 创建负责存储消息的键。
- destination = 'recent:%s:%s' % (name, severity)
- # 将当前时间添加到消息里面,用于记录消息的发送时间。
- message = time.asctime() + ' ' + message
- # 使用流水线来将通信往返次数降低为一次。
- pipe = pipe or conn.pipeline()
- # 将消息添加到日志列表的最前面。
- pipe.lpush(destination, message)
- # 对日志列表进行修剪,让它只包含最新的100条消息。
- pipe.ltrim(destination, 0, 99)
- # 执行两个命令。
- pipe.execute()
+ # 尝试将日志的级别转换成简单的字符串。
+ severity = str(SEVERITY.get(severity, severity)).lower()
+ # 创建负责存储消息的键。
+ destination = 'recent:%s:%s' % (name, severity)
+ # 将当前时间添加到消息里面,用于记录消息的发送时间。
+ message = time.asctime() + ' ' + message
+ # 使用流水线来将通信往返次数降低为一次。
+ pipe = pipe or conn.pipeline()
+ # 将消息添加到日志列表的最前面。
+ pipe.lpush(destination, message)
+ # 对日志列表进行修剪,让它只包含最新的100条消息。
+ pipe.ltrim(destination, 0, 99)
+ # 执行两个命令。
+ pipe.execute()
#
@@ -55,42 +55,42 @@ def log_recent(conn, name, message, severity=logging.INFO, pipe=None):
# 代码清单 5-2
#
def log_common(conn, name, message, severity=logging.INFO, timeout=5):
- # 设置日志的级别。
- severity = str(SEVERITY.get(severity, severity)).lower()
- # 负责存储最新日志的键。
- destination = 'common:%s:%s' % (name, severity)
- # 因为程序每小时需要轮换一次日志,所以它使用一个键来记录当前所处的小时数。
- start_key = destination + ':start'
- pipe = conn.pipeline()
- end = time.time() + timeout
- while time.time() < end:
- try:
- # 对记录当前小时数的键进行监视,确保轮换操作可以正确地执行。
- pipe.watch(start_key)
- # 取得当前时间。
- now = datetime.utcnow().timetuple()
- # 取得当前所处的小时数。
- hour_start = datetime(*now[:4]).isoformat()
+ # 设置日志的级别。
+ severity = str(SEVERITY.get(severity, severity)).lower()
+ # 负责存储最新日志的键。
+ destination = 'common:%s:%s' % (name, severity)
+ # 因为程序每小时需要轮换一次日志,所以它使用一个键来记录当前所处的小时数。
+ start_key = destination + ':start'
+ pipe = conn.pipeline()
+ end = time.time() + timeout
+ while time.time() < end:
+ try:
+ # 对记录当前小时数的键进行监视,确保轮换操作可以正确地执行。
+ pipe.watch(start_key)
+ # 取得当前时间。
+ now = datetime.utcnow().timetuple()
+ # 取得当前所处的小时数。
+ hour_start = datetime(*now[:4]).isoformat()
- existing = pipe.get(start_key)
- # 创建一个事务。
- pipe.multi()
- # 如果目前的常见日志列表是上一个小时的……
- if existing and existing < hour_start:
- # ……那么将旧的常见日志信息进行归档。
- pipe.rename(destination, destination + ':last')
- pipe.rename(start_key, destination + ':pstart')
- # 更新当前所处的小时数。
- pipe.set(start_key, hour_start)
+ existing = pipe.get(start_key)
+ # 创建一个事务。
+ pipe.multi()
+ # 如果目前的常见日志列表是上一个小时的……
+ if existing and existing < hour_start:
+ # ……那么将旧的常见日志信息进行归档。
+ pipe.rename(destination, destination + ':last')
+ pipe.rename(start_key, destination + ':pstart')
+ # 更新当前所处的小时数。
+ pipe.set(start_key, hour_start)
- # 对记录日志出现次数的计数器执行自增操作。
- pipe.zincrby(destination, message)
- # log_recent()函数负责记录日志并调用execute()函数。
- log_recent(pipe, name, message, severity, pipe)
- return
- except redis.exceptions.WatchError:
- # 如果程序因为其他客户端在执行归档操作而出现监视错误,那么重试。
- continue
+ # 对记录日志出现次数的计数器执行自增操作。
+ pipe.zincrby(destination, message)
+ # log_recent()函数负责记录日志并调用execute()函数。
+ log_recent(pipe, name, message, severity, pipe)
+ return
+ except redis.exceptions.WatchError:
+ # 如果程序因为其他客户端在执行归档操作而出现监视错误,那么重试。
+ continue
#
@@ -103,22 +103,22 @@ PRECISION = [1, 5, 60, 300, 3600, 18000, 86400] # A
def update_counter(conn, name, count=1, now=None):
- # 通过取得当前时间来判断应该对哪个时间片执行自增操作。
- now = now or time.time()
- # 为了保证之后的清理工作可以正确地执行,这里需要创建一个事务型流水线。
- pipe = conn.pipeline()
- # 为我们记录的每种精度都创建一个计数器。
- for prec in PRECISION:
- # 取得当前时间片的开始时间。
- pnow = int(now / prec) * prec
- # 创建负责存储计数信息的散列。
- hash = '%s:%s' % (prec, name)
- # 将计数器的引用信息添加到有序集合里面,
- # 并将其分值设置为0,以便在之后执行清理操作。
- pipe.zadd('known:', hash, 0)
- # 对给定名字和精度的计数器进行更新。
- pipe.hincrby('count:' + hash, pnow, count)
- pipe.execute()
+ # 通过取得当前时间来判断应该对哪个时间片执行自增操作。
+ now = now or time.time()
+ # 为了保证之后的清理工作可以正确地执行,这里需要创建一个事务型流水线。
+ pipe = conn.pipeline()
+ # 为我们记录的每种精度都创建一个计数器。
+ for prec in PRECISION:
+ # 取得当前时间片的开始时间。
+ pnow = int(now / prec) * prec
+ # 创建负责存储计数信息的散列。
+ hash = '%s:%s' % (prec, name)
+ # 将计数器的引用信息添加到有序集合里面,
+ # 并将其分值设置为0,以便在之后执行清理操作。
+ pipe.zadd('known:', hash, 0)
+ # 对给定名字和精度的计数器进行更新。
+ pipe.hincrby('count:' + hash, pnow, count)
+ pipe.execute()
#
@@ -127,161 +127,161 @@ def update_counter(conn, name, count=1, now=None):
# 代码清单 5-4
#
def get_counter(conn, name, precision):
- # 取得存储着计数器数据的键的名字。
- hash = '%s:%s' % (precision, name)
- # 从Redis里面取出计数器数据。
- data = conn.hgetall('count:' + hash)
- # 将计数器数据转换成指定的格式。
- to_return = []
- for key, value in data.iteritems():
- to_return.append((int(key), int(value)))
- # 对数据进行排序,把旧的数据样本排在前面。
- to_return.sort()
- return to_return
+ # 取得存储着计数器数据的键的名字。
+ hash = '%s:%s' % (precision, name)
+ # 从Redis里面取出计数器数据。
+ data = conn.hgetall('count:' + hash)
+ # 将计数器数据转换成指定的格式。
+ to_return = []
+ for key, value in data.iteritems():
+ to_return.append((int(key), int(value)))
+ # 对数据进行排序,把旧的数据样本排在前面。
+ to_return.sort()
+ return to_return
#
#
def clean_counters(conn):
- pipe = conn.pipeline(True)
- # 为了平等地处理更新频率各不相同的多个计数器,程序需要记录清理操作执行的次数。
- passes = 0
- # 持续地对计数器进行清理,直到退出为止。
- while not QUIT:
- # 记录清理操作开始执行的时间,用于计算清理操作执行的时长。
- start = time.time()
- # 渐进地遍历所有已知的计数器。
- index = 0
- while index < conn.zcard('known:'):
- # 取得被检查计数器的数据。
- hash = conn.zrange('known:', index, index)
- index += 1
- if not hash:
- break
- hash = hash[0]
- # 取得计数器的精度。
- prec = int(hash.partition(':')[0])
- # 因为清理程序每60秒钟就会循环一次,
- # 所以这里需要根据计数器的更新频率来判断是否真的有必要对计数器进行清理。
- bprec = int(prec // 60) or 1
- # 如果这个计数器在这次循环里不需要进行清理,
- # 那么检查下一个计数器。
- # (举个例子,如果清理程序只循环了三次,而计数器的更新频率为每5分钟一次,
- # 那么程序暂时还不需要对这个计数器进行清理。)
- if passes % bprec:
- continue
+ pipe = conn.pipeline(True)
+ # 为了平等地处理更新频率各不相同的多个计数器,程序需要记录清理操作执行的次数。
+ passes = 0
+ # 持续地对计数器进行清理,直到退出为止。
+ while not QUIT:
+ # 记录清理操作开始执行的时间,用于计算清理操作执行的时长。
+ start = time.time()
+ # 渐进地遍历所有已知的计数器。
+ index = 0
+ while index < conn.zcard('known:'):
+ # 取得被检查计数器的数据。
+ hash = conn.zrange('known:', index, index)
+ index += 1
+ if not hash:
+ break
+ hash = hash[0]
+ # 取得计数器的精度。
+ prec = int(hash.partition(':')[0])
+ # 因为清理程序每60秒钟就会循环一次,
+ # 所以这里需要根据计数器的更新频率来判断是否真的有必要对计数器进行清理。
+ bprec = int(prec // 60) or 1
+ # 如果这个计数器在这次循环里不需要进行清理,
+ # 那么检查下一个计数器。
+ # (举个例子,如果清理程序只循环了三次,而计数器的更新频率为每5分钟一次,
+ # 那么程序暂时还不需要对这个计数器进行清理。)
+ if passes % bprec:
+ continue
- hkey = 'count:' + hash
- # 根据给定的精度以及需要保留的样本数量,
- # 计算出我们需要保留什么时间之前的样本。
- cutoff = time.time() - SAMPLE_COUNT * prec
- # 获取样本的开始时间,并将其从字符串转换为整数。
- samples = map(int, conn.hkeys(hkey))
- # 计算出需要移除的样本数量。
- samples.sort()
- remove = bisect.bisect_right(samples, cutoff)
+ hkey = 'count:' + hash
+ # 根据给定的精度以及需要保留的样本数量,
+ # 计算出我们需要保留什么时间之前的样本。
+ cutoff = time.time() - SAMPLE_COUNT * prec
+ # 获取样本的开始时间,并将其从字符串转换为整数。
+ samples = map(int, conn.hkeys(hkey))
+ # 计算出需要移除的样本数量。
+ samples.sort()
+ remove = bisect.bisect_right(samples, cutoff)
- # 按需移除计数样本。
- if remove:
- conn.hdel(hkey, *samples[:remove])
- # 这个散列可能已经被清空。
- if remove == len(samples):
- try:
- # 在尝试修改计数器散列之前,对其进行监视。
- pipe.watch(hkey)
- # 验证计数器散列是否为空,如果是的话,
- # 那么从记录已知计数器的有序集合里面移除它。
- if not pipe.hlen(hkey):
- pipe.multi()
- pipe.zrem('known:', hash)
- pipe.execute()
- # 在删除了一个计数器的情况下,
- # 下次循环可以使用与本次循环相同的索引。
- index -= 1
- else:
- # 计数器散列并不为空,
- # 继续让它留在记录已有计数器的有序集合里面。
- pipe.unwatch()
- # 有其他程序向这个计算器散列添加了新的数据,
- # 它已经不再是空的了,继续让它留在记录已知计数器的有序集合里面。
- except redis.exceptions.WatchError:
- pass
+ # 按需移除计数样本。
+ if remove:
+ conn.hdel(hkey, *samples[:remove])
+ # 这个散列可能已经被清空。
+ if remove == len(samples):
+ try:
+ # 在尝试修改计数器散列之前,对其进行监视。
+ pipe.watch(hkey)
+ # 验证计数器散列是否为空,如果是的话,
+ # 那么从记录已知计数器的有序集合里面移除它。
+ if not pipe.hlen(hkey):
+ pipe.multi()
+ pipe.zrem('known:', hash)
+ pipe.execute()
+ # 在删除了一个计数器的情况下,
+ # 下次循环可以使用与本次循环相同的索引。
+ index -= 1
+ else:
+ # 计数器散列并不为空,
+ # 继续让它留在记录已有计数器的有序集合里面。
+ pipe.unwatch()
+ # 有其他程序向这个计算器散列添加了新的数据,
+ # 它已经不再是空的了,继续让它留在记录已知计数器的有序集合里面。
+ except redis.exceptions.WatchError:
+ pass
- # 为了让清理操作的执行频率与计数器更新的频率保持一致,
- # 对记录循环次数的变量以及记录执行时长的变量进行更新。
- passes += 1
- duration = min(int(time.time() - start) + 1, 60)
- # 如果这次循环未耗尽60秒钟,那么在余下的时间内进行休眠;
- # 如果60秒钟已经耗尽,那么休眠一秒钟以便稍作休息。
- time.sleep(max(60 - duration, 1))
- #
+ # 为了让清理操作的执行频率与计数器更新的频率保持一致,
+ # 对记录循环次数的变量以及记录执行时长的变量进行更新。
+ passes += 1
+ duration = min(int(time.time() - start) + 1, 60)
+ # 如果这次循环未耗尽60秒钟,那么在余下的时间内进行休眠;
+ # 如果60秒钟已经耗尽,那么休眠一秒钟以便稍作休息。
+ time.sleep(max(60 - duration, 1))
+ #
# 代码清单 5-6
#
def update_stats(conn, context, type, value, timeout=5):
- # 设置用于存储统计数据的键。
- destination = 'stats:%s:%s' % (context, type)
- # 像common_log()函数一样,
- # 处理当前这一个小时的数据和上一个小时的数据。
- start_key = destination + ':start'
- pipe = conn.pipeline(True)
- end = time.time() + timeout
- while time.time() < end:
- try:
- pipe.watch(start_key)
- now = datetime.utcnow().timetuple()
- hour_start = datetime(*now[:4]).isoformat()
+ # 设置用于存储统计数据的键。
+ destination = 'stats:%s:%s' % (context, type)
+ # 像common_log()函数一样,
+ # 处理当前这一个小时的数据和上一个小时的数据。
+ start_key = destination + ':start'
+ pipe = conn.pipeline(True)
+ end = time.time() + timeout
+ while time.time() < end:
+ try:
+ pipe.watch(start_key)
+ now = datetime.utcnow().timetuple()
+ hour_start = datetime(*now[:4]).isoformat()
- existing = pipe.get(start_key)
- pipe.multi()
- if existing and existing < hour_start:
- pipe.rename(destination, destination + ':last')
- pipe.rename(start_key, destination + ':pstart')
- pipe.set(start_key, hour_start)
+ existing = pipe.get(start_key)
+ pipe.multi()
+ if existing and existing < hour_start:
+ pipe.rename(destination, destination + ':last')
+ pipe.rename(start_key, destination + ':pstart')
+ pipe.set(start_key, hour_start)
- tkey1 = str(uuid.uuid4())
- tkey2 = str(uuid.uuid4())
- # 将值添加到临时键里面。
- pipe.zadd(tkey1, 'min', value)
- pipe.zadd(tkey2, 'max', value)
- # 使用合适聚合函数MIN和MAX,
- # 对存储统计数据的键和两个临时键进行并集计算。
- pipe.zunionstore(destination,
- [destination, tkey1], aggregate='min')
- pipe.zunionstore(destination,
- [destination, tkey2], aggregate='max')
+ tkey1 = str(uuid.uuid4())
+ tkey2 = str(uuid.uuid4())
+ # 将值添加到临时键里面。
+ pipe.zadd(tkey1, 'min', value)
+ pipe.zadd(tkey2, 'max', value)
+ # 使用合适聚合函数MIN和MAX,
+ # 对存储统计数据的键和两个临时键进行并集计算。
+ pipe.zunionstore(destination,
+ [destination, tkey1], aggregate='min')
+ pipe.zunionstore(destination,
+ [destination, tkey2], aggregate='max')
- # 删除临时键。
- pipe.delete(tkey1, tkey2)
- # 对有序集合中的样本数量、值的和、值的平方之和三个成员进行更新。
- pipe.zincrby(destination, 'count')
- pipe.zincrby(destination, 'sum', value)
- pipe.zincrby(destination, 'sumsq', value * value)
+ # 删除临时键。
+ pipe.delete(tkey1, tkey2)
+ # 对有序集合中的样本数量、值的和、值的平方之和三个成员进行更新。
+ pipe.zincrby(destination, 'count')
+ pipe.zincrby(destination, 'sum', value)
+ pipe.zincrby(destination, 'sumsq', value * value)
- # 返回基本的计数信息,以便函数调用者在有需要时做进一步的处理。
- return pipe.execute()[-3:]
- except redis.exceptions.WatchError:
- # 如果新的一个小时已经开始,并且旧的数据已经被归档,那么进行重试。
- continue
- #
+ # 返回基本的计数信息,以便函数调用者在有需要时做进一步的处理。
+ return pipe.execute()[-3:]
+ except redis.exceptions.WatchError:
+ # 如果新的一个小时已经开始,并且旧的数据已经被归档,那么进行重试。
+ continue
+ #
# 代码清单 5-7
#
def get_stats(conn, context, type):
- # 程序将从这个键里面取出统计数据。
- key = 'stats:%s:%s' % (context, type)
- # 获取基本的统计数据,并将它们都放到一个字典里面。
- data = dict(conn.zrange(key, 0, -1, withscores=True))
- # 计算平均值。
- data['average'] = data['sum'] / data['count']
- # 计算标准差的第一个步骤。
- numerator = data['sumsq'] - data['sum'] ** 2 / data['count']
- # 完成标准差的计算工作。
- data['stddev'] = (numerator / (data['count'] - 1 or 1)) ** .5
- return data
+ # 程序将从这个键里面取出统计数据。
+ key = 'stats:%s:%s' % (context, type)
+ # 获取基本的统计数据,并将它们都放到一个字典里面。
+ data = dict(conn.zrange(key, 0, -1, withscores=True))
+ # 计算平均值。
+ data['average'] = data['sum'] / data['count']
+ # 计算标准差的第一个步骤。
+ numerator = data['sumsq'] - data['sum'] ** 2 / data['count']
+ # 完成标准差的计算工作。
+ data['stddev'] = (numerator / (data['count'] - 1 or 1)) ** .5
+ return data
#
@@ -292,24 +292,24 @@ def get_stats(conn, context, type):
# 将这个Python生成器用作上下文管理器。
@contextlib.contextmanager
def access_time(conn, context):
- # 记录代码块执行前的时间。
- start = time.time()
- # 运行被包裹的代码块。
- yield
+ # 记录代码块执行前的时间。
+ start = time.time()
+ # 运行被包裹的代码块。
+ yield
- # 计算代码块的执行时长。
- delta = time.time() - start
- # 更新这一上下文的统计数据。
- stats = update_stats(conn, context, 'AccessTime', delta)
- # 计算页面的平均访问时长。
- average = stats[1] / stats[0]
+ # 计算代码块的执行时长。
+ delta = time.time() - start
+ # 更新这一上下文的统计数据。
+ stats = update_stats(conn, context, 'AccessTime', delta)
+ # 计算页面的平均访问时长。
+ average = stats[1] / stats[0]
- pipe = conn.pipeline(True)
- # 将页面的平均访问时长添加到记录最慢访问时间的有序集合里面。
- pipe.zadd('slowest:AccessTime', context, average)
- # AccessTime有序集合只会保留最慢的100条记录。
- pipe.zremrangebyrank('slowest:AccessTime', 0, -101)
- pipe.execute()
+ pipe = conn.pipeline(True)
+ # 将页面的平均访问时长添加到记录最慢访问时间的有序集合里面。
+ pipe.zadd('slowest:AccessTime', context, average)
+ # AccessTime有序集合只会保留最慢的100条记录。
+ pipe.zremrangebyrank('slowest:AccessTime', 0, -101)
+ pipe.execute()
#
@@ -318,20 +318,20 @@ def access_time(conn, context):
#
# 这个视图(view)接受一个Redis连接以及一个生成内容的回调函数为参数。
def process_view(conn, callback):
- # 计算并记录访问时长的上下文管理器就是这样包围代码块的。
- with access_time(conn, request.path):
- # 当上下文管理器中的yield语句被执行时,这个语句就会被执行。
- return callback()
- #
+ # 计算并记录访问时长的上下文管理器就是这样包围代码块的。
+ with access_time(conn, request.path):
+ # 当上下文管理器中的yield语句被执行时,这个语句就会被执行。
+ return callback()
+ #
# 代码清单 5-9
#
def ip_to_score(ip_address):
- score = 0
- for v in ip_address.split('.'):
- score = score * 256 + int(v, 10)
- return score
+ score = 0
+ for v in ip_address.split('.'):
+ score = score * 256 + int(v, 10)
+ return score
#
@@ -341,64 +341,64 @@ def ip_to_score(ip_address):
#
# 这个函数在执行时需要给定GeoLiteCity-Blocks.csv文件所在的位置。
def import_ips_to_redis(conn, filename):
- csv_file = csv.reader(open(filename, 'rb'))
- for count, row in enumerate(csv_file):
- # 按需将IP地址转换为分值。
- start_ip = row[0] if row else ''
- if 'i' in start_ip.lower():
- continue
- if '.' in start_ip:
- start_ip = ip_to_score(start_ip)
- elif start_ip.isdigit():
- start_ip = int(start_ip, 10)
- else:
- # 略过文件的第一行以及格式不正确的条目。
- continue
+ csv_file = csv.reader(open(filename, 'rb'))
+ for count, row in enumerate(csv_file):
+ # 按需将IP地址转换为分值。
+ start_ip = row[0] if row else ''
+ if 'i' in start_ip.lower():
+ continue
+ if '.' in start_ip:
+ start_ip = ip_to_score(start_ip)
+ elif start_ip.isdigit():
+ start_ip = int(start_ip, 10)
+ else:
+ # 略过文件的第一行以及格式不正确的条目。
+ continue
- # 构建唯一城市ID。
- city_id = row[2] + '_' + str(count)
- # 将城市ID及其对应的IP地址分值添加到有序集合里面。
- conn.zadd('ip2cityid:', city_id, start_ip)
- #
+ # 构建唯一城市ID。
+ city_id = row[2] + '_' + str(count)
+ # 将城市ID及其对应的IP地址分值添加到有序集合里面。
+ conn.zadd('ip2cityid:', city_id, start_ip)
+ #
# 代码清单 5-11
#
# 这个函数在执行时需要给定GeoLiteCity-Location.csv文件所在的位置。
def import_cities_to_redis(conn, filename):
- for row in csv.reader(open(filename, 'rb')):
- if len(row) < 4 or not row[0].isdigit():
- continue
- row = [i.decode('latin-1') for i in row]
- # 准备好需要添加到散列里面的信息。
- city_id = row[0]
- country = row[1]
- region = row[2]
- city = row[3]
- # 将城市信息添加到Redis里面。
- conn.hset('cityid2city:', city_id,
- json.dumps([city, region, country]))
- #
+ for row in csv.reader(open(filename, 'rb')):
+ if len(row) < 4 or not row[0].isdigit():
+ continue
+ row = [i.decode('latin-1') for i in row]
+ # 准备好需要添加到散列里面的信息。
+ city_id = row[0]
+ country = row[1]
+ region = row[2]
+ city = row[3]
+ # 将城市信息添加到Redis里面。
+ conn.hset('cityid2city:', city_id,
+ json.dumps([city, region, country]))
+ #
# 代码清单 5-12
#
def find_city_by_ip(conn, ip_address):
- # 将IP地址转换为分值以便执行ZREVRANGEBYSCORE命令。
- if isinstance(ip_address, str): # A
- ip_address = ip_to_score(ip_address) # A
+ # 将IP地址转换为分值以便执行ZREVRANGEBYSCORE命令。
+ if isinstance(ip_address, str): # A
+ ip_address = ip_to_score(ip_address) # A
- # 查找唯一城市ID。
- city_id = conn.zrevrangebyscore( # B
- 'ip2cityid:', ip_address, 0, start=0, num=1) # B
+ # 查找唯一城市ID。
+ city_id = conn.zrevrangebyscore( # B
+ 'ip2cityid:', ip_address, 0, start=0, num=1) # B
- if not city_id:
- return None
+ if not city_id:
+ return None
- # 将唯一城市ID转换为普通城市ID。
- city_id = city_id[0].partition('_')[0] # C
- # 从散列里面取出城市信息。
- return json.loads(conn.hget('cityid2city:', city_id)) # D
+ # 将唯一城市ID转换为普通城市ID。
+ city_id = city_id[0].partition('_')[0] # C
+ # 从散列里面取出城市信息。
+ return json.loads(conn.hget('cityid2city:', city_id)) # D
#
@@ -411,19 +411,19 @@ IS_UNDER_MAINTENANCE = False
def is_under_maintenance(conn):
- # 将两个变量设置为全局变量以便在之后对它们进行写入。
- global LAST_CHECKED, IS_UNDER_MAINTENANCE # A
+ # 将两个变量设置为全局变量以便在之后对它们进行写入。
+ global LAST_CHECKED, IS_UNDER_MAINTENANCE # A
- # 距离上次检查是否已经超过1秒钟?
- if LAST_CHECKED < time.time() - 1: # B
- # 更新最后检查时间。
- LAST_CHECKED = time.time() # C
- # 检查系统是否正在进行维护。
- IS_UNDER_MAINTENANCE = bool( # D
- conn.get('is-under-maintenance')) # D
+ # 距离上次检查是否已经超过1秒钟?
+ if LAST_CHECKED < time.time() - 1: # B
+ # 更新最后检查时间。
+ LAST_CHECKED = time.time() # C
+ # 检查系统是否正在进行维护。
+ IS_UNDER_MAINTENANCE = bool( # D
+ conn.get('is-under-maintenance')) # D
- # 返回一个布尔值,用于表示系统是否正在进行维护。
- return IS_UNDER_MAINTENANCE # E
+ # 返回一个布尔值,用于表示系统是否正在进行维护。
+ return IS_UNDER_MAINTENANCE # E
#
@@ -432,9 +432,9 @@ def is_under_maintenance(conn):
# 代码清单 5-14
#
def set_config(conn, type, component, config):
- conn.set(
- 'config:%s:%s' % (type, component),
- json.dumps(config))
+ conn.set(
+ 'config:%s:%s' % (type, component),
+ json.dumps(config))
#
@@ -448,25 +448,25 @@ CHECKED = {}
def get_config(conn, type, component, wait=1):
- key = 'config:%s:%s' % (type, component)
+ key = 'config:%s:%s' % (type, component)
- # 检查是否需要对这个组件的配置信息进行更新。
- if CHECKED.get(key) < time.time() - wait:
- # 有需要对配置进行更新,记录最后一次检查这个连接的时间。
- CHECKED[key] = time.time()
- # 取得Redis存储的组件配置。
- config = json.loads(conn.get(key) or '{}')
- # 将潜在的Unicode关键字参数转换为字符串关键字参数。
- config = dict((str(k), config[k]) for k in config)
- # 取得组件正在使用的配置。
- old_config = CONFIGS.get(key)
+ # 检查是否需要对这个组件的配置信息进行更新。
+ if CHECKED.get(key) < time.time() - wait:
+ # 有需要对配置进行更新,记录最后一次检查这个连接的时间。
+ CHECKED[key] = time.time()
+ # 取得Redis存储的组件配置。
+ config = json.loads(conn.get(key) or '{}')
+ # 将潜在的Unicode关键字参数转换为字符串关键字参数。
+ config = dict((str(k), config[k]) for k in config)
+ # 取得组件正在使用的配置。
+ old_config = CONFIGS.get(key)
- # 如果两个配置并不相同……
- if config != old_config:
- # ……那么对组件的配置进行更新。
- CONFIGS[key] = config
+ # 如果两个配置并不相同……
+ if config != old_config:
+ # ……那么对组件的配置进行更新。
+ CONFIGS[key] = config
- return CONFIGS.get(key)
+ return CONFIGS.get(key)
#
@@ -479,39 +479,39 @@ REDIS_CONNECTIONS = {}
# 将应用组件的名字传递给装饰器。
def redis_connection(component, wait=1): # A
- # 因为函数每次被调用都需要获取这个配置键,所以我们干脆把它缓存起来。
- key = 'config:redis:' + component # B
+ # 因为函数每次被调用都需要获取这个配置键,所以我们干脆把它缓存起来。
+ key = 'config:redis:' + component # B
- # 包装器接受一个函数作为参数,并使用另一个函数来包裹这个函数。
- def wrapper(function): # C
- # 将被包裹函数里的一些有用的元数据复制到配置处理器。
- @functools.wraps(function) # D
- # 创建负责管理连接信息的函数。
- def call(*args, **kwargs): # E
- # 如果有旧配置存在,那么获取它。
- old_config = CONFIGS.get(key, object()) # F
- # 如果有新配置存在,那么获取它。
- _config = get_config( # G
- config_connection, 'redis', component, wait) # G
+ # 包装器接受一个函数作为参数,并使用另一个函数来包裹这个函数。
+ def wrapper(function): # C
+ # 将被包裹函数里的一些有用的元数据复制到配置处理器。
+ @functools.wraps(function) # D
+ # 创建负责管理连接信息的函数。
+ def call(*args, **kwargs): # E
+ # 如果有旧配置存在,那么获取它。
+ old_config = CONFIGS.get(key, object()) # F
+ # 如果有新配置存在,那么获取它。
+ _config = get_config( # G
+ config_connection, 'redis', component, wait) # G
- config = {}
- # 对配置进行处理并将其用于创建Redis连接。
- for k, v in _config.iteritems(): # L
- config[k.encode('utf-8')] = v # L
+ config = {}
+ # 对配置进行处理并将其用于创建Redis连接。
+ for k, v in _config.iteritems(): # L
+ config[k.encode('utf-8')] = v # L
- # 如果新旧配置并不相同,那么创建新的连接。
- if config != old_config: # H
- REDIS_CONNECTIONS[key] = redis.Redis(**config) # H
+ # 如果新旧配置并不相同,那么创建新的连接。
+ if config != old_config: # H
+ REDIS_CONNECTIONS[key] = redis.Redis(**config) # H
- # 将Redis连接以及其他匹配的参数传递给被包裹函数,然后调用函数并返回执行结果。
- return function( # I
- REDIS_CONNECTIONS.get(key), *args, **kwargs) # I
+ # 将Redis连接以及其他匹配的参数传递给被包裹函数,然后调用函数并返回执行结果。
+ return function( # I
+ REDIS_CONNECTIONS.get(key), *args, **kwargs) # I
- # 返回被包裹的函数。
- return call # J
+ # 返回被包裹的函数。
+ return call # J
- # 返回用于包裹Redis函数的包装器。
- return wrapper # K
+ # 返回用于包裹Redis函数的包装器。
+ return wrapper # K
#
@@ -532,224 +532,224 @@ log_recent('main', 'User 235 logged in') # 我们再也不必在调用log_rec
# --------------- 以下是用于测试代码的辅助函数 --------------------------------
class request:
- pass
+ pass
# a faster version with pipelines for actual testing
def import_ips_to_redis(conn, filename):
- csv_file = csv.reader(open(filename, 'rb'))
- pipe = conn.pipeline(False)
- for count, row in enumerate(csv_file):
- start_ip = row[0] if row else ''
- if 'i' in start_ip.lower():
- continue
- if '.' in start_ip:
- start_ip = ip_to_score(start_ip)
- elif start_ip.isdigit():
- start_ip = int(start_ip, 10)
- else:
- continue
+ csv_file = csv.reader(open(filename, 'rb'))
+ pipe = conn.pipeline(False)
+ for count, row in enumerate(csv_file):
+ start_ip = row[0] if row else ''
+ if 'i' in start_ip.lower():
+ continue
+ if '.' in start_ip:
+ start_ip = ip_to_score(start_ip)
+ elif start_ip.isdigit():
+ start_ip = int(start_ip, 10)
+ else:
+ continue
- city_id = row[2] + '_' + str(count)
- pipe.zadd('ip2cityid:', city_id, start_ip)
- if not (count + 1) % 1000:
- pipe.execute()
- pipe.execute()
+ city_id = row[2] + '_' + str(count)
+ pipe.zadd('ip2cityid:', city_id, start_ip)
+ if not (count + 1) % 1000:
+ pipe.execute()
+ pipe.execute()
def import_cities_to_redis(conn, filename):
- pipe = conn.pipeline(False)
- for count, row in enumerate(csv.reader(open(filename, 'rb'))):
- if len(row) < 4 or not row[0].isdigit():
- continue
- row = [i.decode('latin-1') for i in row]
- city_id = row[0]
- country = row[1]
- region = row[2]
- city = row[3]
- pipe.hset('cityid2city:', city_id,
- json.dumps([city, region, country]))
- if not (count + 1) % 1000:
- pipe.execute()
- pipe.execute()
+ pipe = conn.pipeline(False)
+ for count, row in enumerate(csv.reader(open(filename, 'rb'))):
+ if len(row) < 4 or not row[0].isdigit():
+ continue
+ row = [i.decode('latin-1') for i in row]
+ city_id = row[0]
+ country = row[1]
+ region = row[2]
+ city = row[3]
+ pipe.hset('cityid2city:', city_id,
+ json.dumps([city, region, country]))
+ if not (count + 1) % 1000:
+ pipe.execute()
+ pipe.execute()
class TestCh05(unittest.TestCase):
- def setUp(self):
- global config_connection
- import redis
- self.conn = config_connection = redis.Redis(db=15)
- self.conn.flushdb()
+ def setUp(self):
+ global config_connection
+ import redis
+ self.conn = config_connection = redis.Redis(db=15)
+ self.conn.flushdb()
- def tearDown(self):
- self.conn.flushdb()
- del self.conn
- global config_connection, QUIT, SAMPLE_COUNT
- config_connection = None
- QUIT = False
- SAMPLE_COUNT = 100
- print
- print
+ def tearDown(self):
+ self.conn.flushdb()
+ del self.conn
+ global config_connection, QUIT, SAMPLE_COUNT
+ config_connection = None
+ QUIT = False
+ SAMPLE_COUNT = 100
+ print
+ print
- def test_log_recent(self):
- import pprint
- conn = self.conn
+ def test_log_recent(self):
+ import pprint
+ conn = self.conn
- print "Let's write a few logs to the recent log"
- for msg in xrange(5):
- log_recent(conn, 'test', 'this is message %s' % msg)
- recent = conn.lrange('recent:test:info', 0, -1)
- print "The current recent message log has this many messages:", len(recent)
- print "Those messages include:"
- pprint.pprint(recent[:10])
- self.assertTrue(len(recent) >= 5)
+ print "Let's write a few logs to the recent log"
+ for msg in xrange(5):
+ log_recent(conn, 'test', 'this is message %s' % msg)
+ recent = conn.lrange('recent:test:info', 0, -1)
+ print "The current recent message log has this many messages:", len(recent)
+ print "Those messages include:"
+ pprint.pprint(recent[:10])
+ self.assertTrue(len(recent) >= 5)
- def test_log_common(self):
- import pprint
- conn = self.conn
+ def test_log_common(self):
+ import pprint
+ conn = self.conn
- print "Let's write some items to the common log"
- for count in xrange(1, 6):
- for i in xrange(count):
- log_common(conn, 'test', "message-%s" % count)
- common = conn.zrevrange('common:test:info', 0, -1, withscores=True)
- print "The current number of common messages is:", len(common)
- print "Those common messages are:"
- pprint.pprint(common)
- self.assertTrue(len(common) >= 5)
+ print "Let's write some items to the common log"
+ for count in xrange(1, 6):
+ for i in xrange(count):
+ log_common(conn, 'test', "message-%s" % count)
+ common = conn.zrevrange('common:test:info', 0, -1, withscores=True)
+ print "The current number of common messages is:", len(common)
+ print "Those common messages are:"
+ pprint.pprint(common)
+ self.assertTrue(len(common) >= 5)
- def test_counters(self):
- import pprint
- global QUIT, SAMPLE_COUNT
- conn = self.conn
+ def test_counters(self):
+ import pprint
+ global QUIT, SAMPLE_COUNT
+ conn = self.conn
- print "Let's update some counters for now and a little in the future"
- now = time.time()
- for delta in xrange(10):
- update_counter(conn, 'test', count=random.randrange(1, 5), now=now + delta)
- counter = get_counter(conn, 'test', 1)
- print "We have some per-second counters:", len(counter)
- self.assertTrue(len(counter) >= 10)
- counter = get_counter(conn, 'test', 5)
- print "We have some per-5-second counters:", len(counter)
- print "These counters include:"
- pprint.pprint(counter[:10])
- self.assertTrue(len(counter) >= 2)
- print
+ print "Let's update some counters for now and a little in the future"
+ now = time.time()
+ for delta in xrange(10):
+ update_counter(conn, 'test', count=random.randrange(1, 5), now=now + delta)
+ counter = get_counter(conn, 'test', 1)
+ print "We have some per-second counters:", len(counter)
+ self.assertTrue(len(counter) >= 10)
+ counter = get_counter(conn, 'test', 5)
+ print "We have some per-5-second counters:", len(counter)
+ print "These counters include:"
+ pprint.pprint(counter[:10])
+ self.assertTrue(len(counter) >= 2)
+ print
- tt = time.time
+ tt = time.time
- def new_tt():
- return tt() + 2 * 86400
+ def new_tt():
+ return tt() + 2 * 86400
- time.time = new_tt
+ time.time = new_tt
- print "Let's clean out some counters by setting our sample count to 0"
- SAMPLE_COUNT = 0
- t = threading.Thread(target=clean_counters, args=(conn,))
- t.setDaemon(1) # to make sure it dies if we ctrl+C quit
- t.start()
- time.sleep(1)
- QUIT = True
- time.time = tt
- counter = get_counter(conn, 'test', 86400)
- print "Did we clean out all of the counters?", not counter
- self.assertFalse(counter)
+ print "Let's clean out some counters by setting our sample count to 0"
+ SAMPLE_COUNT = 0
+ t = threading.Thread(target=clean_counters, args=(conn,))
+ t.setDaemon(1) # to make sure it dies if we ctrl+C quit
+ t.start()
+ time.sleep(1)
+ QUIT = True
+ time.time = tt
+ counter = get_counter(conn, 'test', 86400)
+ print "Did we clean out all of the counters?", not counter
+ self.assertFalse(counter)
- def test_stats(self):
- import pprint
- conn = self.conn
+ def test_stats(self):
+ import pprint
+ conn = self.conn
- print "Let's add some data for our statistics!"
- for i in xrange(5):
- r = update_stats(conn, 'temp', 'example', random.randrange(5, 15))
- print "We have some aggregate statistics:", r
- rr = get_stats(conn, 'temp', 'example')
- print "Which we can also fetch manually:"
- pprint.pprint(rr)
- self.assertTrue(rr['count'] >= 5)
+ print "Let's add some data for our statistics!"
+ for i in xrange(5):
+ r = update_stats(conn, 'temp', 'example', random.randrange(5, 15))
+ print "We have some aggregate statistics:", r
+ rr = get_stats(conn, 'temp', 'example')
+ print "Which we can also fetch manually:"
+ pprint.pprint(rr)
+ self.assertTrue(rr['count'] >= 5)
- def test_access_time(self):
- import pprint
- conn = self.conn
+ def test_access_time(self):
+ import pprint
+ conn = self.conn
- print "Let's calculate some access times..."
- for i in xrange(10):
- with access_time(conn, "req-%s" % i):
- time.sleep(.5 + random.random())
- print "The slowest access times are:"
- atimes = conn.zrevrange('slowest:AccessTime', 0, -1, withscores=True)
- pprint.pprint(atimes[:10])
- self.assertTrue(len(atimes) >= 10)
- print
+ print "Let's calculate some access times..."
+ for i in xrange(10):
+ with access_time(conn, "req-%s" % i):
+ time.sleep(.5 + random.random())
+ print "The slowest access times are:"
+ atimes = conn.zrevrange('slowest:AccessTime', 0, -1, withscores=True)
+ pprint.pprint(atimes[:10])
+ self.assertTrue(len(atimes) >= 10)
+ print
- def cb():
- time.sleep(1 + random.random())
+ def cb():
+ time.sleep(1 + random.random())
- print "Let's use the callback version..."
- for i in xrange(5):
- request.path = 'cbreq-%s' % i
- process_view(conn, cb)
- print "The slowest access times are:"
- atimes = conn.zrevrange('slowest:AccessTime', 0, -1, withscores=True)
- pprint.pprint(atimes[:10])
- self.assertTrue(len(atimes) >= 10)
+ print "Let's use the callback version..."
+ for i in xrange(5):
+ request.path = 'cbreq-%s' % i
+ process_view(conn, cb)
+ print "The slowest access times are:"
+ atimes = conn.zrevrange('slowest:AccessTime', 0, -1, withscores=True)
+ pprint.pprint(atimes[:10])
+ self.assertTrue(len(atimes) >= 10)
- def test_ip_lookup(self):
- conn = self.conn
+ def test_ip_lookup(self):
+ conn = self.conn
- try:
- open('GeoLiteCity-Blocks.csv', 'rb')
- open('GeoLiteCity-Location.csv', 'rb')
- except:
- print "********"
- print "You do not have the GeoLiteCity database available, aborting test"
- print "Please have the following two files in the current path:"
- print "GeoLiteCity-Blocks.csv"
- print "GeoLiteCity-Location.csv"
- print "********"
- return
+ try:
+ open('GeoLiteCity-Blocks.csv', 'rb')
+ open('GeoLiteCity-Location.csv', 'rb')
+ except:
+ print "********"
+ print "You do not have the GeoLiteCity database available, aborting test"
+ print "Please have the following two files in the current path:"
+ print "GeoLiteCity-Blocks.csv"
+ print "GeoLiteCity-Location.csv"
+ print "********"
+ return
- print "Importing IP addresses to Redis... (this may take a while)"
- import_ips_to_redis(conn, 'GeoLiteCity-Blocks.csv')
- ranges = conn.zcard('ip2cityid:')
- print "Loaded ranges into Redis:", ranges
- self.assertTrue(ranges > 1000)
- print
+ print "Importing IP addresses to Redis... (this may take a while)"
+ import_ips_to_redis(conn, 'GeoLiteCity-Blocks.csv')
+ ranges = conn.zcard('ip2cityid:')
+ print "Loaded ranges into Redis:", ranges
+ self.assertTrue(ranges > 1000)
+ print
- print "Importing Location lookups to Redis... (this may take a while)"
- import_cities_to_redis(conn, 'GeoLiteCity-Location.csv')
- cities = conn.hlen('cityid2city:')
- print "Loaded city lookups into Redis:", cities
- self.assertTrue(cities > 1000)
- print
+ print "Importing Location lookups to Redis... (this may take a while)"
+ import_cities_to_redis(conn, 'GeoLiteCity-Location.csv')
+ cities = conn.hlen('cityid2city:')
+ print "Loaded city lookups into Redis:", cities
+ self.assertTrue(cities > 1000)
+ print
- print "Let's lookup some locations!"
- rr = random.randrange
- for i in xrange(5):
- print find_city_by_ip(conn, '%s.%s.%s.%s' % (rr(1, 255), rr(256), rr(256), rr(256)))
+ print "Let's lookup some locations!"
+ rr = random.randrange
+ for i in xrange(5):
+ print find_city_by_ip(conn, '%s.%s.%s.%s' % (rr(1, 255), rr(256), rr(256), rr(256)))
- def test_is_under_maintenance(self):
- print "Are we under maintenance (we shouldn't be)?", is_under_maintenance(self.conn)
- self.conn.set('is-under-maintenance', 'yes')
- print "We cached this, so it should be the same:", is_under_maintenance(self.conn)
- time.sleep(1)
- print "But after a sleep, it should change:", is_under_maintenance(self.conn)
- print "Cleaning up..."
- self.conn.delete('is-under-maintenance')
- time.sleep(1)
- print "Should be False again:", is_under_maintenance(self.conn)
+ def test_is_under_maintenance(self):
+ print "Are we under maintenance (we shouldn't be)?", is_under_maintenance(self.conn)
+ self.conn.set('is-under-maintenance', 'yes')
+ print "We cached this, so it should be the same:", is_under_maintenance(self.conn)
+ time.sleep(1)
+ print "But after a sleep, it should change:", is_under_maintenance(self.conn)
+ print "Cleaning up..."
+ self.conn.delete('is-under-maintenance')
+ time.sleep(1)
+ print "Should be False again:", is_under_maintenance(self.conn)
- def test_config(self):
- print "Let's set a config and then get a connection from that config..."
- set_config(self.conn, 'redis', 'test', {'db': 15})
+ def test_config(self):
+ print "Let's set a config and then get a connection from that config..."
+ set_config(self.conn, 'redis', 'test', {'db': 15})
- @redis_connection('test')
- def test(conn2):
- return bool(conn2.info())
+ @redis_connection('test')
+ def test(conn2):
+ return bool(conn2.info())
- print "We can run commands from the configured connection:", test()
+ print "We can run commands from the configured connection:", test()
if __name__ == '__main__':
- unittest.main()
+ unittest.main()
diff --git a/codes/redis/redis-in-action-py/ch06_listing_source.py b/codes/redis/redis-in-action-py/ch06_listing_source.py
index 800cf62..a5ab572 100644
--- a/codes/redis/redis-in-action-py/ch06_listing_source.py
+++ b/codes/redis/redis-in-action-py/ch06_listing_source.py
@@ -18,17 +18,17 @@ pipe = inv = item = buyer = seller = inventory = None
# 代码清单 6-1
#
def add_update_contact(conn, user, contact):
- ac_list = 'recent:' + user
- # 准备执行原子操作。
- pipeline = conn.pipeline(True)
- # 如果联系人已经存在,那么移除他。
- pipeline.lrem(ac_list, contact)
- # 将联系人推入到列表的最前端。
- pipeline.lpush(ac_list, contact)
- # 只保留列表里面的前100个联系人。
- pipeline.ltrim(ac_list, 0, 99)
- # 实际地执行以上操作。
- pipeline.execute()
+ ac_list = 'recent:' + user
+ # 准备执行原子操作。
+ pipeline = conn.pipeline(True)
+ # 如果联系人已经存在,那么移除他。
+ pipeline.lrem(ac_list, contact)
+ # 将联系人推入到列表的最前端。
+ pipeline.lpush(ac_list, contact)
+ # 只保留列表里面的前100个联系人。
+ pipeline.ltrim(ac_list, 0, 99)
+ # 实际地执行以上操作。
+ pipeline.execute()
#
@@ -36,7 +36,7 @@ def add_update_contact(conn, user, contact):
#
def remove_contact(conn, user, contact):
- conn.lrem('recent:' + user, contact)
+ conn.lrem('recent:' + user, contact)
#
@@ -45,16 +45,16 @@ def remove_contact(conn, user, contact):
# 代码清单 6-2
#
def fetch_autocomplete_list(conn, user, prefix):
- # 获取自动补完列表。
- candidates = conn.lrange('recent:' + user, 0, -1)
- matches = []
- # 检查每个候选联系人。
- for candidate in candidates:
- if candidate.lower().startswith(prefix):
- # 发现一个匹配的联系人。
- matches.append(candidate)
- # 返回所有匹配的联系人。
- return matches
+ # 获取自动补完列表。
+ candidates = conn.lrange('recent:' + user, 0, -1)
+ matches = []
+ # 检查每个候选联系人。
+ for candidate in candidates:
+ if candidate.lower().startswith(prefix):
+ # 发现一个匹配的联系人。
+ matches.append(candidate)
+ # 返回所有匹配的联系人。
+ return matches
#
@@ -67,12 +67,12 @@ valid_characters = '`abcdefghijklmnopqrstuvwxyz{'
def find_prefix_range(prefix):
- # 在字符列表中查找前缀字符所处的位置。
- posn = bisect.bisect_left(valid_characters, prefix[-1:])
- # 找到前驱字符。
- suffix = valid_characters[(posn or 1) - 1]
- # 返回范围。
- return prefix[:-1] + suffix + '{', prefix + '{'
+ # 在字符列表中查找前缀字符所处的位置。
+ posn = bisect.bisect_left(valid_characters, prefix[-1:])
+ # 找到前驱字符。
+ suffix = valid_characters[(posn or 1) - 1]
+ # 返回范围。
+ return prefix[:-1] + suffix + '{', prefix + '{'
#
@@ -81,36 +81,36 @@ def find_prefix_range(prefix):
# 代码清单 6-4
#
def autocomplete_on_prefix(conn, guild, prefix):
- # 根据给定的前缀计算出查找范围的起点和终点。
- start, end = find_prefix_range(prefix)
- identifier = str(uuid.uuid4())
- start += identifier
- end += identifier
- zset_name = 'members:' + guild
+ # 根据给定的前缀计算出查找范围的起点和终点。
+ start, end = find_prefix_range(prefix)
+ identifier = str(uuid.uuid4())
+ start += identifier
+ end += identifier
+ zset_name = 'members:' + guild
- # 将范围的起始元素和结束元素添加到有序集合里面。
- conn.zadd(zset_name, start, 0, end, 0)
- pipeline = conn.pipeline(True)
- while 1:
- try:
- pipeline.watch(zset_name)
- # 找到两个被插入元素在有序集合中的排名。
- sindex = pipeline.zrank(zset_name, start)
- eindex = pipeline.zrank(zset_name, end)
- erange = min(sindex + 9, eindex - 2)
- pipeline.multi()
- # 获取范围内的值,然后删除之前插入的起始元素和结束元素。
- pipeline.zrem(zset_name, start, end)
- pipeline.zrange(zset_name, sindex, erange)
- items = pipeline.execute()[-1]
- break
- # 如果自动补完有序集合已经被其他客户端修改过了,那么进行重试。
- except redis.exceptions.WatchError:
- continue
+ # 将范围的起始元素和结束元素添加到有序集合里面。
+ conn.zadd(zset_name, start, 0, end, 0)
+ pipeline = conn.pipeline(True)
+ while 1:
+ try:
+ pipeline.watch(zset_name)
+ # 找到两个被插入元素在有序集合中的排名。
+ sindex = pipeline.zrank(zset_name, start)
+ eindex = pipeline.zrank(zset_name, end)
+ erange = min(sindex + 9, eindex - 2)
+ pipeline.multi()
+ # 获取范围内的值,然后删除之前插入的起始元素和结束元素。
+ pipeline.zrem(zset_name, start, end)
+ pipeline.zrange(zset_name, sindex, erange)
+ items = pipeline.execute()[-1]
+ break
+ # 如果自动补完有序集合已经被其他客户端修改过了,那么进行重试。
+ except redis.exceptions.WatchError:
+ continue
- # 如果有其他自动补完操作正在执行,
- # 那么从获取到的元素里面移除起始元素和终结元素。
- return [item for item in items if '{' not in item]
+ # 如果有其他自动补完操作正在执行,
+ # 那么从获取到的元素里面移除起始元素和终结元素。
+ return [item for item in items if '{' not in item]
#
@@ -119,11 +119,11 @@ def autocomplete_on_prefix(conn, guild, prefix):
# 代码清单 6-5
#
def join_guild(conn, guild, user):
- conn.zadd('members:' + guild, user, 0)
+ conn.zadd('members:' + guild, user, 0)
def leave_guild(conn, guild, user):
- conn.zrem('members:' + guild, user)
+ conn.zrem('members:' + guild, user)
#
@@ -133,20 +133,20 @@ def leave_guild(conn, guild, user):
# 代码清单 6-6
#
def list_item(conn, itemid, sellerid, price):
- # ...
- # 监视卖家包裹发生的变动。
- pipe.watch(inv)
- # 确保被出售的物品仍然存在于卖家的包裹里面。
- if not pipe.sismember(inv, itemid):
- pipe.unwatch()
- return None
+ # ...
+ # 监视卖家包裹发生的变动。
+ pipe.watch(inv)
+ # 确保被出售的物品仍然存在于卖家的包裹里面。
+ if not pipe.sismember(inv, itemid):
+ pipe.unwatch()
+ return None
- # 将物品添加到市场里面。
- pipe.multi()
- pipe.zadd("market:", item, price)
- pipe.srem(inv, itemid)
- pipe.execute()
- return True
+ # 将物品添加到市场里面。
+ pipe.multi()
+ pipe.zadd("market:", item, price)
+ pipe.srem(inv, itemid)
+ pipe.execute()
+ return True
# ...
@@ -156,26 +156,26 @@ def list_item(conn, itemid, sellerid, price):
# 代码清单 6-7
#
def purchase_item(conn, buyerid, itemid, sellerid, lprice):
- # ...
- # 监视市场以及买家个人信息发生的变化。
- pipe.watch("market:", buyer)
+ # ...
+ # 监视市场以及买家个人信息发生的变化。
+ pipe.watch("market:", buyer)
- # 检查物品是否已经售出、物品的价格是否已经发生了变化,
- # 以及买家是否有足够的金钱来购买这件物品。
- price = pipe.zscore("market:", item)
- funds = int(pipe.hget(buyer, 'funds'))
- if price != lprice or price > funds:
- pipe.unwatch()
- return None
+ # 检查物品是否已经售出、物品的价格是否已经发生了变化,
+ # 以及买家是否有足够的金钱来购买这件物品。
+ price = pipe.zscore("market:", item)
+ funds = int(pipe.hget(buyer, 'funds'))
+ if price != lprice or price > funds:
+ pipe.unwatch()
+ return None
- # 将买家支付的货款转移给卖家,并将被卖出的物品转移给买家。
- pipe.multi()
- pipe.hincrby(seller, 'funds', int(price))
- pipe.hincrby(buyerid, 'funds', int(-price))
- pipe.sadd(inventory, itemid)
- pipe.zrem("market:", item)
- pipe.execute()
- return True
+ # 将买家支付的货款转移给卖家,并将被卖出的物品转移给买家。
+ pipe.multi()
+ pipe.hincrby(seller, 'funds', int(price))
+ pipe.hincrby(buyerid, 'funds', int(-price))
+ pipe.sadd(inventory, itemid)
+ pipe.zrem("market:", item)
+ pipe.execute()
+ return True
# ...
@@ -185,18 +185,18 @@ def purchase_item(conn, buyerid, itemid, sellerid, lprice):
# 代码清单 6-8
#
def acquire_lock(conn, lockname, acquire_timeout=10):
- # 128位随机标识符。
- identifier = str(uuid.uuid4())
+ # 128位随机标识符。
+ identifier = str(uuid.uuid4())
- end = time.time() + acquire_timeout
- while time.time() < end:
- # 尝试取得锁。
- if conn.setnx('lock:' + lockname, identifier):
- return identifier
+ end = time.time() + acquire_timeout
+ while time.time() < end:
+ # 尝试取得锁。
+ if conn.setnx('lock:' + lockname, identifier):
+ return identifier
- time.sleep(.001)
+ time.sleep(.001)
- return False
+ return False
#
@@ -205,64 +205,64 @@ def acquire_lock(conn, lockname, acquire_timeout=10):
# 代码清单 6-9
#
def purchase_item_with_lock(conn, buyerid, itemid, sellerid):
- buyer = "users:%s" % buyerid
- seller = "users:%s" % sellerid
- item = "%s.%s" % (itemid, sellerid)
- inventory = "inventory:%s" % buyerid
+ buyer = "users:%s" % buyerid
+ seller = "users:%s" % sellerid
+ item = "%s.%s" % (itemid, sellerid)
+ inventory = "inventory:%s" % buyerid
- # 尝试获取锁。
- locked = acquire_lock(conn, 'market:')
- if not locked:
- return False
+ # 尝试获取锁。
+ locked = acquire_lock(conn, 'market:')
+ if not locked:
+ return False
- pipe = conn.pipeline(True)
- try:
- # 检查物品是否已经售出,以及买家是否有足够的金钱来购买物品。
- pipe.zscore("market:", item)
- pipe.hget(buyer, 'funds')
- price, funds = pipe.execute()
- if price is None or price > funds:
- return None
+ pipe = conn.pipeline(True)
+ try:
+ # 检查物品是否已经售出,以及买家是否有足够的金钱来购买物品。
+ pipe.zscore("market:", item)
+ pipe.hget(buyer, 'funds')
+ price, funds = pipe.execute()
+ if price is None or price > funds:
+ return None
- # 将买家支付的货款转移给卖家,并将售出的物品转移给买家。
- pipe.hincrby(seller, 'funds', int(price))
- pipe.hincrby(buyer, 'funds', int(-price))
- pipe.sadd(inventory, itemid)
- pipe.zrem("market:", item)
- pipe.execute()
- return True
- finally:
- # 释放锁。
- release_lock(conn, 'market:', locked)
- #
+ # 将买家支付的货款转移给卖家,并将售出的物品转移给买家。
+ pipe.hincrby(seller, 'funds', int(price))
+ pipe.hincrby(buyer, 'funds', int(-price))
+ pipe.sadd(inventory, itemid)
+ pipe.zrem("market:", item)
+ pipe.execute()
+ return True
+ finally:
+ # 释放锁。
+ release_lock(conn, 'market:', locked)
+ #
# 代码清单 6-10
#
def release_lock(conn, lockname, identifier):
- pipe = conn.pipeline(True)
- lockname = 'lock:' + lockname
+ pipe = conn.pipeline(True)
+ lockname = 'lock:' + lockname
- while True:
- try:
- # 检查并确认进程还持有着锁。
- pipe.watch(lockname)
- if pipe.get(lockname) == identifier:
- # 释放锁。
- pipe.multi()
- pipe.delete(lockname)
- pipe.execute()
- return True
+ while True:
+ try:
+ # 检查并确认进程还持有着锁。
+ pipe.watch(lockname)
+ if pipe.get(lockname) == identifier:
+ # 释放锁。
+ pipe.multi()
+ pipe.delete(lockname)
+ pipe.execute()
+ return True
- pipe.unwatch()
- break
+ pipe.unwatch()
+ break
- # 有其他客户端修改了锁;重试。
- except redis.exceptions.WatchError:
- pass
+ # 有其他客户端修改了锁;重试。
+ except redis.exceptions.WatchError:
+ pass
- # 进程已经失去了锁。
- return False
+ # 进程已经失去了锁。
+ return False
#
@@ -271,26 +271,26 @@ def release_lock(conn, lockname, identifier):
# 代码清单 6-11
#
def acquire_lock_with_timeout(
- conn, lockname, acquire_timeout=10, lock_timeout=10):
- # 128位随机标识符。
- identifier = str(uuid.uuid4())
- lockname = 'lock:' + lockname
- # 确保传给EXPIRE的都是整数。
- lock_timeout = int(math.ceil(lock_timeout))
+ conn, lockname, acquire_timeout=10, lock_timeout=10):
+ # 128位随机标识符。
+ identifier = str(uuid.uuid4())
+ lockname = 'lock:' + lockname
+ # 确保传给EXPIRE的都是整数。
+ lock_timeout = int(math.ceil(lock_timeout))
- end = time.time() + acquire_timeout
- while time.time() < end:
- # 获取锁并设置过期时间。
- if conn.setnx(lockname, identifier):
- conn.expire(lockname, lock_timeout)
- return identifier
- # 检查过期时间,并在有需要时对其进行更新。
- elif not conn.ttl(lockname):
- conn.expire(lockname, lock_timeout)
+ end = time.time() + acquire_timeout
+ while time.time() < end:
+ # 获取锁并设置过期时间。
+ if conn.setnx(lockname, identifier):
+ conn.expire(lockname, lock_timeout)
+ return identifier
+ # 检查过期时间,并在有需要时对其进行更新。
+ elif not conn.ttl(lockname):
+ conn.expire(lockname, lock_timeout)
- time.sleep(.001)
+ time.sleep(.001)
- return False
+ return False
#
@@ -299,23 +299,23 @@ def acquire_lock_with_timeout(
# 代码清单 6-12
#
def acquire_semaphore(conn, semname, limit, timeout=10):
- # 128位随机标识符。
- identifier = str(uuid.uuid4())
- now = time.time()
+ # 128位随机标识符。
+ identifier = str(uuid.uuid4())
+ now = time.time()
- pipeline = conn.pipeline(True)
- # 清理过期的信号量持有者。
- pipeline.zremrangebyscore(semname, '-inf', now - timeout)
- # 尝试获取信号量。
- pipeline.zadd(semname, identifier, now)
- # 检查是否成功取得了信号量。
- pipeline.zrank(semname, identifier)
- if pipeline.execute()[-1] < limit:
- return identifier
+ pipeline = conn.pipeline(True)
+ # 清理过期的信号量持有者。
+ pipeline.zremrangebyscore(semname, '-inf', now - timeout)
+ # 尝试获取信号量。
+ pipeline.zadd(semname, identifier, now)
+ # 检查是否成功取得了信号量。
+ pipeline.zrank(semname, identifier)
+ if pipeline.execute()[-1] < limit:
+ return identifier
- # 获取信号量失败,删除之前添加的标识符。
- conn.zrem(semname, identifier)
- return None
+ # 获取信号量失败,删除之前添加的标识符。
+ conn.zrem(semname, identifier)
+ return None
#
@@ -324,9 +324,9 @@ def acquire_semaphore(conn, semname, limit, timeout=10):
# 代码清单 6-13
#
def release_semaphore(conn, semname, identifier):
- # 如果信号量已经被正确地释放,那么返回True;
- # 返回False则表示该信号量已经因为过期而被删除了。
- return conn.zrem(semname, identifier)
+ # 如果信号量已经被正确地释放,那么返回True;
+ # 返回False则表示该信号量已经因为过期而被删除了。
+ return conn.zrem(semname, identifier)
#
@@ -335,36 +335,36 @@ def release_semaphore(conn, semname, identifier):
# 代码清单 6-14
#
def acquire_fair_semaphore(conn, semname, limit, timeout=10):
- # 128位随机标识符。
- identifier = str(uuid.uuid4())
- czset = semname + ':owner'
- ctr = semname + ':counter'
+ # 128位随机标识符。
+ identifier = str(uuid.uuid4())
+ czset = semname + ':owner'
+ ctr = semname + ':counter'
- now = time.time()
- pipeline = conn.pipeline(True)
- # 删除超时的信号量。
- pipeline.zremrangebyscore(semname, '-inf', now - timeout)
- pipeline.zinterstore(czset, {czset: 1, semname: 0})
+ now = time.time()
+ pipeline = conn.pipeline(True)
+ # 删除超时的信号量。
+ pipeline.zremrangebyscore(semname, '-inf', now - timeout)
+ pipeline.zinterstore(czset, {czset: 1, semname: 0})
- # 对计数器执行自增操作,并获取操作执行之后的值。
- pipeline.incr(ctr)
- counter = pipeline.execute()[-1]
+ # 对计数器执行自增操作,并获取操作执行之后的值。
+ pipeline.incr(ctr)
+ counter = pipeline.execute()[-1]
- # 尝试获取信号量。
- pipeline.zadd(semname, identifier, now)
- pipeline.zadd(czset, identifier, counter)
+ # 尝试获取信号量。
+ pipeline.zadd(semname, identifier, now)
+ pipeline.zadd(czset, identifier, counter)
- # 通过检查排名来判断客户端是否取得了信号量。
- pipeline.zrank(czset, identifier)
- if pipeline.execute()[-1] < limit:
- # 客户端成功取得了信号量。
- return identifier
+ # 通过检查排名来判断客户端是否取得了信号量。
+ pipeline.zrank(czset, identifier)
+ if pipeline.execute()[-1] < limit:
+ # 客户端成功取得了信号量。
+ return identifier
- # 客户端未能取得信号量,清理无用数据。
- pipeline.zrem(semname, identifier)
- pipeline.zrem(czset, identifier)
- pipeline.execute()
- return None
+ # 客户端未能取得信号量,清理无用数据。
+ pipeline.zrem(semname, identifier)
+ pipeline.zrem(czset, identifier)
+ pipeline.execute()
+ return None
#
@@ -373,12 +373,12 @@ def acquire_fair_semaphore(conn, semname, limit, timeout=10):
# 代码清单 6-15
#
def release_fair_semaphore(conn, semname, identifier):
- pipeline = conn.pipeline(True)
- pipeline.zrem(semname, identifier)
- pipeline.zrem(semname + ':owner', identifier)
- # 返回True表示信号量已被正确地释放,
- # 返回False则表示想要释放的信号量已经因为超时而被删除了。
- return pipeline.execute()[0]
+ pipeline = conn.pipeline(True)
+ pipeline.zrem(semname, identifier)
+ pipeline.zrem(semname + ':owner', identifier)
+ # 返回True表示信号量已被正确地释放,
+ # 返回False则表示想要释放的信号量已经因为超时而被删除了。
+ return pipeline.execute()[0]
#
@@ -387,13 +387,13 @@ def release_fair_semaphore(conn, semname, identifier):
# 代码清单 6-16
#
def refresh_fair_semaphore(conn, semname, identifier):
- # 更新客户端持有的信号量。
- if conn.zadd(semname, identifier, time.time()):
- # 告知调用者,客户端已经失去了信号量。
- release_fair_semaphore(conn, semname, identifier)
- return False
- # 客户端仍然持有信号量。
- return True
+ # 更新客户端持有的信号量。
+ if conn.zadd(semname, identifier, time.time()):
+ # 告知调用者,客户端已经失去了信号量。
+ release_fair_semaphore(conn, semname, identifier)
+ return False
+ # 客户端仍然持有信号量。
+ return True
#
@@ -402,12 +402,12 @@ def refresh_fair_semaphore(conn, semname, identifier):
# 代码清单 6-17
#
def acquire_semaphore_with_lock(conn, semname, limit, timeout=10):
- identifier = acquire_lock(conn, semname, acquire_timeout=.01)
- if identifier:
- try:
- return acquire_fair_semaphore(conn, semname, limit, timeout)
- finally:
- release_lock(conn, semname, identifier)
+ identifier = acquire_lock(conn, semname, acquire_timeout=.01)
+ if identifier:
+ try:
+ return acquire_fair_semaphore(conn, semname, limit, timeout)
+ finally:
+ release_lock(conn, semname, identifier)
#
@@ -416,16 +416,16 @@ def acquire_semaphore_with_lock(conn, semname, limit, timeout=10):
# 代码清单 6-18
#
def send_sold_email_via_queue(conn, seller, item, price, buyer):
- # 准备好待发送邮件。
- data = {
- 'seller_id': seller,
- 'item_id': item,
- 'price': price,
- 'buyer_id': buyer,
- 'time': time.time()
- }
- # 将待发送邮件推入到队列里面。
- conn.rpush('queue:email', json.dumps(data))
+ # 准备好待发送邮件。
+ data = {
+ 'seller_id': seller,
+ 'item_id': item,
+ 'price': price,
+ 'buyer_id': buyer,
+ 'time': time.time()
+ }
+ # 将待发送邮件推入到队列里面。
+ conn.rpush('queue:email', json.dumps(data))
#
@@ -434,22 +434,22 @@ def send_sold_email_via_queue(conn, seller, item, price, buyer):
# 代码清单 6-19
#
def process_sold_email_queue(conn):
- while not QUIT:
- # 尝试获取一封待发送邮件。
- packed = conn.blpop(['queue:email'], 30)
- # 队列里面暂时还没有待发送邮件,重试。
- if not packed:
- continue
+ while not QUIT:
+ # 尝试获取一封待发送邮件。
+ packed = conn.blpop(['queue:email'], 30)
+ # 队列里面暂时还没有待发送邮件,重试。
+ if not packed:
+ continue
- # 从JSON对象中解码出邮件信息。
- to_send = json.loads(packed[1])
- try:
- # 使用预先编写好的邮件发送函数来发送邮件。
- fetch_data_and_send_sold_email(to_send)
- except EmailSendError as err:
- log_error("Failed to send sold email", err, to_send)
- else:
- log_success("Sent sold email", to_send)
+ # 从JSON对象中解码出邮件信息。
+ to_send = json.loads(packed[1])
+ try:
+ # 使用预先编写好的邮件发送函数来发送邮件。
+ fetch_data_and_send_sold_email(to_send)
+ except EmailSendError as err:
+ log_error("Failed to send sold email", err, to_send)
+ else:
+ log_success("Sent sold email", to_send)
#
@@ -458,37 +458,37 @@ def process_sold_email_queue(conn):
# 代码清单 6-20
#
def worker_watch_queue(conn, queue, callbacks):
- while not QUIT:
- # 尝试从队列里面取出一项待执行任务。
- packed = conn.blpop([queue], 30)
- # 队列为空,没有任务需要执行;重试。
- if not packed:
- continue
+ while not QUIT:
+ # 尝试从队列里面取出一项待执行任务。
+ packed = conn.blpop([queue], 30)
+ # 队列为空,没有任务需要执行;重试。
+ if not packed:
+ continue
- # 解码任务信息。
- name, args = json.loads(packed[1])
- # 没有找到任务指定的回调函数,用日志记录错误并重试。
- if name not in callbacks:
- log_error("Unknown callback %s" % name)
- continue
- # 执行任务。
- callbacks[name](*args)
- #
+ # 解码任务信息。
+ name, args = json.loads(packed[1])
+ # 没有找到任务指定的回调函数,用日志记录错误并重试。
+ if name not in callbacks:
+ log_error("Unknown callback %s" % name)
+ continue
+ # 执行任务。
+ callbacks[name](*args)
+ #
# 代码清单 6-21
#
def worker_watch_queues(conn, queues, callbacks): # 实现优先级特性要修改的第一行代码。
- while not QUIT:
- packed = conn.blpop(queues, 30) # 实现优先级特性要修改的第二行代码。
- if not packed:
- continue
+ while not QUIT:
+ packed = conn.blpop(queues, 30) # 实现优先级特性要修改的第二行代码。
+ if not packed:
+ continue
- name, args = json.loads(packed[1])
- if name not in callbacks:
- log_error("Unknown callback %s" % name)
- continue
- callbacks[name](*args)
+ name, args = json.loads(packed[1])
+ if name not in callbacks:
+ log_error("Unknown callback %s" % name)
+ continue
+ callbacks[name](*args)
#
@@ -497,18 +497,18 @@ def worker_watch_queues(conn, queues, callbacks): # 实现优先级特性要修
# 代码清单 6-22
#
def execute_later(conn, queue, name, args, delay=0):
- # 创建唯一标识符。
- identifier = str(uuid.uuid4())
- # 准备好需要入队的任务。
- item = json.dumps([identifier, queue, name, args])
- if delay > 0:
- # 延迟执行这个任务。
- conn.zadd('delayed:', item, time.time() + delay)
- else:
- # 立即执行这个任务。
- conn.rpush('queue:' + queue, item)
- # 返回标识符。
- return identifier
+ # 创建唯一标识符。
+ identifier = str(uuid.uuid4())
+ # 准备好需要入队的任务。
+ item = json.dumps([identifier, queue, name, args])
+ if delay > 0:
+ # 延迟执行这个任务。
+ conn.zadd('delayed:', item, time.time() + delay)
+ else:
+ # 立即执行这个任务。
+ conn.rpush('queue:' + queue, item)
+ # 返回标识符。
+ return identifier
#
@@ -517,53 +517,53 @@ def execute_later(conn, queue, name, args, delay=0):
# 代码清单 6-23
#
def poll_queue(conn):
- while not QUIT:
- # 获取队列中的第一个任务。
- item = conn.zrange('delayed:', 0, 0, withscores=True)
- # 队列没有包含任何任务,或者任务的执行时间未到。
- if not item or item[0][1] > time.time():
- time.sleep(.01)
- continue
+ while not QUIT:
+ # 获取队列中的第一个任务。
+ item = conn.zrange('delayed:', 0, 0, withscores=True)
+ # 队列没有包含任何任务,或者任务的执行时间未到。
+ if not item or item[0][1] > time.time():
+ time.sleep(.01)
+ continue
- # 解码要被执行的任务,弄清楚它应该被推入到哪个任务队列里面。
- item = item[0][0]
- identifier, queue, function, args = json.loads(item)
+ # 解码要被执行的任务,弄清楚它应该被推入到哪个任务队列里面。
+ item = item[0][0]
+ identifier, queue, function, args = json.loads(item)
- # 为了对任务进行移动,尝试获取锁。
- locked = acquire_lock(conn, identifier)
- # 获取锁失败,跳过后续步骤并重试。
- if not locked:
- continue
+ # 为了对任务进行移动,尝试获取锁。
+ locked = acquire_lock(conn, identifier)
+ # 获取锁失败,跳过后续步骤并重试。
+ if not locked:
+ continue
- # 将任务推入到适当的任务队列里面。
- if conn.zrem('delayed:', item):
- conn.rpush('queue:' + queue, item)
+ # 将任务推入到适当的任务队列里面。
+ if conn.zrem('delayed:', item):
+ conn.rpush('queue:' + queue, item)
- # 释放锁。
- release_lock(conn, identifier, locked)
- #
+ # 释放锁。
+ release_lock(conn, identifier, locked)
+ #
# 代码清单 6-24
#
def create_chat(conn, sender, recipients, message, chat_id=None):
- # 获得新的群组ID。
- chat_id = chat_id or str(conn.incr('ids:chat:'))
+ # 获得新的群组ID。
+ chat_id = chat_id or str(conn.incr('ids:chat:'))
- # 创建一个由用户和分值组成的字典,字典里面的信息将被添加到有序集合里面。
- recipients.append(sender)
- recipientsd = dict((r, 0) for r in recipients)
+ # 创建一个由用户和分值组成的字典,字典里面的信息将被添加到有序集合里面。
+ recipients.append(sender)
+ recipientsd = dict((r, 0) for r in recipients)
- pipeline = conn.pipeline(True)
- # 将所有参与群聊的用户添加到有序集合里面。
- pipeline.zadd('chat:' + chat_id, **recipientsd)
- # 初始化已读有序集合。
- for rec in recipients:
- pipeline.zadd('seen:' + rec, chat_id, 0)
- pipeline.execute()
+ pipeline = conn.pipeline(True)
+ # 将所有参与群聊的用户添加到有序集合里面。
+ pipeline.zadd('chat:' + chat_id, **recipientsd)
+ # 初始化已读有序集合。
+ for rec in recipients:
+ pipeline.zadd('seen:' + rec, chat_id, 0)
+ pipeline.execute()
- # 发送消息。
- return send_message(conn, chat_id, sender, message)
+ # 发送消息。
+ return send_message(conn, chat_id, sender, message)
#
@@ -572,25 +572,25 @@ def create_chat(conn, sender, recipients, message, chat_id=None):
# 代码清单 6-25
#
def send_message(conn, chat_id, sender, message):
- identifier = acquire_lock(conn, 'chat:' + chat_id)
- if not identifier:
- raise Exception("Couldn't get the lock")
- try:
- # 筹备待发送的消息。
- mid = conn.incr('ids:' + chat_id)
- ts = time.time()
- packed = json.dumps({
- 'id': mid,
- 'ts': ts,
- 'sender': sender,
- 'message': message,
- })
+ identifier = acquire_lock(conn, 'chat:' + chat_id)
+ if not identifier:
+ raise Exception("Couldn't get the lock")
+ try:
+ # 筹备待发送的消息。
+ mid = conn.incr('ids:' + chat_id)
+ ts = time.time()
+ packed = json.dumps({
+ 'id': mid,
+ 'ts': ts,
+ 'sender': sender,
+ 'message': message,
+ })
- # 将消息发送至群组。
- conn.zadd('msgs:' + chat_id, packed, mid)
- finally:
- release_lock(conn, 'chat:' + chat_id, identifier)
- return chat_id
+ # 将消息发送至群组。
+ conn.zadd('msgs:' + chat_id, packed, mid)
+ finally:
+ release_lock(conn, 'chat:' + chat_id, identifier)
+ return chat_id
#
@@ -599,40 +599,40 @@ def send_message(conn, chat_id, sender, message):
# 代码清单 6-26
#
def fetch_pending_messages(conn, recipient):
- # 获取最后接收到的消息的ID。
- seen = conn.zrange('seen:' + recipient, 0, -1, withscores=True)
+ # 获取最后接收到的消息的ID。
+ seen = conn.zrange('seen:' + recipient, 0, -1, withscores=True)
- pipeline = conn.pipeline(True)
+ pipeline = conn.pipeline(True)
- # 获取所有未读消息。
- for chat_id, seen_id in seen:
- pipeline.zrangebyscore(
- 'msgs:' + chat_id, seen_id + 1, 'inf')
- # 这些数据将被返回给函数调用者。
- chat_info = zip(seen, pipeline.execute())
+ # 获取所有未读消息。
+ for chat_id, seen_id in seen:
+ pipeline.zrangebyscore(
+ 'msgs:' + chat_id, seen_id + 1, 'inf')
+ # 这些数据将被返回给函数调用者。
+ chat_info = zip(seen, pipeline.execute())
- for i, ((chat_id, seen_id), messages) in enumerate(chat_info):
- if not messages:
- continue
- messages[:] = map(json.loads, messages)
- # 使用最新收到的消息来更新群组有序集合。
- seen_id = messages[-1]['id']
- conn.zadd('chat:' + chat_id, recipient, seen_id)
+ for i, ((chat_id, seen_id), messages) in enumerate(chat_info):
+ if not messages:
+ continue
+ messages[:] = map(json.loads, messages)
+ # 使用最新收到的消息来更新群组有序集合。
+ seen_id = messages[-1]['id']
+ conn.zadd('chat:' + chat_id, recipient, seen_id)
- # 找出那些所有人都已经阅读过的消息。
- min_id = conn.zrange(
- 'chat:' + chat_id, 0, 0, withscores=True)
+ # 找出那些所有人都已经阅读过的消息。
+ min_id = conn.zrange(
+ 'chat:' + chat_id, 0, 0, withscores=True)
- # 更新已读消息有序集合。
- pipeline.zadd('seen:' + recipient, chat_id, seen_id)
- if min_id:
- # 清除那些已经被所有人阅读过的消息。
- pipeline.zremrangebyscore(
- 'msgs:' + chat_id, 0, min_id[0][1])
- chat_info[i] = (chat_id, messages)
- pipeline.execute()
+ # 更新已读消息有序集合。
+ pipeline.zadd('seen:' + recipient, chat_id, seen_id)
+ if min_id:
+ # 清除那些已经被所有人阅读过的消息。
+ pipeline.zremrangebyscore(
+ 'msgs:' + chat_id, 0, min_id[0][1])
+ chat_info[i] = (chat_id, messages)
+ pipeline.execute()
- return chat_info
+ return chat_info
#
@@ -641,15 +641,15 @@ def fetch_pending_messages(conn, recipient):
# 代码清单 6-27
#
def join_chat(conn, chat_id, user):
- # 取得最新群组消息的ID。
- message_id = int(conn.get('ids:' + chat_id))
+ # 取得最新群组消息的ID。
+ message_id = int(conn.get('ids:' + chat_id))
- pipeline = conn.pipeline(True)
- # 将用户添加到群组成员列表里面。
- pipeline.zadd('chat:' + chat_id, user, message_id)
- # 将群组添加到用户的已读列表里面。
- pipeline.zadd('seen:' + user, chat_id, message_id)
- pipeline.execute()
+ pipeline = conn.pipeline(True)
+ # 将用户添加到群组成员列表里面。
+ pipeline.zadd('chat:' + chat_id, user, message_id)
+ # 将群组添加到用户的已读列表里面。
+ pipeline.zadd('seen:' + user, chat_id, message_id)
+ pipeline.execute()
#
@@ -658,24 +658,24 @@ def join_chat(conn, chat_id, user):
# 代码清单 6-28
#
def leave_chat(conn, chat_id, user):
- pipeline = conn.pipeline(True)
- # 从群组里面移除给定的用户。
- pipeline.zrem('chat:' + chat_id, user)
- pipeline.zrem('seen:' + user, chat_id)
- # 查看群组剩余成员的数量。
- pipeline.zcard('chat:' + chat_id)
+ pipeline = conn.pipeline(True)
+ # 从群组里面移除给定的用户。
+ pipeline.zrem('chat:' + chat_id, user)
+ pipeline.zrem('seen:' + user, chat_id)
+ # 查看群组剩余成员的数量。
+ pipeline.zcard('chat:' + chat_id)
- if not pipeline.execute()[-1]:
- # 删除群组。
- pipeline.delete('msgs:' + chat_id)
- pipeline.delete('ids:' + chat_id)
- pipeline.execute()
- else:
- # 查找那些已经被所有成员阅读过的消息。
- oldest = conn.zrange(
- 'chat:' + chat_id, 0, 0, withscores=True)
- # 删除那些已经被所有成员阅读过的消息。
- conn.zremrangebyscore('msgs:' + chat_id, 0, oldest[0][1])
+ if not pipeline.execute()[-1]:
+ # 删除群组。
+ pipeline.delete('msgs:' + chat_id)
+ pipeline.delete('ids:' + chat_id)
+ pipeline.execute()
+ else:
+ # 查找那些已经被所有成员阅读过的消息。
+ oldest = conn.zrange(
+ 'chat:' + chat_id, 0, 0, withscores=True)
+ # 删除那些已经被所有成员阅读过的消息。
+ conn.zremrangebyscore('msgs:' + chat_id, 0, oldest[0][1])
#
@@ -688,83 +688,83 @@ aggregates = defaultdict(lambda: defaultdict(int))
def daily_country_aggregate(conn, line):
- if line:
- line = line.split()
- # 提取日志行中的信息。
- ip = line[0]
- day = line[1]
- # 根据IP地址判断用户所在国家。
- country = find_city_by_ip_local(ip)[2]
- # 对本地聚合数据执行自增操作。
- aggregates[day][country] += 1
- return
+ if line:
+ line = line.split()
+ # 提取日志行中的信息。
+ ip = line[0]
+ day = line[1]
+ # 根据IP地址判断用户所在国家。
+ country = find_city_by_ip_local(ip)[2]
+ # 对本地聚合数据执行自增操作。
+ aggregates[day][country] += 1
+ return
- # 当天的日志文件已经处理完毕,将聚合计算的结果写入到Redis里面。
- for day, aggregate in aggregates.items():
- conn.zadd('daily:country:' + day, **aggregate)
- del aggregates[day]
- #
+ # 当天的日志文件已经处理完毕,将聚合计算的结果写入到Redis里面。
+ for day, aggregate in aggregates.items():
+ conn.zadd('daily:country:' + day, **aggregate)
+ del aggregates[day]
+ #
# 代码清单 6-30
#
def copy_logs_to_redis(conn, path, channel, count=10,
- limit=2 ** 30, quit_when_done=True):
- bytes_in_redis = 0
- waiting = deque()
- # 创建用于向客户端发送消息的群组。
- create_chat(conn, 'source', map(str, range(count)), '', channel)
- count = str(count)
- # 遍历所有日志文件。
- for logfile in sorted(os.listdir(path)):
- full_path = os.path.join(path, logfile)
+ limit=2 ** 30, quit_when_done=True):
+ bytes_in_redis = 0
+ waiting = deque()
+ # 创建用于向客户端发送消息的群组。
+ create_chat(conn, 'source', map(str, range(count)), '', channel)
+ count = str(count)
+ # 遍历所有日志文件。
+ for logfile in sorted(os.listdir(path)):
+ full_path = os.path.join(path, logfile)
- fsize = os.stat(full_path).st_size
- # 如果程序需要更多空间,那么清除已经处理完毕的文件。
- while bytes_in_redis + fsize > limit:
- cleaned = _clean(conn, channel, waiting, count)
- if cleaned:
- bytes_in_redis -= cleaned
- else:
- time.sleep(.25)
+ fsize = os.stat(full_path).st_size
+ # 如果程序需要更多空间,那么清除已经处理完毕的文件。
+ while bytes_in_redis + fsize > limit:
+ cleaned = _clean(conn, channel, waiting, count)
+ if cleaned:
+ bytes_in_redis -= cleaned
+ else:
+ time.sleep(.25)
- # 将文件上传至Redis。
- with open(full_path, 'rb') as inp:
- block = ' '
- while block:
- block = inp.read(2 ** 17)
- conn.append(channel + logfile, block)
+ # 将文件上传至Redis。
+ with open(full_path, 'rb') as inp:
+ block = ' '
+ while block:
+ block = inp.read(2 ** 17)
+ conn.append(channel + logfile, block)
- # 提醒监听者,文件已经准备就绪。
- send_message(conn, channel, 'source', logfile)
+ # 提醒监听者,文件已经准备就绪。
+ send_message(conn, channel, 'source', logfile)
- # 对本地记录的Redis内存占用量相关信息进行更新。
- bytes_in_redis += fsize
- waiting.append((logfile, fsize))
+ # 对本地记录的Redis内存占用量相关信息进行更新。
+ bytes_in_redis += fsize
+ waiting.append((logfile, fsize))
- # 所有日志文件已经处理完毕,向监听者报告此事。
- if quit_when_done:
- send_message(conn, channel, 'source', ':done')
+ # 所有日志文件已经处理完毕,向监听者报告此事。
+ if quit_when_done:
+ send_message(conn, channel, 'source', ':done')
- # 在工作完成之后,清理无用的日志文件。
- while waiting:
- cleaned = _clean(conn, channel, waiting, count)
- if cleaned:
- bytes_in_redis -= cleaned
- else:
- time.sleep(.25)
+ # 在工作完成之后,清理无用的日志文件。
+ while waiting:
+ cleaned = _clean(conn, channel, waiting, count)
+ if cleaned:
+ bytes_in_redis -= cleaned
+ else:
+ time.sleep(.25)
- # 对Redis进行清理的详细步骤。
+ # 对Redis进行清理的详细步骤。
def _clean(conn, channel, waiting, count):
- if not waiting:
- return 0
- w0 = waiting[0][0]
- if conn.get(channel + w0 + ':done') == count:
- conn.delete(channel + w0, channel + w0 + ':done')
- return waiting.popleft()[1]
- return 0
+ if not waiting:
+ return 0
+ w0 = waiting[0][0]
+ if conn.get(channel + w0 + ':done') == count:
+ conn.delete(channel + w0, channel + w0 + ':done')
+ return waiting.popleft()[1]
+ return 0
#
@@ -773,37 +773,37 @@ def _clean(conn, channel, waiting, count):
# 代码清单 6-31
#
def process_logs_from_redis(conn, id, callback):
- while 1:
- # 获取文件列表。
- fdata = fetch_pending_messages(conn, id)
+ while 1:
+ # 获取文件列表。
+ fdata = fetch_pending_messages(conn, id)
- for ch, mdata in fdata:
- for message in mdata:
- logfile = message['message']
+ for ch, mdata in fdata:
+ for message in mdata:
+ logfile = message['message']
- # 所有日志行已经处理完毕。
- if logfile == ':done':
- return
- elif not logfile:
- continue
+ # 所有日志行已经处理完毕。
+ if logfile == ':done':
+ return
+ elif not logfile:
+ continue
- # 选择一个块读取器(block reader)。
- block_reader = readblocks
- if logfile.endswith('.gz'):
- block_reader = readblocks_gz
+ # 选择一个块读取器(block reader)。
+ block_reader = readblocks
+ if logfile.endswith('.gz'):
+ block_reader = readblocks_gz
- # 遍历日志行。
- for line in readlines(conn, ch + logfile, block_reader):
- # 将日志行传递给回调函数。
- callback(conn, line)
- # 强制地刷新聚合数据缓存。
- callback(conn, None)
+ # 遍历日志行。
+ for line in readlines(conn, ch + logfile, block_reader):
+ # 将日志行传递给回调函数。
+ callback(conn, line)
+ # 强制地刷新聚合数据缓存。
+ callback(conn, None)
- # 报告日志已经处理完毕。
- conn.incr(ch + logfile + ':done')
+ # 报告日志已经处理完毕。
+ conn.incr(ch + logfile + ':done')
- if not fdata:
- time.sleep(.1)
+ if not fdata:
+ time.sleep(.1)
#
@@ -812,23 +812,23 @@ def process_logs_from_redis(conn, id, callback):
# 代码清单 6-32
#
def readlines(conn, key, rblocks):
- out = ''
- for block in rblocks(conn, key):
- out += block
- # 查找位于文本最右端的断行符;如果断行符不存在,那么rfind()返回-1。
- posn = out.rfind('\n')
- # 找到一个断行符。
- if posn >= 0:
- # 根据断行符来分割日志行。
- for line in out[:posn].split('\n'):
- # 向调用者返回每个行。
- yield line + '\n'
- # 保留余下的数据。
- out = out[posn + 1:]
- # 所有数据块已经处理完毕。
- if not block:
- yield out
- break
+ out = ''
+ for block in rblocks(conn, key):
+ out += block
+ # 查找位于文本最右端的断行符;如果断行符不存在,那么rfind()返回-1。
+ posn = out.rfind('\n')
+ # 找到一个断行符。
+ if posn >= 0:
+ # 根据断行符来分割日志行。
+ for line in out[:posn].split('\n'):
+ # 向调用者返回每个行。
+ yield line + '\n'
+ # 保留余下的数据。
+ out = out[posn + 1:]
+ # 所有数据块已经处理完毕。
+ if not block:
+ yield out
+ break
#
@@ -837,17 +837,17 @@ def readlines(conn, key, rblocks):
# 代码清单 6-33
#
def readblocks(conn, key, blocksize=2 ** 17):
- lb = blocksize
- pos = 0
- # 尽可能地读取更多数据,直到出现不完整读操作(partial read)为止。
- while lb == blocksize:
- # 获取数据块。
- block = conn.substr(key, pos, pos + blocksize - 1)
- # 准备进行下一次遍历。
- yield block
- lb = len(block)
- pos += lb
- yield ''
+ lb = blocksize
+ pos = 0
+ # 尽可能地读取更多数据,直到出现不完整读操作(partial read)为止。
+ while lb == blocksize:
+ # 获取数据块。
+ block = conn.substr(key, pos, pos + blocksize - 1)
+ # 准备进行下一次遍历。
+ yield block
+ lb = len(block)
+ pos += lb
+ yield ''
#
@@ -856,272 +856,272 @@ def readblocks(conn, key, blocksize=2 ** 17):
# 代码清单 6-34
#
def readblocks_gz(conn, key):
- inp = ''
- decoder = None
- # 从Redis里面读入原始数据。
- for block in readblocks(conn, key, 2 ** 17):
- if not decoder:
- inp += block
- try:
- # 分析头信息以便取得被压缩数据。
- if inp[:3] != "\x1f\x8b\x08":
- raise IOError("invalid gzip data")
- i = 10
- flag = ord(inp[3])
- if flag & 4:
- i += 2 + ord(inp[i]) + 256 * ord(inp[i + 1])
- if flag & 8:
- i = inp.index('\0', i) + 1
- if flag & 16:
- i = inp.index('\0', i) + 1
- if flag & 2:
- i += 2
+ inp = ''
+ decoder = None
+ # 从Redis里面读入原始数据。
+ for block in readblocks(conn, key, 2 ** 17):
+ if not decoder:
+ inp += block
+ try:
+ # 分析头信息以便取得被压缩数据。
+ if inp[:3] != "\x1f\x8b\x08":
+ raise IOError("invalid gzip data")
+ i = 10
+ flag = ord(inp[3])
+ if flag & 4:
+ i += 2 + ord(inp[i]) + 256 * ord(inp[i + 1])
+ if flag & 8:
+ i = inp.index('\0', i) + 1
+ if flag & 16:
+ i = inp.index('\0', i) + 1
+ if flag & 2:
+ i += 2
- # 程序读取的头信息并不完整。
- if i > len(inp):
- raise IndexError("not enough data")
- except (IndexError, ValueError):
- continue
+ # 程序读取的头信息并不完整。
+ if i > len(inp):
+ raise IndexError("not enough data")
+ except (IndexError, ValueError):
+ continue
- else:
- # 已经找到头信息,准备好相应的解压程序。
- block = inp[i:]
- inp = None
- decoder = zlib.decompressobj(-zlib.MAX_WBITS)
- if not block:
- continue
+ else:
+ # 已经找到头信息,准备好相应的解压程序。
+ block = inp[i:]
+ inp = None
+ decoder = zlib.decompressobj(-zlib.MAX_WBITS)
+ if not block:
+ continue
- # 所有数据已经处理完毕,向调用者返回最后剩下的数据块。
- if not block:
- yield decoder.flush()
- break
+ # 所有数据已经处理完毕,向调用者返回最后剩下的数据块。
+ if not block:
+ yield decoder.flush()
+ break
- # 向调用者返回解压后的数据块。
- yield decoder.decompress(block)
- #
+ # 向调用者返回解压后的数据块。
+ yield decoder.decompress(block)
+ #
class TestCh06(unittest.TestCase):
- def setUp(self):
- import redis
- self.conn = redis.Redis(db=15)
+ def setUp(self):
+ import redis
+ self.conn = redis.Redis(db=15)
- def tearDown(self):
- self.conn.flushdb()
- del self.conn
- print
- print
+ def tearDown(self):
+ self.conn.flushdb()
+ del self.conn
+ print
+ print
- def test_add_update_contact(self):
- import pprint
- conn = self.conn
- conn.delete('recent:user')
+ def test_add_update_contact(self):
+ import pprint
+ conn = self.conn
+ conn.delete('recent:user')
- print "Let's add a few contacts..."
- for i in xrange(10):
- add_update_contact(conn, 'user', 'contact-%i-%i' % (i // 3, i))
- print "Current recently contacted contacts"
- contacts = conn.lrange('recent:user', 0, -1)
- pprint.pprint(contacts)
- self.assertTrue(len(contacts) >= 10)
- print
+ print "Let's add a few contacts..."
+ for i in xrange(10):
+ add_update_contact(conn, 'user', 'contact-%i-%i' % (i // 3, i))
+ print "Current recently contacted contacts"
+ contacts = conn.lrange('recent:user', 0, -1)
+ pprint.pprint(contacts)
+ self.assertTrue(len(contacts) >= 10)
+ print
- print "Let's pull one of the older ones up to the front"
- add_update_contact(conn, 'user', 'contact-1-4')
- contacts = conn.lrange('recent:user', 0, 2)
- print "New top-3 contacts:"
- pprint.pprint(contacts)
- self.assertEquals(contacts[0], 'contact-1-4')
- print
+ print "Let's pull one of the older ones up to the front"
+ add_update_contact(conn, 'user', 'contact-1-4')
+ contacts = conn.lrange('recent:user', 0, 2)
+ print "New top-3 contacts:"
+ pprint.pprint(contacts)
+ self.assertEquals(contacts[0], 'contact-1-4')
+ print
- print "Let's remove a contact..."
- print remove_contact(conn, 'user', 'contact-2-6')
- contacts = conn.lrange('recent:user', 0, -1)
- print "New contacts:"
- pprint.pprint(contacts)
- self.assertTrue(len(contacts) >= 9)
- print
+ print "Let's remove a contact..."
+ print remove_contact(conn, 'user', 'contact-2-6')
+ contacts = conn.lrange('recent:user', 0, -1)
+ print "New contacts:"
+ pprint.pprint(contacts)
+ self.assertTrue(len(contacts) >= 9)
+ print
- print "And let's finally autocomplete on "
- all = conn.lrange('recent:user', 0, -1)
- contacts = fetch_autocomplete_list(conn, 'user', 'c')
- self.assertTrue(all == contacts)
- equiv = [c for c in all if c.startswith('contact-2-')]
- contacts = fetch_autocomplete_list(conn, 'user', 'contact-2-')
- equiv.sort()
- contacts.sort()
- self.assertEquals(equiv, contacts)
- conn.delete('recent:user')
+ print "And let's finally autocomplete on "
+ all = conn.lrange('recent:user', 0, -1)
+ contacts = fetch_autocomplete_list(conn, 'user', 'c')
+ self.assertTrue(all == contacts)
+ equiv = [c for c in all if c.startswith('contact-2-')]
+ contacts = fetch_autocomplete_list(conn, 'user', 'contact-2-')
+ equiv.sort()
+ contacts.sort()
+ self.assertEquals(equiv, contacts)
+ conn.delete('recent:user')
- def test_address_book_autocomplete(self):
- self.conn.delete('members:test')
- print "the start/end range of 'abc' is:", find_prefix_range('abc')
- print
+ def test_address_book_autocomplete(self):
+ self.conn.delete('members:test')
+ print "the start/end range of 'abc' is:", find_prefix_range('abc')
+ print
- print "Let's add a few people to the guild"
- for name in ['jeff', 'jenny', 'jack', 'jennifer']:
- join_guild(self.conn, 'test', name)
- print
- print "now let's try to find users with names starting with 'je':"
- r = autocomplete_on_prefix(self.conn, 'test', 'je')
- print r
- self.assertTrue(len(r) == 3)
- print "jeff just left to join a different guild..."
- leave_guild(self.conn, 'test', 'jeff')
- r = autocomplete_on_prefix(self.conn, 'test', 'je')
- print r
- self.assertTrue(len(r) == 2)
- self.conn.delete('members:test')
+ print "Let's add a few people to the guild"
+ for name in ['jeff', 'jenny', 'jack', 'jennifer']:
+ join_guild(self.conn, 'test', name)
+ print
+ print "now let's try to find users with names starting with 'je':"
+ r = autocomplete_on_prefix(self.conn, 'test', 'je')
+ print r
+ self.assertTrue(len(r) == 3)
+ print "jeff just left to join a different guild..."
+ leave_guild(self.conn, 'test', 'jeff')
+ r = autocomplete_on_prefix(self.conn, 'test', 'je')
+ print r
+ self.assertTrue(len(r) == 2)
+ self.conn.delete('members:test')
- def test_distributed_locking(self):
- self.conn.delete('lock:testlock')
- print "Getting an initial lock..."
- self.assertTrue(acquire_lock_with_timeout(self.conn, 'testlock', 1, 1))
- print "Got it!"
- print "Trying to get it again without releasing the first one..."
- self.assertFalse(acquire_lock_with_timeout(self.conn, 'testlock', .01, 1))
- print "Failed to get it!"
- print
- print "Waiting for the lock to timeout..."
- time.sleep(2)
- print "Getting the lock again..."
- r = acquire_lock_with_timeout(self.conn, 'testlock', 1, 1)
- self.assertTrue(r)
- print "Got it!"
- print "Releasing the lock..."
- self.assertTrue(release_lock(self.conn, 'testlock', r))
- print "Released it..."
- print
- print "Acquiring it again..."
- self.assertTrue(acquire_lock_with_timeout(self.conn, 'testlock', 1, 1))
- print "Got it!"
- self.conn.delete('lock:testlock')
+ def test_distributed_locking(self):
+ self.conn.delete('lock:testlock')
+ print "Getting an initial lock..."
+ self.assertTrue(acquire_lock_with_timeout(self.conn, 'testlock', 1, 1))
+ print "Got it!"
+ print "Trying to get it again without releasing the first one..."
+ self.assertFalse(acquire_lock_with_timeout(self.conn, 'testlock', .01, 1))
+ print "Failed to get it!"
+ print
+ print "Waiting for the lock to timeout..."
+ time.sleep(2)
+ print "Getting the lock again..."
+ r = acquire_lock_with_timeout(self.conn, 'testlock', 1, 1)
+ self.assertTrue(r)
+ print "Got it!"
+ print "Releasing the lock..."
+ self.assertTrue(release_lock(self.conn, 'testlock', r))
+ print "Released it..."
+ print
+ print "Acquiring it again..."
+ self.assertTrue(acquire_lock_with_timeout(self.conn, 'testlock', 1, 1))
+ print "Got it!"
+ self.conn.delete('lock:testlock')
- def test_counting_semaphore(self):
- self.conn.delete('testsem', 'testsem:owner', 'testsem:counter')
- print "Getting 3 initial semaphores with a limit of 3..."
- for i in xrange(3):
- self.assertTrue(acquire_fair_semaphore(self.conn, 'testsem', 3, 1))
- print "Done!"
- print "Getting one more that should fail..."
- self.assertFalse(acquire_fair_semaphore(self.conn, 'testsem', 3, 1))
- print "Couldn't get it!"
- print
- print "Lets's wait for some of them to time out"
- time.sleep(2)
- print "Can we get one?"
- r = acquire_fair_semaphore(self.conn, 'testsem', 3, 1)
- self.assertTrue(r)
- print "Got one!"
- print "Let's release it..."
- self.assertTrue(release_fair_semaphore(self.conn, 'testsem', r))
- print "Released!"
- print
- print "And let's make sure we can get 3 more!"
- for i in xrange(3):
- self.assertTrue(acquire_fair_semaphore(self.conn, 'testsem', 3, 1))
- print "We got them!"
- self.conn.delete('testsem', 'testsem:owner', 'testsem:counter')
+ def test_counting_semaphore(self):
+ self.conn.delete('testsem', 'testsem:owner', 'testsem:counter')
+ print "Getting 3 initial semaphores with a limit of 3..."
+ for i in xrange(3):
+ self.assertTrue(acquire_fair_semaphore(self.conn, 'testsem', 3, 1))
+ print "Done!"
+ print "Getting one more that should fail..."
+ self.assertFalse(acquire_fair_semaphore(self.conn, 'testsem', 3, 1))
+ print "Couldn't get it!"
+ print
+ print "Lets's wait for some of them to time out"
+ time.sleep(2)
+ print "Can we get one?"
+ r = acquire_fair_semaphore(self.conn, 'testsem', 3, 1)
+ self.assertTrue(r)
+ print "Got one!"
+ print "Let's release it..."
+ self.assertTrue(release_fair_semaphore(self.conn, 'testsem', r))
+ print "Released!"
+ print
+ print "And let's make sure we can get 3 more!"
+ for i in xrange(3):
+ self.assertTrue(acquire_fair_semaphore(self.conn, 'testsem', 3, 1))
+ print "We got them!"
+ self.conn.delete('testsem', 'testsem:owner', 'testsem:counter')
- def test_delayed_tasks(self):
- import threading
- self.conn.delete('queue:tqueue', 'delayed:')
- print "Let's start some regular and delayed tasks..."
- for delay in [0, .5, 0, 1.5]:
- self.assertTrue(execute_later(self.conn, 'tqueue', 'testfn', [], delay))
- r = self.conn.llen('queue:tqueue')
- print "How many non-delayed tasks are there (should be 2)?", r
- self.assertEquals(r, 2)
- print
- print "Let's start up a thread to bring those delayed tasks back..."
- t = threading.Thread(target=poll_queue, args=(self.conn,))
- t.setDaemon(1)
- t.start()
- print "Started."
- print "Let's wait for those tasks to be prepared..."
- time.sleep(2)
- global QUIT
- QUIT = True
- t.join()
- r = self.conn.llen('queue:tqueue')
- print "Waiting is over, how many tasks do we have (should be 4)?", r
- self.assertEquals(r, 4)
- self.conn.delete('queue:tqueue', 'delayed:')
+ def test_delayed_tasks(self):
+ import threading
+ self.conn.delete('queue:tqueue', 'delayed:')
+ print "Let's start some regular and delayed tasks..."
+ for delay in [0, .5, 0, 1.5]:
+ self.assertTrue(execute_later(self.conn, 'tqueue', 'testfn', [], delay))
+ r = self.conn.llen('queue:tqueue')
+ print "How many non-delayed tasks are there (should be 2)?", r
+ self.assertEquals(r, 2)
+ print
+ print "Let's start up a thread to bring those delayed tasks back..."
+ t = threading.Thread(target=poll_queue, args=(self.conn,))
+ t.setDaemon(1)
+ t.start()
+ print "Started."
+ print "Let's wait for those tasks to be prepared..."
+ time.sleep(2)
+ global QUIT
+ QUIT = True
+ t.join()
+ r = self.conn.llen('queue:tqueue')
+ print "Waiting is over, how many tasks do we have (should be 4)?", r
+ self.assertEquals(r, 4)
+ self.conn.delete('queue:tqueue', 'delayed:')
- def test_multi_recipient_messaging(self):
- self.conn.delete('ids:chat:', 'msgs:1', 'ids:1', 'seen:joe', 'seen:jeff', 'seen:jenny')
+ def test_multi_recipient_messaging(self):
+ self.conn.delete('ids:chat:', 'msgs:1', 'ids:1', 'seen:joe', 'seen:jeff', 'seen:jenny')
- print "Let's create a new chat session with some recipients..."
- chat_id = create_chat(self.conn, 'joe', ['jeff', 'jenny'], 'message 1')
- print "Now let's send a few messages..."
- for i in xrange(2, 5):
- send_message(self.conn, chat_id, 'joe', 'message %s' % i)
- print
- print "And let's get the messages that are waiting for jeff and jenny..."
- r1 = fetch_pending_messages(self.conn, 'jeff')
- r2 = fetch_pending_messages(self.conn, 'jenny')
- print "They are the same?", r1 == r2
- self.assertEquals(r1, r2)
- print "Those messages are:"
- import pprint
- pprint.pprint(r1)
- self.conn.delete('ids:chat:', 'msgs:1', 'ids:1', 'seen:joe', 'seen:jeff', 'seen:jenny')
+ print "Let's create a new chat session with some recipients..."
+ chat_id = create_chat(self.conn, 'joe', ['jeff', 'jenny'], 'message 1')
+ print "Now let's send a few messages..."
+ for i in xrange(2, 5):
+ send_message(self.conn, chat_id, 'joe', 'message %s' % i)
+ print
+ print "And let's get the messages that are waiting for jeff and jenny..."
+ r1 = fetch_pending_messages(self.conn, 'jeff')
+ r2 = fetch_pending_messages(self.conn, 'jenny')
+ print "They are the same?", r1 == r2
+ self.assertEquals(r1, r2)
+ print "Those messages are:"
+ import pprint
+ pprint.pprint(r1)
+ self.conn.delete('ids:chat:', 'msgs:1', 'ids:1', 'seen:joe', 'seen:jeff', 'seen:jenny')
- def test_file_distribution(self):
- import gzip, shutil, tempfile, threading
- self.conn.delete('test:temp-1.txt', 'test:temp-2.txt', 'test:temp-3.txt', 'msgs:test:', 'seen:0', 'seen:source',
- 'ids:test:', 'chat:test:')
+ def test_file_distribution(self):
+ import gzip, shutil, tempfile, threading
+ self.conn.delete('test:temp-1.txt', 'test:temp-2.txt', 'test:temp-3.txt', 'msgs:test:', 'seen:0', 'seen:source',
+ 'ids:test:', 'chat:test:')
- dire = tempfile.mkdtemp()
- try:
- print "Creating some temporary 'log' files..."
- with open(dire + '/temp-1.txt', 'wb') as f:
- f.write('one line\n')
- with open(dire + '/temp-2.txt', 'wb') as f:
- f.write(10000 * 'many lines\n')
- out = gzip.GzipFile(dire + '/temp-3.txt.gz', mode='wb')
- for i in xrange(100000):
- out.write('random line %s\n' % (os.urandom(16).encode('hex'),))
- out.close()
- size = os.stat(dire + '/temp-3.txt.gz').st_size
- print "Done."
- print
- print "Starting up a thread to copy logs to redis..."
- t = threading.Thread(target=copy_logs_to_redis, args=(self.conn, dire, 'test:', 1, size))
- t.setDaemon(1)
- t.start()
+ dire = tempfile.mkdtemp()
+ try:
+ print "Creating some temporary 'log' files..."
+ with open(dire + '/temp-1.txt', 'wb') as f:
+ f.write('one line\n')
+ with open(dire + '/temp-2.txt', 'wb') as f:
+ f.write(10000 * 'many lines\n')
+ out = gzip.GzipFile(dire + '/temp-3.txt.gz', mode='wb')
+ for i in xrange(100000):
+ out.write('random line %s\n' % (os.urandom(16).encode('hex'),))
+ out.close()
+ size = os.stat(dire + '/temp-3.txt.gz').st_size
+ print "Done."
+ print
+ print "Starting up a thread to copy logs to redis..."
+ t = threading.Thread(target=copy_logs_to_redis, args=(self.conn, dire, 'test:', 1, size))
+ t.setDaemon(1)
+ t.start()
- print "Let's pause to let some logs get copied to Redis..."
- time.sleep(.25)
- print
- print "Okay, the logs should be ready. Let's process them!"
+ print "Let's pause to let some logs get copied to Redis..."
+ time.sleep(.25)
+ print
+ print "Okay, the logs should be ready. Let's process them!"
- index = [0]
- counts = [0, 0, 0]
+ index = [0]
+ counts = [0, 0, 0]
- def callback(conn, line):
- if line is None:
- print "Finished with a file %s, linecount: %s" % (index[0], counts[index[0]])
- index[0] += 1
- elif line or line.endswith('\n'):
- counts[index[0]] += 1
+ def callback(conn, line):
+ if line is None:
+ print "Finished with a file %s, linecount: %s" % (index[0], counts[index[0]])
+ index[0] += 1
+ elif line or line.endswith('\n'):
+ counts[index[0]] += 1
- print "Files should have 1, 10000, and 100000 lines"
- process_logs_from_redis(self.conn, '0', callback)
- self.assertEquals(counts, [1, 10000, 100000])
+ print "Files should have 1, 10000, and 100000 lines"
+ process_logs_from_redis(self.conn, '0', callback)
+ self.assertEquals(counts, [1, 10000, 100000])
- print
- print "Let's wait for the copy thread to finish cleaning up..."
- t.join()
- print "Done cleaning out Redis!"
+ print
+ print "Let's wait for the copy thread to finish cleaning up..."
+ t.join()
+ print "Done cleaning out Redis!"
- finally:
- print "Time to clean up files..."
- shutil.rmtree(dire)
- print "Cleaned out files!"
- self.conn.delete('test:temp-1.txt', 'test:temp-2.txt', 'test:temp-3.txt', 'msgs:test:', 'seen:0', 'seen:source',
- 'ids:test:', 'chat:test:')
+ finally:
+ print "Time to clean up files..."
+ shutil.rmtree(dire)
+ print "Cleaned out files!"
+ self.conn.delete('test:temp-1.txt', 'test:temp-2.txt', 'test:temp-3.txt', 'msgs:test:', 'seen:0', 'seen:source',
+ 'ids:test:', 'chat:test:')
if __name__ == '__main__':
- unittest.main()
+ unittest.main()
diff --git a/codes/redis/redis-in-action-py/ch07_listing_source.py b/codes/redis/redis-in-action-py/ch07_listing_source.py
index e3a81d4..0191266 100644
--- a/codes/redis/redis-in-action-py/ch07_listing_source.py
+++ b/codes/redis/redis-in-action-py/ch07_listing_source.py
@@ -26,29 +26,29 @@ WORDS_RE = re.compile("[a-z']{2,}")
def tokenize(content):
- # 将文章包含的单词储存到 Python 集合里面。
- words = set()
- # 遍历文章包含的所有单词。
- for match in WORDS_RE.finditer(content.lower()):
- # 剔除所有位于单词前面或后面的单引号。
- word = match.group().strip("'")
- # 保留那些至少有两个字符长的单词。
- if len(word) >= 2:
- words.add(word)
- # 返回一个集合,集合里面包含了所有被保留并且不是停止词的单词。
- return words - STOP_WORDS
+ # 将文章包含的单词储存到 Python 集合里面。
+ words = set()
+ # 遍历文章包含的所有单词。
+ for match in WORDS_RE.finditer(content.lower()):
+ # 剔除所有位于单词前面或后面的单引号。
+ word = match.group().strip("'")
+ # 保留那些至少有两个字符长的单词。
+ if len(word) >= 2:
+ words.add(word)
+ # 返回一个集合,集合里面包含了所有被保留并且不是停止词的单词。
+ return words - STOP_WORDS
def index_document(conn, docid, content):
- # 对内容进行标记化处理,并取得处理产生的单词。
- words = tokenize(content)
+ # 对内容进行标记化处理,并取得处理产生的单词。
+ words = tokenize(content)
- pipeline = conn.pipeline(True)
- # 将文章添加到正确的反向索引集合里面。
- for word in words:
- pipeline.sadd('idx:' + word, docid)
- # 计算一下,程序为这篇文章添加了多少个独一无二并且不是停止词的单词。
- return len(pipeline.execute())
+ pipeline = conn.pipeline(True)
+ # 将文章添加到正确的反向索引集合里面。
+ for word in words:
+ pipeline.sadd('idx:' + word, docid)
+ # 计算一下,程序为这篇文章添加了多少个独一无二并且不是停止词的单词。
+ return len(pipeline.execute())
#
@@ -57,36 +57,36 @@ def index_document(conn, docid, content):
# 代码清单 7-2
#
def _set_common(conn, method, names, ttl=30, execute=True):
- # 创建一个新的临时标识符。
- id = str(uuid.uuid4())
- # 设置事务流水线,确保每个调用都能获得一致的执行结果。
- pipeline = conn.pipeline(True) if execute else conn
- # 给每个单词加上 'idx:' 前缀。
- names = ['idx:' + name for name in names]
- # 为将要执行的集合操作设置相应的参数。
- getattr(pipeline, method)('idx:' + id, *names)
- # 吩咐 Redis 在将来自动删除这个集合。
- pipeline.expire('idx:' + id, ttl)
- if execute:
- # 实际地执行操作。
- pipeline.execute()
- # 将结果集合的 ID 返回给调用者,以便做进一步的处理。
- return id
+ # 创建一个新的临时标识符。
+ id = str(uuid.uuid4())
+ # 设置事务流水线,确保每个调用都能获得一致的执行结果。
+ pipeline = conn.pipeline(True) if execute else conn
+ # 给每个单词加上 'idx:' 前缀。
+ names = ['idx:' + name for name in names]
+ # 为将要执行的集合操作设置相应的参数。
+ getattr(pipeline, method)('idx:' + id, *names)
+ # 吩咐 Redis 在将来自动删除这个集合。
+ pipeline.expire('idx:' + id, ttl)
+ if execute:
+ # 实际地执行操作。
+ pipeline.execute()
+ # 将结果集合的 ID 返回给调用者,以便做进一步的处理。
+ return id
# 执行交集计算的辅助函数。
def intersect(conn, items, ttl=30, _execute=True):
- return _set_common(conn, 'sinterstore', items, ttl, _execute)
+ return _set_common(conn, 'sinterstore', items, ttl, _execute)
# 执行并集计算的辅助函数。
def union(conn, items, ttl=30, _execute=True):
- return _set_common(conn, 'sunionstore', items, ttl, _execute)
+ return _set_common(conn, 'sunionstore', items, ttl, _execute)
# 执行差集计算的辅助函数。
def difference(conn, items, ttl=30, _execute=True):
- return _set_common(conn, 'sdiffstore', items, ttl, _execute)
+ return _set_common(conn, 'sdiffstore', items, ttl, _execute)
#
@@ -99,47 +99,47 @@ QUERY_RE = re.compile("[+-]?[a-z']{2,}")
def parse(query):
- # 这个集合将用于储存不需要的单词。
- unwanted = set()
- # 这个列表将用于储存需要执行交集计算的单词。
- all = []
- # 这个集合将用于储存目前已发现的同义词。
- current = set()
- # 遍历搜索查询语句中的所有单词。
- for match in QUERY_RE.finditer(query.lower()):
- # 检查单词是否带有 + 号前缀或 - 号前缀。
- word = match.group()
- prefix = word[:1]
- if prefix in '+-':
- word = word[1:]
- else:
- prefix = None
+ # 这个集合将用于储存不需要的单词。
+ unwanted = set()
+ # 这个列表将用于储存需要执行交集计算的单词。
+ all = []
+ # 这个集合将用于储存目前已发现的同义词。
+ current = set()
+ # 遍历搜索查询语句中的所有单词。
+ for match in QUERY_RE.finditer(query.lower()):
+ # 检查单词是否带有 + 号前缀或 - 号前缀。
+ word = match.group()
+ prefix = word[:1]
+ if prefix in '+-':
+ word = word[1:]
+ else:
+ prefix = None
- # 剔除所有位于单词前面或者后面的单引号,并略过所有停止词。
- word = word.strip("'")
- if len(word) < 2 or word in STOP_WORDS:
- continue
+ # 剔除所有位于单词前面或者后面的单引号,并略过所有停止词。
+ word = word.strip("'")
+ if len(word) < 2 or word in STOP_WORDS:
+ continue
- # 如果这是一个不需要的单词,
- # 那么将它添加到储存不需要单词的集合里面。
- if prefix == '-':
- unwanted.add(word)
- continue
+ # 如果这是一个不需要的单词,
+ # 那么将它添加到储存不需要单词的集合里面。
+ if prefix == '-':
+ unwanted.add(word)
+ continue
- # 如果在同义词集合非空的情况下,
- # 遇到了一个不带 + 号前缀的单词,
- # 那么创建一个新的同义词集合。
- if current and not prefix:
- all.append(list(current))
- current = set()
- current.add(word)
+ # 如果在同义词集合非空的情况下,
+ # 遇到了一个不带 + 号前缀的单词,
+ # 那么创建一个新的同义词集合。
+ if current and not prefix:
+ all.append(list(current))
+ current = set()
+ current.add(word)
- # 将正在处理的单词添加到同义词集合里面。
- if current:
- all.append(list(current))
+ # 将正在处理的单词添加到同义词集合里面。
+ if current:
+ all.append(list(current))
- # 把所有剩余的单词都放到最后的交集计算里面进行处理。
- return all, list(unwanted)
+ # 把所有剩余的单词都放到最后的交集计算里面进行处理。
+ return all, list(unwanted)
#
@@ -148,37 +148,37 @@ def parse(query):
# 代码清单 7-4
#
def parse_and_search(conn, query, ttl=30):
- # 对查询语句进行分析。
- all, unwanted = parse(query)
- # 如果查询语句只包含停止词,那么这次搜索没有任何结果。
- if not all:
- return None
+ # 对查询语句进行分析。
+ all, unwanted = parse(query)
+ # 如果查询语句只包含停止词,那么这次搜索没有任何结果。
+ if not all:
+ return None
- to_intersect = []
- # 遍历各个同义词列表。
- for syn in all:
- # 如果同义词列表包含的单词不止一个,那么执行并集计算。
- if len(syn) > 1:
- to_intersect.append(union(conn, syn, ttl=ttl))
- # 如果同义词列表只包含一个单词,那么直接使用这个单词。
- else:
- to_intersect.append(syn[0])
+ to_intersect = []
+ # 遍历各个同义词列表。
+ for syn in all:
+ # 如果同义词列表包含的单词不止一个,那么执行并集计算。
+ if len(syn) > 1:
+ to_intersect.append(union(conn, syn, ttl=ttl))
+ # 如果同义词列表只包含一个单词,那么直接使用这个单词。
+ else:
+ to_intersect.append(syn[0])
- # 如果单词(或者并集计算的结果)有不止一个,那么执行交集计算。
- if len(to_intersect) > 1:
- intersect_result = intersect(conn, to_intersect, ttl=ttl)
- # 如果单词(或者并集计算的结果)只有一个,那么将它用作交集计算的结果。
- else:
- intersect_result = to_intersect[0]
+ # 如果单词(或者并集计算的结果)有不止一个,那么执行交集计算。
+ if len(to_intersect) > 1:
+ intersect_result = intersect(conn, to_intersect, ttl=ttl)
+ # 如果单词(或者并集计算的结果)只有一个,那么将它用作交集计算的结果。
+ else:
+ intersect_result = to_intersect[0]
- # 如果用户给定了不需要的单词,
- # 那么从交集计算结果里面移除包含这些单词的文章,然后返回搜索结果。
- if unwanted:
- unwanted.insert(0, intersect_result)
- return difference(conn, unwanted, ttl=ttl)
+ # 如果用户给定了不需要的单词,
+ # 那么从交集计算结果里面移除包含这些单词的文章,然后返回搜索结果。
+ if unwanted:
+ unwanted.insert(0, intersect_result)
+ return difference(conn, unwanted, ttl=ttl)
- # 如果用户没有给定不需要的单词,那么直接返回交集计算的结果作为搜索的结果。
- return intersect_result
+ # 如果用户没有给定不需要的单词,那么直接返回交集计算的结果作为搜索的结果。
+ return intersect_result
#
@@ -188,37 +188,37 @@ def parse_and_search(conn, query, ttl=30):
#
# 用户可以通过可选的参数来传入已有的搜索结果、指定搜索结果的排序方式,并对结果进行分页。
def search_and_sort(conn, query, id=None, ttl=300, sort="-updated",
- start=0, num=20):
- # 决定基于文章的哪个属性进行排序,以及是进行升序排序还是降序排序。
- desc = sort.startswith('-')
- sort = sort.lstrip('-')
- by = "kb:doc:*->" + sort
- # 告知 Redis ,排序是以数值方式进行还是字母方式进行。
- alpha = sort not in ('updated', 'id', 'created')
+ start=0, num=20):
+ # 决定基于文章的哪个属性进行排序,以及是进行升序排序还是降序排序。
+ desc = sort.startswith('-')
+ sort = sort.lstrip('-')
+ by = "kb:doc:*->" + sort
+ # 告知 Redis ,排序是以数值方式进行还是字母方式进行。
+ alpha = sort not in ('updated', 'id', 'created')
- # 如果用户给定了已有的搜索结果,
- # 并且这个结果仍然存在的话,
- # 那么延长它的生存时间。
- if id and not conn.expire(id, ttl):
- id = None
+ # 如果用户给定了已有的搜索结果,
+ # 并且这个结果仍然存在的话,
+ # 那么延长它的生存时间。
+ if id and not conn.expire(id, ttl):
+ id = None
- # 如果用户没有给定已有的搜索结果,
- # 或者给定的搜索结果已经过期,
- # 那么执行一次新的搜索操作。
- if not id:
- id = parse_and_search(conn, query, ttl=ttl)
+ # 如果用户没有给定已有的搜索结果,
+ # 或者给定的搜索结果已经过期,
+ # 那么执行一次新的搜索操作。
+ if not id:
+ id = parse_and_search(conn, query, ttl=ttl)
- pipeline = conn.pipeline(True)
- # 获取结果集合的元素数量。
- pipeline.scard('idx:' + id)
- # 根据指定属性对结果进行排序,并且只获取用户指定的那一部分结果。
- pipeline.sort('idx:' + id, by=by, alpha=alpha,
- desc=desc, start=start, num=num)
- results = pipeline.execute()
+ pipeline = conn.pipeline(True)
+ # 获取结果集合的元素数量。
+ pipeline.scard('idx:' + id)
+ # 根据指定属性对结果进行排序,并且只获取用户指定的那一部分结果。
+ pipeline.sort('idx:' + id, by=by, alpha=alpha,
+ desc=desc, start=start, num=num)
+ results = pipeline.execute()
- # 返回搜索结果包含的元素数量、搜索结果本身以及搜索结果的 ID ,
- # 其中搜索结果的 ID 可以用于在之后再次获取本次搜索的结果。
- return results[0], results[1], id
+ # 返回搜索结果包含的元素数量、搜索结果本身以及搜索结果的 ID ,
+ # 其中搜索结果的 ID 可以用于在之后再次获取本次搜索的结果。
+ return results[0], results[1], id
#
@@ -229,41 +229,41 @@ def search_and_sort(conn, query, id=None, ttl=300, sort="-updated",
# 和之前一样,函数接受一个已有搜索结果的 ID 作为可选参数,
# 以便在结果仍然可用的情况下,对其进行分页。
def search_and_zsort(conn, query, id=None, ttl=300, update=1, vote=0,
- start=0, num=20, desc=True):
- # 尝试更新已有搜索结果的生存时间。
- if id and not conn.expire(id, ttl):
- id = None
+ start=0, num=20, desc=True):
+ # 尝试更新已有搜索结果的生存时间。
+ if id and not conn.expire(id, ttl):
+ id = None
- # 如果传入的结果已经过期,
- # 或者这是函数第一次进行搜索,
- # 那么执行标准的集合搜索操作。
- if not id:
- id = parse_and_search(conn, query, ttl=ttl)
+ # 如果传入的结果已经过期,
+ # 或者这是函数第一次进行搜索,
+ # 那么执行标准的集合搜索操作。
+ if not id:
+ id = parse_and_search(conn, query, ttl=ttl)
- scored_search = {
- # 函数在计算并集的时候也会用到传入的 ID 键,
- # 但这个键不会被用作排序权重(weight)。
- id: 0,
- # 对文章评分进行调整以平衡更新时间和投票数量。
- # 根据待排序数据的需要,投票数量可以被调整为 1 、10 、100 ,甚至更高。
- 'sort:update': update,
- 'sort:votes': vote
- }
- # 使用代码清单 7-7 定义的辅助函数执行交集计算。
- id = zintersect(conn, scored_search, ttl)
+ scored_search = {
+ # 函数在计算并集的时候也会用到传入的 ID 键,
+ # 但这个键不会被用作排序权重(weight)。
+ id: 0,
+ # 对文章评分进行调整以平衡更新时间和投票数量。
+ # 根据待排序数据的需要,投票数量可以被调整为 1 、10 、100 ,甚至更高。
+ 'sort:update': update,
+ 'sort:votes': vote
+ }
+ # 使用代码清单 7-7 定义的辅助函数执行交集计算。
+ id = zintersect(conn, scored_search, ttl)
- pipeline = conn.pipeline(True)
- # 获取结果有序集合的大小。
- pipeline.zcard('idx:' + id)
- # 从搜索结果里面取出一页(page)。
- if desc:
- pipeline.zrevrange('idx:' + id, start, start + num - 1)
- else:
- pipeline.zrange('idx:' + id, start, start + num - 1)
- results = pipeline.execute()
+ pipeline = conn.pipeline(True)
+ # 获取结果有序集合的大小。
+ pipeline.zcard('idx:' + id)
+ # 从搜索结果里面取出一页(page)。
+ if desc:
+ pipeline.zrevrange('idx:' + id, start, start + num - 1)
+ else:
+ pipeline.zrange('idx:' + id, start, start + num - 1)
+ results = pipeline.execute()
- # 返回搜索结果,以及分页用的 ID 值。
- return results[0], results[1], id
+ # 返回搜索结果,以及分页用的 ID 值。
+ return results[0], results[1], id
#
@@ -272,34 +272,34 @@ def search_and_zsort(conn, query, id=None, ttl=300, update=1, vote=0,
# 代码清单 7-7
#
def _zset_common(conn, method, scores, ttl=30, **kw):
- # 创建一个新的临时标识符。
- id = str(uuid.uuid4())
- # 调用者可以通过传递参数来决定是否使用事务流水线。
- execute = kw.pop('_execute', True)
- # 设置事务流水线,保证每个单独的调用都有一致的结果。
- pipeline = conn.pipeline(True) if execute else conn
- # 为输入的键添加 ‘idx:’ 前缀。
- for key in scores.keys():
- scores['idx:' + key] = scores.pop(key)
- # 为将要被执行的操作设置好相应的参数。
- getattr(pipeline, method)('idx:' + id, scores, **kw)
- # 为计算结果有序集合设置过期时间。
- pipeline.expire('idx:' + id, ttl)
- # 除非调用者明确指示要延迟执行操作,否则实际地执行计算操作。
- if execute:
- pipeline.execute()
- # 将计算结果的 ID 返回给调用者,以便做进一步的处理。
- return id
+ # 创建一个新的临时标识符。
+ id = str(uuid.uuid4())
+ # 调用者可以通过传递参数来决定是否使用事务流水线。
+ execute = kw.pop('_execute', True)
+ # 设置事务流水线,保证每个单独的调用都有一致的结果。
+ pipeline = conn.pipeline(True) if execute else conn
+ # 为输入的键添加 ‘idx:’ 前缀。
+ for key in scores.keys():
+ scores['idx:' + key] = scores.pop(key)
+ # 为将要被执行的操作设置好相应的参数。
+ getattr(pipeline, method)('idx:' + id, scores, **kw)
+ # 为计算结果有序集合设置过期时间。
+ pipeline.expire('idx:' + id, ttl)
+ # 除非调用者明确指示要延迟执行操作,否则实际地执行计算操作。
+ if execute:
+ pipeline.execute()
+ # 将计算结果的 ID 返回给调用者,以便做进一步的处理。
+ return id
# 对有序集合执行交集计算的辅助函数。
def zintersect(conn, items, ttl=30, **kw):
- return _zset_common(conn, 'zinterstore', dict(items), ttl, **kw)
+ return _zset_common(conn, 'zinterstore', dict(items), ttl, **kw)
# 对有序集合执行并集计算的辅助函数。
def zunion(conn, items, ttl=30, **kw):
- return _zset_common(conn, 'zunionstore', dict(items), ttl, **kw)
+ return _zset_common(conn, 'zunionstore', dict(items), ttl, **kw)
#
@@ -308,38 +308,38 @@ def zunion(conn, items, ttl=30, **kw):
# 代码清单 7-8
#
def string_to_score(string, ignore_case=False):
- # 用户可以通过参数来决定是否以大小写无关的方式建立前缀索引。
- if ignore_case:
- string = string.lower()
+ # 用户可以通过参数来决定是否以大小写无关的方式建立前缀索引。
+ if ignore_case:
+ string = string.lower()
- # 将字符串的前 6 个字符转换为相应的数字值,
- # 比如把空字符转换为 0 、制表符(tab)转换为 9 、大写 A 转换为 65 ,
- # 诸如此类。
- pieces = map(ord, string[:6])
- # 为长度不足 6 个字符的字符串添加占位符,以此来表示这是一个短字符。
- while len(pieces) < 6:
- pieces.append(-1)
+ # 将字符串的前 6 个字符转换为相应的数字值,
+ # 比如把空字符转换为 0 、制表符(tab)转换为 9 、大写 A 转换为 65 ,
+ # 诸如此类。
+ pieces = map(ord, string[:6])
+ # 为长度不足 6 个字符的字符串添加占位符,以此来表示这是一个短字符。
+ while len(pieces) < 6:
+ pieces.append(-1)
- score = 0
- # 对字符串进行转换得出的每个值都会被计算到分值里面,
- # 并且程序处理空字符的方式和处理占位符的方式并不相同。
- for piece in pieces:
- score = score * 257 + piece + 1
+ score = 0
+ # 对字符串进行转换得出的每个值都会被计算到分值里面,
+ # 并且程序处理空字符的方式和处理占位符的方式并不相同。
+ for piece in pieces:
+ score = score * 257 + piece + 1
- # 通过多使用一个二进制位,
- # 程序可以表明字符串是否正好为 6 个字符长,
- # 这样它就可以正确地区分出 “robber” 和 “robbers” ,
- # 尽管这对于区分 “robbers” 和 “robbery” 并无帮助。
- return score * 2 + (len(string) > 6)
+ # 通过多使用一个二进制位,
+ # 程序可以表明字符串是否正好为 6 个字符长,
+ # 这样它就可以正确地区分出 “robber” 和 “robbers” ,
+ # 尽管这对于区分 “robbers” 和 “robbery” 并无帮助。
+ return score * 2 + (len(string) > 6)
#
def to_char_map(set):
- out = {}
- for pos, val in enumerate(sorted(set)):
- out[val] = pos - 1
- return out
+ out = {}
+ for pos, val in enumerate(sorted(set)):
+ out[val] = pos - 1
+ return out
LOWER = to_char_map(set([-1]) | set(xrange(ord('a'), ord('z') + 1)))
@@ -349,31 +349,31 @@ ALPHA_NUMERIC = to_char_map(set(LOWER_NUMERIC) | set(ALPHA))
def string_to_score_generic(string, mapping):
- length = int(52 / math.log(len(mapping), 2)) # A
+ length = int(52 / math.log(len(mapping), 2)) # A
- pieces = map(ord, string[:length]) # B
- while len(pieces) < length: # C
- pieces.append(-1) # C
+ pieces = map(ord, string[:length]) # B
+ while len(pieces) < length: # C
+ pieces.append(-1) # C
- score = 0
- for piece in pieces: # D
- value = mapping[piece] # D
- score = score * len(mapping) + value + 1 # D
+ score = 0
+ for piece in pieces: # D
+ value = mapping[piece] # D
+ score = score * len(mapping) + value + 1 # D
- return score * 2 + (len(string) > length) # E
+ return score * 2 + (len(string) > length) # E
#
def zadd_string(conn, name, *args, **kwargs):
- pieces = list(args) # 为了进行之后的修改,
- for piece in kwargs.iteritems(): # 对传入的不同类型的参数进行合并(combine)
- pieces.extend(piece) #
+ pieces = list(args) # 为了进行之后的修改,
+ for piece in kwargs.iteritems(): # 对传入的不同类型的参数进行合并(combine)
+ pieces.extend(piece) #
- for i, v in enumerate(pieces):
- if i & 1: # 将字符串格式的分值转换为整数分值
- pieces[i] = string_to_score(v) #
+ for i, v in enumerate(pieces):
+ if i & 1: # 将字符串格式的分值转换为整数分值
+ pieces[i] = string_to_score(v) #
- return conn.zadd(name, *pieces) # 调用已有的 ZADD 方法
+ return conn.zadd(name, *pieces) # 调用已有的 ZADD 方法
#
@@ -382,14 +382,14 @@ def zadd_string(conn, name, *args, **kwargs):
# 代码清单 7-9
#
def cpc_to_ecpm(views, clicks, cpc):
- return 1000. * cpc * clicks / views
+ return 1000. * cpc * clicks / views
def cpa_to_ecpm(views, actions, cpa):
- # 因为点击通过率是由点击次数除以展示次数计算出的,
- # 而动作的执行概率则是由动作执行次数除以点击次数计算出的,
- # 所以这两个概率相乘的结果等于动作执行次数除以展示次数。
- return 1000. * cpa * actions / views
+ # 因为点击通过率是由点击次数除以展示次数计算出的,
+ # 而动作的执行概率则是由动作执行次数除以点击次数计算出的,
+ # 所以这两个概率相乘的结果等于动作执行次数除以展示次数。
+ return 1000. * cpa * actions / views
#
@@ -398,38 +398,38 @@ def cpa_to_ecpm(views, actions, cpa):
# 代码清单 7-10
#
TO_ECPM = {
- 'cpc': cpc_to_ecpm,
- 'cpa': cpa_to_ecpm,
- 'cpm': lambda *args: args[-1],
+ 'cpc': cpc_to_ecpm,
+ 'cpa': cpa_to_ecpm,
+ 'cpm': lambda *args: args[-1],
}
def index_ad(conn, id, locations, content, type, value):
- # 设置流水线,使得程序可以在一次通信往返里面完成整个索引操作。
- pipeline = conn.pipeline(True)
+ # 设置流水线,使得程序可以在一次通信往返里面完成整个索引操作。
+ pipeline = conn.pipeline(True)
- for location in locations:
- # 为了进行定向操作,把广告 ID 添加到所有相关的位置集合里面。
- pipeline.sadd('idx:req:' + location, id)
+ for location in locations:
+ # 为了进行定向操作,把广告 ID 添加到所有相关的位置集合里面。
+ pipeline.sadd('idx:req:' + location, id)
- words = tokenize(content)
- # 对广告包含的单词进行索引。
- for word in tokenize(content):
- pipeline.zadd('idx:' + word, id, 0)
+ words = tokenize(content)
+ # 对广告包含的单词进行索引。
+ for word in tokenize(content):
+ pipeline.zadd('idx:' + word, id, 0)
- # 为了评估新广告的效果,
- # 程序会使用字典来储存广告每千次展示的平均点击次数或平均动作执行次数。
- rvalue = TO_ECPM[type](
- 1000, AVERAGE_PER_1K.get(type, 1), value)
- # 记录这个广告的类型。
- pipeline.hset('type:', id, type)
- # 将广告的 eCPM 添加到一个记录了所有广告的 eCPM 的有序集合里面。
- pipeline.zadd('idx:ad:value:', id, rvalue)
- # 将广告的基本价格(base value)添加到一个记录了所有广告的基本价格的有序集合里面。
- pipeline.zadd('ad:base_value:', id, value)
- # 把能够对广告进行定向的单词全部记录起来。
- pipeline.sadd('terms:' + id, *list(words))
- pipeline.execute()
+ # 为了评估新广告的效果,
+ # 程序会使用字典来储存广告每千次展示的平均点击次数或平均动作执行次数。
+ rvalue = TO_ECPM[type](
+ 1000, AVERAGE_PER_1K.get(type, 1), value)
+ # 记录这个广告的类型。
+ pipeline.hset('type:', id, type)
+ # 将广告的 eCPM 添加到一个记录了所有广告的 eCPM 的有序集合里面。
+ pipeline.zadd('idx:ad:value:', id, rvalue)
+ # 将广告的基本价格(base value)添加到一个记录了所有广告的基本价格的有序集合里面。
+ pipeline.zadd('ad:base_value:', id, value)
+ # 把能够对广告进行定向的单词全部记录起来。
+ pipeline.sadd('terms:' + id, *list(words))
+ pipeline.execute()
#
@@ -438,29 +438,29 @@ def index_ad(conn, id, locations, content, type, value):
# 代码清单 7-11
#
def target_ads(conn, locations, content):
- pipeline = conn.pipeline(True)
- # 根据用户传入的位置定向参数,找到所有位于该位置的广告,以及这些广告的 eCPM 。
- matched_ads, base_ecpm = match_location(pipeline, locations)
- # 基于匹配的内容计算附加值。
- words, targeted_ads = finish_scoring(
- pipeline, matched_ads, base_ecpm, content)
+ pipeline = conn.pipeline(True)
+ # 根据用户传入的位置定向参数,找到所有位于该位置的广告,以及这些广告的 eCPM 。
+ matched_ads, base_ecpm = match_location(pipeline, locations)
+ # 基于匹配的内容计算附加值。
+ words, targeted_ads = finish_scoring(
+ pipeline, matched_ads, base_ecpm, content)
- # 获取一个 ID ,它可以用于汇报并记录这个被定向的广告。
- pipeline.incr('ads:served:')
- # 找到 eCPM 最高的广告,并获取这个广告的 ID 。
- pipeline.zrevrange('idx:' + targeted_ads, 0, 0)
- target_id, targeted_ad = pipeline.execute()[-2:]
+ # 获取一个 ID ,它可以用于汇报并记录这个被定向的广告。
+ pipeline.incr('ads:served:')
+ # 找到 eCPM 最高的广告,并获取这个广告的 ID 。
+ pipeline.zrevrange('idx:' + targeted_ads, 0, 0)
+ target_id, targeted_ad = pipeline.execute()[-2:]
- # 如果没有任何广告与目标位置相匹配,那么返回空值。
- if not targeted_ad:
- return None, None
+ # 如果没有任何广告与目标位置相匹配,那么返回空值。
+ if not targeted_ad:
+ return None, None
- ad_id = targeted_ad[0]
- # 记录一系列定向操作的执行结果,作为学习用户行为的其中一个步骤。
- record_targeting_result(conn, target_id, ad_id, words)
+ ad_id = targeted_ad[0]
+ # 记录一系列定向操作的执行结果,作为学习用户行为的其中一个步骤。
+ record_targeting_result(conn, target_id, ad_id, words)
- # 向调用者返回记录本次定向操作相关信息的 ID ,以及被选中的广告的 ID 。
- return target_id, ad_id
+ # 向调用者返回记录本次定向操作相关信息的 ID ,以及被选中的广告的 ID 。
+ return target_id, ad_id
#
@@ -469,15 +469,15 @@ def target_ads(conn, locations, content):
# 代码清单 7-12
#
def match_location(pipe, locations):
- # 根据给定的位置,找出所有需要执行并集操作的集合键。
- required = ['req:' + loc for loc in locations]
- # 找出与指定地区相匹配的广告,并将它们储存到集合里面。
- matched_ads = union(pipe, required, ttl=300, _execute=False)
- # 找到储存着所有被匹配广告的集合,
- # 以及储存着所有被匹配广告的基本 eCPM 的有序集合,
- # 然后返回它们的 ID 。
- return matched_ads, zintersect(pipe,
- {matched_ads: 0, 'ad:value:': 1}, _execute=False)
+ # 根据给定的位置,找出所有需要执行并集操作的集合键。
+ required = ['req:' + loc for loc in locations]
+ # 找出与指定地区相匹配的广告,并将它们储存到集合里面。
+ matched_ads = union(pipe, required, ttl=300, _execute=False)
+ # 找到储存着所有被匹配广告的集合,
+ # 以及储存着所有被匹配广告的基本 eCPM 的有序集合,
+ # 然后返回它们的 ID 。
+ return matched_ads, zintersect(pipe,
+ {matched_ads: 0, 'ad:value:': 1}, _execute=False)
#
@@ -486,27 +486,27 @@ def match_location(pipe, locations):
# 代码清单 7-13
#
def finish_scoring(pipe, matched, base, content):
- bonus_ecpm = {}
- # 对内容进行标记化处理,以便与广告进行匹配。
- words = tokenize(content)
- for word in words:
- # 找出那些既位于定向位置之内,又拥有页面内容其中一个单词的广告。
- word_bonus = zintersect(
- pipe, {matched: 0, word: 1}, _execute=False)
- bonus_ecpm[word_bonus] = 1
+ bonus_ecpm = {}
+ # 对内容进行标记化处理,以便与广告进行匹配。
+ words = tokenize(content)
+ for word in words:
+ # 找出那些既位于定向位置之内,又拥有页面内容其中一个单词的广告。
+ word_bonus = zintersect(
+ pipe, {matched: 0, word: 1}, _execute=False)
+ bonus_ecpm[word_bonus] = 1
- if bonus_ecpm:
- # 计算每个广告的最小 eCPM 附加值和最大 eCPM 附加值。
- minimum = zunion(
- pipe, bonus_ecpm, aggregate='MIN', _execute=False)
- maximum = zunion(
- pipe, bonus_ecpm, aggregate='MAX', _execute=False)
+ if bonus_ecpm:
+ # 计算每个广告的最小 eCPM 附加值和最大 eCPM 附加值。
+ minimum = zunion(
+ pipe, bonus_ecpm, aggregate='MIN', _execute=False)
+ maximum = zunion(
+ pipe, bonus_ecpm, aggregate='MAX', _execute=False)
- # 将广告的基本价格、最小 eCPM 附加值的一半以及最大 eCPM 附加值的一半这三者相加起来。
- return words, zunion(
- pipe, {base: 1, minimum: .5, maximum: .5}, _execute=False)
- # 如果页面内容中没有出现任何可匹配的单词,那么返回广告的基本 eCPM 。
- return words, base
+ # 将广告的基本价格、最小 eCPM 附加值的一半以及最大 eCPM 附加值的一半这三者相加起来。
+ return words, zunion(
+ pipe, {base: 1, minimum: .5, maximum: .5}, _execute=False)
+ # 如果页面内容中没有出现任何可匹配的单词,那么返回广告的基本 eCPM 。
+ return words, base
#
@@ -515,65 +515,65 @@ def finish_scoring(pipe, matched, base, content):
# 代码清单 7-14
#
def record_targeting_result(conn, target_id, ad_id, words):
- pipeline = conn.pipeline(True)
+ pipeline = conn.pipeline(True)
- # 找出内容与广告之间相匹配的那些单词。
- terms = conn.smembers('terms:' + ad_id)
- matched = list(words & terms)
- if matched:
- matched_key = 'terms:matched:%s' % target_id
- # 如果有相匹配的单词出现,那么把它们记录起来,并设置 15 分钟的生存时间。
- pipeline.sadd(matched_key, *matched)
- pipeline.expire(matched_key, 900)
+ # 找出内容与广告之间相匹配的那些单词。
+ terms = conn.smembers('terms:' + ad_id)
+ matched = list(words & terms)
+ if matched:
+ matched_key = 'terms:matched:%s' % target_id
+ # 如果有相匹配的单词出现,那么把它们记录起来,并设置 15 分钟的生存时间。
+ pipeline.sadd(matched_key, *matched)
+ pipeline.expire(matched_key, 900)
- # 为每种类型的广告分别记录它们的展示次数。
- type = conn.hget('type:', ad_id)
- pipeline.incr('type:%s:views:' % type)
- # 对广告以及广告包含的单词的展示信息进行记录。
- for word in matched:
- pipeline.zincrby('views:%s' % ad_id, word)
- pipeline.zincrby('views:%s' % ad_id, '')
+ # 为每种类型的广告分别记录它们的展示次数。
+ type = conn.hget('type:', ad_id)
+ pipeline.incr('type:%s:views:' % type)
+ # 对广告以及广告包含的单词的展示信息进行记录。
+ for word in matched:
+ pipeline.zincrby('views:%s' % ad_id, word)
+ pipeline.zincrby('views:%s' % ad_id, '')
- # 广告每展示 100 次,就更新一次它的 eCPM 。
- if not pipeline.execute()[-1] % 100:
- update_cpms(conn, ad_id)
+ # 广告每展示 100 次,就更新一次它的 eCPM 。
+ if not pipeline.execute()[-1] % 100:
+ update_cpms(conn, ad_id)
- #
+ #
# 代码清单 7-15
#
def record_click(conn, target_id, ad_id, action=False):
- pipeline = conn.pipeline(True)
- click_key = 'clicks:%s' % ad_id
+ pipeline = conn.pipeline(True)
+ click_key = 'clicks:%s' % ad_id
- match_key = 'terms:matched:%s' % target_id
+ match_key = 'terms:matched:%s' % target_id
- type = conn.hget('type:', ad_id)
- # 如果这是一个按动作计费的广告,
- # 并且被匹配的单词仍然存在,
- # 那么刷新这些单词的过期时间。
- if type == 'cpa':
- pipeline.expire(match_key, 900)
- if action:
- # 记录动作信息,而不是点击信息。
- click_key = 'actions:%s' % ad_id
+ type = conn.hget('type:', ad_id)
+ # 如果这是一个按动作计费的广告,
+ # 并且被匹配的单词仍然存在,
+ # 那么刷新这些单词的过期时间。
+ if type == 'cpa':
+ pipeline.expire(match_key, 900)
+ if action:
+ # 记录动作信息,而不是点击信息。
+ click_key = 'actions:%s' % ad_id
- if action and type == 'cpa':
- # 根据广告的类型,维持一个全局的点击/动作计数器。
- pipeline.incr('type:%s:actions:' % type)
- else:
- pipeline.incr('type:%s:clicks:' % type)
+ if action and type == 'cpa':
+ # 根据广告的类型,维持一个全局的点击/动作计数器。
+ pipeline.incr('type:%s:actions:' % type)
+ else:
+ pipeline.incr('type:%s:clicks:' % type)
- # 为广告以及所有被定向至该广告的单词记录下本次点击(或动作)。
- matched = list(conn.smembers(match_key))
- matched.append('')
- for word in matched:
- pipeline.zincrby(click_key, word)
- pipeline.execute()
+ # 为广告以及所有被定向至该广告的单词记录下本次点击(或动作)。
+ matched = list(conn.smembers(match_key))
+ matched.append('')
+ for word in matched:
+ pipeline.zincrby(click_key, word)
+ pipeline.execute()
- # 对广告中出现的所有单词的 eCPM 进行更新。
- update_cpms(conn, ad_id)
+ # 对广告中出现的所有单词的 eCPM 进行更新。
+ update_cpms(conn, ad_id)
#
@@ -582,67 +582,67 @@ def record_click(conn, target_id, ad_id, action=False):
# 代码清单 7-16
#
def update_cpms(conn, ad_id):
- pipeline = conn.pipeline(True)
- # 获取广告的类型和价格,以及广告包含的所有单词。
- pipeline.hget('type:', ad_id)
- pipeline.zscore('ad:base_value:', ad_id)
- pipeline.smembers('terms:' + ad_id)
- type, base_value, words = pipeline.execute()
+ pipeline = conn.pipeline(True)
+ # 获取广告的类型和价格,以及广告包含的所有单词。
+ pipeline.hget('type:', ad_id)
+ pipeline.zscore('ad:base_value:', ad_id)
+ pipeline.smembers('terms:' + ad_id)
+ type, base_value, words = pipeline.execute()
- # 判断广告的 eCPM 应该基于点击次数进行计算还是基于动作执行次数进行计算。
- which = 'clicks'
- if type == 'cpa':
- which = 'actions'
+ # 判断广告的 eCPM 应该基于点击次数进行计算还是基于动作执行次数进行计算。
+ which = 'clicks'
+ if type == 'cpa':
+ which = 'actions'
- # 根据广告的类型,
- # 获取这类广告的展示次数和点击次数(或者动作执行次数)。
- pipeline.get('type:%s:views:' % type)
- pipeline.get('type:%s:%s' % (type, which))
- type_views, type_clicks = pipeline.execute()
- # 将广告的点击率或动作执行率重新写入到全局字典里面。
- AVERAGE_PER_1K[type] = (
- 1000. * int(type_clicks or '1') / int(type_views or '1'))
+ # 根据广告的类型,
+ # 获取这类广告的展示次数和点击次数(或者动作执行次数)。
+ pipeline.get('type:%s:views:' % type)
+ pipeline.get('type:%s:%s' % (type, which))
+ type_views, type_clicks = pipeline.execute()
+ # 将广告的点击率或动作执行率重新写入到全局字典里面。
+ AVERAGE_PER_1K[type] = (
+ 1000. * int(type_clicks or '1') / int(type_views or '1'))
- # 如果正在处理的是一个 CPM 广告,
- # 那么它的 eCPM 已经更新完毕,
- # 无需再做其他处理。
- if type == 'cpm':
- return
+ # 如果正在处理的是一个 CPM 广告,
+ # 那么它的 eCPM 已经更新完毕,
+ # 无需再做其他处理。
+ if type == 'cpm':
+ return
- view_key = 'views:%s' % ad_id
- click_key = '%s:%s' % (which, ad_id)
+ view_key = 'views:%s' % ad_id
+ click_key = '%s:%s' % (which, ad_id)
- to_ecpm = TO_ECPM[type]
+ to_ecpm = TO_ECPM[type]
- # 获取广告的展示次数,以及广告的点击次数(或者动作执行次数)。
- pipeline.zscore(view_key, '')
- pipeline.zscore(click_key, '')
- ad_views, ad_clicks = pipeline.execute()
- # 如果广告还没有被点击过,那么使用已有的 eCPM 。
- if (ad_clicks or 0) < 1:
- ad_ecpm = conn.zscore('idx:ad:value:', ad_id)
- else:
- # 计算广告的 eCPM 并更新它的价格。
- ad_ecpm = to_ecpm(ad_views or 1, ad_clicks or 0, base_value)
- pipeline.zadd('idx:ad:value:', ad_id, ad_ecpm)
+ # 获取广告的展示次数,以及广告的点击次数(或者动作执行次数)。
+ pipeline.zscore(view_key, '')
+ pipeline.zscore(click_key, '')
+ ad_views, ad_clicks = pipeline.execute()
+ # 如果广告还没有被点击过,那么使用已有的 eCPM 。
+ if (ad_clicks or 0) < 1:
+ ad_ecpm = conn.zscore('idx:ad:value:', ad_id)
+ else:
+ # 计算广告的 eCPM 并更新它的价格。
+ ad_ecpm = to_ecpm(ad_views or 1, ad_clicks or 0, base_value)
+ pipeline.zadd('idx:ad:value:', ad_id, ad_ecpm)
- for word in words:
- # 获取单词的展示次数和点击次数(或者动作执行次数)。
- pipeline.zscore(view_key, word)
- pipeline.zscore(click_key, word)
- views, clicks = pipeline.execute()[-2:]
+ for word in words:
+ # 获取单词的展示次数和点击次数(或者动作执行次数)。
+ pipeline.zscore(view_key, word)
+ pipeline.zscore(click_key, word)
+ views, clicks = pipeline.execute()[-2:]
- # 如果广告还未被点击过,那么不对 eCPM 进行更新。
- if (clicks or 0) < 1:
- continue
+ # 如果广告还未被点击过,那么不对 eCPM 进行更新。
+ if (clicks or 0) < 1:
+ continue
- # 计算单词的 eCPM 。
- word_ecpm = to_ecpm(views or 1, clicks or 0, base_value)
- # 计算单词的附加值。
- bonus = word_ecpm - ad_ecpm
- # 将单词的附加值重新写入到为广告包含的每个单词分别记录附加值的有序集合里面。
- pipeline.zadd('idx:' + word, ad_id, bonus)
- pipeline.execute()
+ # 计算单词的 eCPM 。
+ word_ecpm = to_ecpm(views or 1, clicks or 0, base_value)
+ # 计算单词的附加值。
+ bonus = word_ecpm - ad_ecpm
+ # 将单词的附加值重新写入到为广告包含的每个单词分别记录附加值的有序集合里面。
+ pipeline.zadd('idx:' + word, ad_id, bonus)
+ pipeline.execute()
#
@@ -651,20 +651,20 @@ def update_cpms(conn, ad_id):
# 代码清单 7-17
#
def add_job(conn, job_id, required_skills):
- # 把职位所需的技能全部添加到职位对应的集合里面。
- conn.sadd('job:' + job_id, *required_skills)
+ # 把职位所需的技能全部添加到职位对应的集合里面。
+ conn.sadd('job:' + job_id, *required_skills)
def is_qualified(conn, job_id, candidate_skills):
- temp = str(uuid.uuid4())
- pipeline = conn.pipeline(True)
- # 把求职者拥有的技能全部添加到一个临时集合里面,并设置过期时间。
- pipeline.sadd(temp, *candidate_skills)
- pipeline.expire(temp, 5)
- # 找出职位所需技能当中,求职者不具备的那些技能,并将它们记录到结果集合里面。
- pipeline.sdiff('job:' + job_id, temp)
- # 如果求职者具备职位所需的全部技能,那么返回 True 。
- return not pipeline.execute()[-1]
+ temp = str(uuid.uuid4())
+ pipeline = conn.pipeline(True)
+ # 把求职者拥有的技能全部添加到一个临时集合里面,并设置过期时间。
+ pipeline.sadd(temp, *candidate_skills)
+ pipeline.expire(temp, 5)
+ # 找出职位所需技能当中,求职者不具备的那些技能,并将它们记录到结果集合里面。
+ pipeline.sdiff('job:' + job_id, temp)
+ # 如果求职者具备职位所需的全部技能,那么返回 True 。
+ return not pipeline.execute()[-1]
#
@@ -673,13 +673,13 @@ def is_qualified(conn, job_id, candidate_skills):
# 代码清单 7-18
#
def index_job(conn, job_id, skills):
- pipeline = conn.pipeline(True)
- for skill in skills:
- # 将职位 ID 添加到相应的技能集合里面。
- pipeline.sadd('idx:skill:' + skill, job_id)
- # 将职位所需技能的数量添加到记录了所有职位所需技能数量的有序集合里面。
- pipeline.zadd('idx:jobs:req', job_id, len(set(skills)))
- pipeline.execute()
+ pipeline = conn.pipeline(True)
+ for skill in skills:
+ # 将职位 ID 添加到相应的技能集合里面。
+ pipeline.sadd('idx:skill:' + skill, job_id)
+ # 将职位所需技能的数量添加到记录了所有职位所需技能数量的有序集合里面。
+ pipeline.zadd('idx:jobs:req', job_id, len(set(skills)))
+ pipeline.execute()
#
@@ -688,19 +688,19 @@ def index_job(conn, job_id, skills):
# 代码清单 7-19
#
def find_jobs(conn, candidate_skills):
- # 设置好用于计算职位得分的字典。
- skills = {}
- for skill in set(candidate_skills):
- skills['skill:' + skill] = 1
+ # 设置好用于计算职位得分的字典。
+ skills = {}
+ for skill in set(candidate_skills):
+ skills['skill:' + skill] = 1
- # 计算求职者对于每个职位的得分。
- job_scores = zunion(conn, skills)
- # 计算出求职者能够胜任以及不能够胜任的职位。
- final_result = zintersect(
- conn, {job_scores: -1, 'jobs:req': 1})
+ # 计算求职者对于每个职位的得分。
+ job_scores = zunion(conn, skills)
+ # 计算出求职者能够胜任以及不能够胜任的职位。
+ final_result = zintersect(
+ conn, {job_scores: -1, 'jobs:req': 1})
- # 返回求职者能够胜任的那些职位。
- return conn.zrangebyscore('idx:' + final_result, 0, 0)
+ # 返回求职者能够胜任的那些职位。
+ return conn.zrangebyscore('idx:' + final_result, 0, 0)
#
@@ -710,212 +710,212 @@ SKILL_LEVEL_LIMIT = 2
def index_job_levels(conn, job_id, skill_levels):
- total_skills = len(set(skill for skill, level in skill_levels))
- pipeline = conn.pipeline(True)
- for skill, level in skill_levels:
- level = min(level, SKILL_LEVEL_LIMIT)
- for wlevel in xrange(level, SKILL_LEVEL_LIMIT + 1):
- pipeline.sadd('idx:skill:%s:%s' % (skill, wlevel), job_id)
- pipeline.zadd('idx:jobs:req', job_id, total_skills)
- pipeline.execute()
+ total_skills = len(set(skill for skill, level in skill_levels))
+ pipeline = conn.pipeline(True)
+ for skill, level in skill_levels:
+ level = min(level, SKILL_LEVEL_LIMIT)
+ for wlevel in xrange(level, SKILL_LEVEL_LIMIT + 1):
+ pipeline.sadd('idx:skill:%s:%s' % (skill, wlevel), job_id)
+ pipeline.zadd('idx:jobs:req', job_id, total_skills)
+ pipeline.execute()
def search_job_levels(conn, skill_levels):
- skills = {}
- for skill, level in skill_levels:
- level = min(level, SKILL_LEVEL_LIMIT)
- for wlevel in xrange(level, SKILL_LEVEL_LIMIT + 1):
- skills['skill:%s:%s' % (skill, wlevel)] = 1
+ skills = {}
+ for skill, level in skill_levels:
+ level = min(level, SKILL_LEVEL_LIMIT)
+ for wlevel in xrange(level, SKILL_LEVEL_LIMIT + 1):
+ skills['skill:%s:%s' % (skill, wlevel)] = 1
- job_scores = zunion(conn, skills)
- final_result = zintersect(conn, {job_scores: -1, 'jobs:req': 1})
+ job_scores = zunion(conn, skills)
+ final_result = zintersect(conn, {job_scores: -1, 'jobs:req': 1})
- return conn.zrangebyscore('idx:' + final_result, 0, 0)
+ return conn.zrangebyscore('idx:' + final_result, 0, 0)
def index_job_years(conn, job_id, skill_years):
- total_skills = len(set(skill for skill, level in skill_years))
- pipeline = conn.pipeline(True)
- for skill, years in skill_years:
- pipeline.zadd(
- 'idx:skill:%s:years' % skill, job_id, max(years, 0))
- pipeline.sadd('idx:jobs:all', job_id)
- pipeline.zadd('idx:jobs:req', job_id, total_skills)
+ total_skills = len(set(skill for skill, level in skill_years))
+ pipeline = conn.pipeline(True)
+ for skill, years in skill_years:
+ pipeline.zadd(
+ 'idx:skill:%s:years' % skill, job_id, max(years, 0))
+ pipeline.sadd('idx:jobs:all', job_id)
+ pipeline.zadd('idx:jobs:req', job_id, total_skills)
def search_job_years(conn, skill_years):
- skill_years = dict(skill_years)
- pipeline = conn.pipeline(True)
+ skill_years = dict(skill_years)
+ pipeline = conn.pipeline(True)
- union = []
- for skill, years in skill_years.iteritems():
- sub_result = zintersect(pipeline,
- {'jobs:all': -years, 'skill:%s:years' % skill: 1}, _execute=False)
- pipeline.zremrangebyscore('idx:' + sub_result, '(0', 'inf')
- union.append(
- zintersect(pipeline, {'jobs:all': 1, sub_result: 0}), _execute=False)
+ union = []
+ for skill, years in skill_years.iteritems():
+ sub_result = zintersect(pipeline,
+ {'jobs:all': -years, 'skill:%s:years' % skill: 1}, _execute=False)
+ pipeline.zremrangebyscore('idx:' + sub_result, '(0', 'inf')
+ union.append(
+ zintersect(pipeline, {'jobs:all': 1, sub_result: 0}), _execute=False)
- job_scores = zunion(pipeline, dict((key, 1) for key in union), _execute=False)
- final_result = zintersect(pipeline, {job_scores: -1, 'jobs:req': 1}, _execute=False)
+ job_scores = zunion(pipeline, dict((key, 1) for key in union), _execute=False)
+ final_result = zintersect(pipeline, {job_scores: -1, 'jobs:req': 1}, _execute=False)
- pipeline.zrange('idx:' + final_result, 0, 0)
- return pipeline.execute()[-1]
+ pipeline.zrange('idx:' + final_result, 0, 0)
+ return pipeline.execute()[-1]
class TestCh07(unittest.TestCase):
- content = 'this is some random content, look at how it is indexed.'
+ content = 'this is some random content, look at how it is indexed.'
- def setUp(self):
- self.conn = redis.Redis(db=15)
- self.conn.flushdb()
+ def setUp(self):
+ self.conn = redis.Redis(db=15)
+ self.conn.flushdb()
- def tearDown(self):
- self.conn.flushdb()
+ def tearDown(self):
+ self.conn.flushdb()
- def test_index_document(self):
- print "We're tokenizing some content..."
- tokens = tokenize(self.content)
- print "Those tokens are:", tokens
- self.assertTrue(tokens)
+ def test_index_document(self):
+ print "We're tokenizing some content..."
+ tokens = tokenize(self.content)
+ print "Those tokens are:", tokens
+ self.assertTrue(tokens)
- print "And now we are indexing that content..."
- r = index_document(self.conn, 'test', self.content)
- self.assertEquals(r, len(tokens))
- for t in tokens:
- self.assertEquals(self.conn.smembers('idx:' + t), set(['test']))
+ print "And now we are indexing that content..."
+ r = index_document(self.conn, 'test', self.content)
+ self.assertEquals(r, len(tokens))
+ for t in tokens:
+ self.assertEquals(self.conn.smembers('idx:' + t), set(['test']))
- def test_set_operations(self):
- index_document(self.conn, 'test', self.content)
+ def test_set_operations(self):
+ index_document(self.conn, 'test', self.content)
- r = intersect(self.conn, ['content', 'indexed'])
- self.assertEquals(self.conn.smembers('idx:' + r), set(['test']))
+ r = intersect(self.conn, ['content', 'indexed'])
+ self.assertEquals(self.conn.smembers('idx:' + r), set(['test']))
- r = intersect(self.conn, ['content', 'ignored'])
- self.assertEquals(self.conn.smembers('idx:' + r), set())
+ r = intersect(self.conn, ['content', 'ignored'])
+ self.assertEquals(self.conn.smembers('idx:' + r), set())
- r = union(self.conn, ['content', 'ignored'])
- self.assertEquals(self.conn.smembers('idx:' + r), set(['test']))
+ r = union(self.conn, ['content', 'ignored'])
+ self.assertEquals(self.conn.smembers('idx:' + r), set(['test']))
- r = difference(self.conn, ['content', 'ignored'])
- self.assertEquals(self.conn.smembers('idx:' + r), set(['test']))
+ r = difference(self.conn, ['content', 'ignored'])
+ self.assertEquals(self.conn.smembers('idx:' + r), set(['test']))
- r = difference(self.conn, ['content', 'indexed'])
- self.assertEquals(self.conn.smembers('idx:' + r), set())
+ r = difference(self.conn, ['content', 'indexed'])
+ self.assertEquals(self.conn.smembers('idx:' + r), set())
- def test_parse_query(self):
- query = 'test query without stopwords'
- self.assertEquals(parse(query), ([[x] for x in query.split()], []))
+ def test_parse_query(self):
+ query = 'test query without stopwords'
+ self.assertEquals(parse(query), ([[x] for x in query.split()], []))
- query = 'test +query without -stopwords'
- self.assertEquals(parse(query), ([['test', 'query'], ['without']], ['stopwords']))
+ query = 'test +query without -stopwords'
+ self.assertEquals(parse(query), ([['test', 'query'], ['without']], ['stopwords']))
- def test_parse_and_search(self):
- print "And now we are testing search..."
- index_document(self.conn, 'test', self.content)
+ def test_parse_and_search(self):
+ print "And now we are testing search..."
+ index_document(self.conn, 'test', self.content)
- r = parse_and_search(self.conn, 'content')
- self.assertEquals(self.conn.smembers('idx:' + r), set(['test']))
+ r = parse_and_search(self.conn, 'content')
+ self.assertEquals(self.conn.smembers('idx:' + r), set(['test']))
- r = parse_and_search(self.conn, 'content indexed random')
- self.assertEquals(self.conn.smembers('idx:' + r), set(['test']))
+ r = parse_and_search(self.conn, 'content indexed random')
+ self.assertEquals(self.conn.smembers('idx:' + r), set(['test']))
- r = parse_and_search(self.conn, 'content +indexed random')
- self.assertEquals(self.conn.smembers('idx:' + r), set(['test']))
+ r = parse_and_search(self.conn, 'content +indexed random')
+ self.assertEquals(self.conn.smembers('idx:' + r), set(['test']))
- r = parse_and_search(self.conn, 'content indexed +random')
- self.assertEquals(self.conn.smembers('idx:' + r), set(['test']))
+ r = parse_and_search(self.conn, 'content indexed +random')
+ self.assertEquals(self.conn.smembers('idx:' + r), set(['test']))
- r = parse_and_search(self.conn, 'content indexed -random')
- self.assertEquals(self.conn.smembers('idx:' + r), set())
+ r = parse_and_search(self.conn, 'content indexed -random')
+ self.assertEquals(self.conn.smembers('idx:' + r), set())
- r = parse_and_search(self.conn, 'content indexed +random')
- self.assertEquals(self.conn.smembers('idx:' + r), set(['test']))
+ r = parse_and_search(self.conn, 'content indexed +random')
+ self.assertEquals(self.conn.smembers('idx:' + r), set(['test']))
- print "Which passed!"
+ print "Which passed!"
- def test_search_with_sort(self):
- print "And now let's test searching with sorting..."
+ def test_search_with_sort(self):
+ print "And now let's test searching with sorting..."
- index_document(self.conn, 'test', self.content)
- index_document(self.conn, 'test2', self.content)
- self.conn.hmset('kb:doc:test', {'updated': 12345, 'id': 10})
- self.conn.hmset('kb:doc:test2', {'updated': 54321, 'id': 1})
+ index_document(self.conn, 'test', self.content)
+ index_document(self.conn, 'test2', self.content)
+ self.conn.hmset('kb:doc:test', {'updated': 12345, 'id': 10})
+ self.conn.hmset('kb:doc:test2', {'updated': 54321, 'id': 1})
- r = search_and_sort(self.conn, "content")
- self.assertEquals(r[1], ['test2', 'test'])
+ r = search_and_sort(self.conn, "content")
+ self.assertEquals(r[1], ['test2', 'test'])
- r = search_and_sort(self.conn, "content", sort='-id')
- self.assertEquals(r[1], ['test', 'test2'])
- print "Which passed!"
+ r = search_and_sort(self.conn, "content", sort='-id')
+ self.assertEquals(r[1], ['test', 'test2'])
+ print "Which passed!"
- def test_search_with_zsort(self):
- print "And now let's test searching with sorting via zset..."
+ def test_search_with_zsort(self):
+ print "And now let's test searching with sorting via zset..."
- index_document(self.conn, 'test', self.content)
- index_document(self.conn, 'test2', self.content)
- self.conn.zadd('idx:sort:update', 'test', 12345, 'test2', 54321)
- self.conn.zadd('idx:sort:votes', 'test', 10, 'test2', 1)
+ index_document(self.conn, 'test', self.content)
+ index_document(self.conn, 'test2', self.content)
+ self.conn.zadd('idx:sort:update', 'test', 12345, 'test2', 54321)
+ self.conn.zadd('idx:sort:votes', 'test', 10, 'test2', 1)
- r = search_and_zsort(self.conn, "content", desc=False)
- self.assertEquals(r[1], ['test', 'test2'])
+ r = search_and_zsort(self.conn, "content", desc=False)
+ self.assertEquals(r[1], ['test', 'test2'])
- r = search_and_zsort(self.conn, "content", update=0, vote=1, desc=False)
- self.assertEquals(r[1], ['test2', 'test'])
- print "Which passed!"
+ r = search_and_zsort(self.conn, "content", update=0, vote=1, desc=False)
+ self.assertEquals(r[1], ['test2', 'test'])
+ print "Which passed!"
- def test_string_to_score(self):
- words = 'these are some words that will be sorted'.split()
- pairs = [(word, string_to_score(word)) for word in words]
- pairs2 = list(pairs)
- pairs.sort()
- pairs2.sort(key=lambda x: x[1])
- self.assertEquals(pairs, pairs2)
+ def test_string_to_score(self):
+ words = 'these are some words that will be sorted'.split()
+ pairs = [(word, string_to_score(word)) for word in words]
+ pairs2 = list(pairs)
+ pairs.sort()
+ pairs2.sort(key=lambda x: x[1])
+ self.assertEquals(pairs, pairs2)
- words = 'these are some words that will be sorted'.split()
- pairs = [(word, string_to_score_generic(word, LOWER)) for word in words]
- pairs2 = list(pairs)
- pairs.sort()
- pairs2.sort(key=lambda x: x[1])
- self.assertEquals(pairs, pairs2)
+ words = 'these are some words that will be sorted'.split()
+ pairs = [(word, string_to_score_generic(word, LOWER)) for word in words]
+ pairs2 = list(pairs)
+ pairs.sort()
+ pairs2.sort(key=lambda x: x[1])
+ self.assertEquals(pairs, pairs2)
- zadd_string(self.conn, 'key', 'test', 'value', test2='other')
- self.assertTrue(self.conn.zscore('key', 'test'), string_to_score('value'))
- self.assertTrue(self.conn.zscore('key', 'test2'), string_to_score('other'))
+ zadd_string(self.conn, 'key', 'test', 'value', test2='other')
+ self.assertTrue(self.conn.zscore('key', 'test'), string_to_score('value'))
+ self.assertTrue(self.conn.zscore('key', 'test2'), string_to_score('other'))
- def test_index_and_target_ads(self):
- index_ad(self.conn, '1', ['USA', 'CA'], self.content, 'cpc', .25)
- index_ad(self.conn, '2', ['USA', 'VA'], self.content + ' wooooo', 'cpc', .125)
+ def test_index_and_target_ads(self):
+ index_ad(self.conn, '1', ['USA', 'CA'], self.content, 'cpc', .25)
+ index_ad(self.conn, '2', ['USA', 'VA'], self.content + ' wooooo', 'cpc', .125)
- for i in xrange(100):
- ro = target_ads(self.conn, ['USA'], self.content)
- self.assertEquals(ro[1], '1')
+ for i in xrange(100):
+ ro = target_ads(self.conn, ['USA'], self.content)
+ self.assertEquals(ro[1], '1')
- r = target_ads(self.conn, ['VA'], 'wooooo')
- self.assertEquals(r[1], '2')
+ r = target_ads(self.conn, ['VA'], 'wooooo')
+ self.assertEquals(r[1], '2')
- self.assertEquals(self.conn.zrange('idx:ad:value:', 0, -1, withscores=True), [('2', 0.125), ('1', 0.25)])
- self.assertEquals(self.conn.zrange('ad:base_value:', 0, -1, withscores=True), [('2', 0.125), ('1', 0.25)])
+ self.assertEquals(self.conn.zrange('idx:ad:value:', 0, -1, withscores=True), [('2', 0.125), ('1', 0.25)])
+ self.assertEquals(self.conn.zrange('ad:base_value:', 0, -1, withscores=True), [('2', 0.125), ('1', 0.25)])
- record_click(self.conn, ro[0], ro[1])
+ record_click(self.conn, ro[0], ro[1])
- self.assertEquals(self.conn.zrange('idx:ad:value:', 0, -1, withscores=True), [('2', 0.125), ('1', 2.5)])
- self.assertEquals(self.conn.zrange('ad:base_value:', 0, -1, withscores=True), [('2', 0.125), ('1', 0.25)])
+ self.assertEquals(self.conn.zrange('idx:ad:value:', 0, -1, withscores=True), [('2', 0.125), ('1', 2.5)])
+ self.assertEquals(self.conn.zrange('ad:base_value:', 0, -1, withscores=True), [('2', 0.125), ('1', 0.25)])
- def test_is_qualified_for_job(self):
- add_job(self.conn, 'test', ['q1', 'q2', 'q3'])
- self.assertTrue(is_qualified(self.conn, 'test', ['q1', 'q3', 'q2']))
- self.assertFalse(is_qualified(self.conn, 'test', ['q1', 'q2']))
+ def test_is_qualified_for_job(self):
+ add_job(self.conn, 'test', ['q1', 'q2', 'q3'])
+ self.assertTrue(is_qualified(self.conn, 'test', ['q1', 'q3', 'q2']))
+ self.assertFalse(is_qualified(self.conn, 'test', ['q1', 'q2']))
- def test_index_and_find_jobs(self):
- index_job(self.conn, 'test1', ['q1', 'q2', 'q3'])
- index_job(self.conn, 'test2', ['q1', 'q3', 'q4'])
- index_job(self.conn, 'test3', ['q1', 'q3', 'q5'])
+ def test_index_and_find_jobs(self):
+ index_job(self.conn, 'test1', ['q1', 'q2', 'q3'])
+ index_job(self.conn, 'test2', ['q1', 'q3', 'q4'])
+ index_job(self.conn, 'test3', ['q1', 'q3', 'q5'])
- self.assertEquals(find_jobs(self.conn, ['q1']), [])
- self.assertEquals(find_jobs(self.conn, ['q1', 'q3', 'q4']), ['test2'])
- self.assertEquals(find_jobs(self.conn, ['q1', 'q3', 'q5']), ['test3'])
- self.assertEquals(find_jobs(self.conn, ['q1', 'q2', 'q3', 'q4', 'q5']), ['test1', 'test2', 'test3'])
+ self.assertEquals(find_jobs(self.conn, ['q1']), [])
+ self.assertEquals(find_jobs(self.conn, ['q1', 'q3', 'q4']), ['test2'])
+ self.assertEquals(find_jobs(self.conn, ['q1', 'q3', 'q5']), ['test3'])
+ self.assertEquals(find_jobs(self.conn, ['q1', 'q2', 'q3', 'q4', 'q5']), ['test1', 'test2', 'test3'])
if __name__ == '__main__':
- unittest.main()
+ unittest.main()
diff --git a/codes/redis/redis-in-action-py/ch08_listing_source.py b/codes/redis/redis-in-action-py/ch08_listing_source.py
index d772531..5cc7506 100644
--- a/codes/redis/redis-in-action-py/ch08_listing_source.py
+++ b/codes/redis/redis-in-action-py/ch08_listing_source.py
@@ -17,44 +17,44 @@ import uuid
def acquire_lock_with_timeout(
- conn, lockname, acquire_timeout=10, lock_timeout=10):
- identifier = str(uuid.uuid4()) # A
- lockname = 'lock:' + lockname
- lock_timeout = int(math.ceil(lock_timeout)) # D
+ conn, lockname, acquire_timeout=10, lock_timeout=10):
+ identifier = str(uuid.uuid4()) # A
+ lockname = 'lock:' + lockname
+ lock_timeout = int(math.ceil(lock_timeout)) # D
- end = time.time() + acquire_timeout
- while time.time() < end:
- if conn.setnx(lockname, identifier): # B
- conn.expire(lockname, lock_timeout) # B
- return identifier
- elif not conn.ttl(lockname): # C
- conn.expire(lockname, lock_timeout) # C
+ end = time.time() + acquire_timeout
+ while time.time() < end:
+ if conn.setnx(lockname, identifier): # B
+ conn.expire(lockname, lock_timeout) # B
+ return identifier
+ elif not conn.ttl(lockname): # C
+ conn.expire(lockname, lock_timeout) # C
- time.sleep(.001)
+ time.sleep(.001)
- return False
+ return False
def release_lock(conn, lockname, identifier):
- pipe = conn.pipeline(True)
- lockname = 'lock:' + lockname
+ pipe = conn.pipeline(True)
+ lockname = 'lock:' + lockname
- while True:
- try:
- pipe.watch(lockname) # A
- if pipe.get(lockname) == identifier: # A
- pipe.multi() # B
- pipe.delete(lockname) # B
- pipe.execute() # B
- return True # B
+ while True:
+ try:
+ pipe.watch(lockname) # A
+ if pipe.get(lockname) == identifier: # A
+ pipe.multi() # B
+ pipe.delete(lockname) # B
+ pipe.execute() # B
+ return True # B
- pipe.unwatch()
- break
+ pipe.unwatch()
+ break
- except redis.exceptions.WatchError: # C
- pass # C
+ except redis.exceptions.WatchError: # C
+ pass # C
- return False # D
+ return False # D
CONFIGS = {}
@@ -62,93 +62,93 @@ CHECKED = {}
def get_config(conn, type, component, wait=1):
- key = 'config:%s:%s' % (type, component)
+ key = 'config:%s:%s' % (type, component)
- if CHECKED.get(key) < time.time() - wait: # A
- CHECKED[key] = time.time() # B
- config = json.loads(conn.get(key) or '{}') # C
- old_config = CONFIGS.get(key) # D
+ if CHECKED.get(key) < time.time() - wait: # A
+ CHECKED[key] = time.time() # B
+ config = json.loads(conn.get(key) or '{}') # C
+ old_config = CONFIGS.get(key) # D
- if config != old_config: # E
- CONFIGS[key] = config # F
+ if config != old_config: # E
+ CONFIGS[key] = config # F
- return CONFIGS.get(key)
+ return CONFIGS.get(key)
REDIS_CONNECTIONS = {}
def redis_connection(component, wait=1): # A
- key = 'config:redis:' + component # B
+ key = 'config:redis:' + component # B
- def wrapper(function): # C
- @functools.wraps(function) # D
- def call(*args, **kwargs): # E
- old_config = CONFIGS.get(key, object()) # F
- _config = get_config( # G
- config_connection, 'redis', component, wait) # G
+ def wrapper(function): # C
+ @functools.wraps(function) # D
+ def call(*args, **kwargs): # E
+ old_config = CONFIGS.get(key, object()) # F
+ _config = get_config( # G
+ config_connection, 'redis', component, wait) # G
- config = {}
- for k, v in _config.iteritems(): # L
- config[k.encode('utf-8')] = v # L
+ config = {}
+ for k, v in _config.iteritems(): # L
+ config[k.encode('utf-8')] = v # L
- if config != old_config: # H
- REDIS_CONNECTIONS[key] = redis.Redis(**config) # H
+ if config != old_config: # H
+ REDIS_CONNECTIONS[key] = redis.Redis(**config) # H
- return function( # I
- REDIS_CONNECTIONS.get(key), *args, **kwargs) # I
+ return function( # I
+ REDIS_CONNECTIONS.get(key), *args, **kwargs) # I
- return call # J
+ return call # J
- return wrapper # K
+ return wrapper # K
def execute_later(conn, queue, name, args):
- # this is just for testing purposes
- assert conn is args[0]
- t = threading.Thread(target=globals()[name], args=tuple(args))
- t.setDaemon(1)
- t.start()
+ # this is just for testing purposes
+ assert conn is args[0]
+ t = threading.Thread(target=globals()[name], args=tuple(args))
+ t.setDaemon(1)
+ t.start()
# 代码清单 8-1
#
def create_user(conn, login, name):
- llogin = login.lower()
- # 使用第 6 章定义的加锁函数尝试对小写的用户名进行加锁。
- lock = acquire_lock_with_timeout(conn, 'user:' + llogin, 1)
- # 如果加锁不成功,那么说明给定的用户名已经被其他用户占用了。
- if not lock:
- return None
+ llogin = login.lower()
+ # 使用第 6 章定义的加锁函数尝试对小写的用户名进行加锁。
+ lock = acquire_lock_with_timeout(conn, 'user:' + llogin, 1)
+ # 如果加锁不成功,那么说明给定的用户名已经被其他用户占用了。
+ if not lock:
+ return None
- # 程序使用了一个散列来储存小写的用户名以及用户 ID 之间的映射,
- # 如果给定的用户名已经被映射到了某个用户 ID ,
- # 那么程序就不会再将这个用户名分配给其他人。
- if conn.hget('users:', llogin):
- release_lock(conn, 'user:' + llogin, lock)
- return None
+ # 程序使用了一个散列来储存小写的用户名以及用户 ID 之间的映射,
+ # 如果给定的用户名已经被映射到了某个用户 ID ,
+ # 那么程序就不会再将这个用户名分配给其他人。
+ if conn.hget('users:', llogin):
+ release_lock(conn, 'user:' + llogin, lock)
+ return None
- # 每个用户都有一个独一无二的 ID ,
- # 这个 ID 是通过对计数器执行自增操作产生的。
- id = conn.incr('user:id:')
- pipeline = conn.pipeline(True)
- # 在散列里面将小写的用户名映射至用户 ID 。
- pipeline.hset('users:', llogin, id)
- # 将用户信息添加到用户对应的散列里面。
- pipeline.hmset('user:%s' % id, {
- 'login': login,
- 'id': id,
- 'name': name,
- 'followers': 0,
- 'following': 0,
- 'posts': 0,
- 'signup': time.time(),
- })
- pipeline.execute()
- # 释放之前对用户名加的锁。
- release_lock(conn, 'user:' + llogin, lock)
- # 返回用户 ID 。
- return id
+ # 每个用户都有一个独一无二的 ID ,
+ # 这个 ID 是通过对计数器执行自增操作产生的。
+ id = conn.incr('user:id:')
+ pipeline = conn.pipeline(True)
+ # 在散列里面将小写的用户名映射至用户 ID 。
+ pipeline.hset('users:', llogin, id)
+ # 将用户信息添加到用户对应的散列里面。
+ pipeline.hmset('user:%s' % id, {
+ 'login': login,
+ 'id': id,
+ 'name': name,
+ 'followers': 0,
+ 'following': 0,
+ 'posts': 0,
+ 'signup': time.time(),
+ })
+ pipeline.execute()
+ # 释放之前对用户名加的锁。
+ release_lock(conn, 'user:' + llogin, lock)
+ # 返回用户 ID 。
+ return id
#
@@ -157,31 +157,31 @@ def create_user(conn, login, name):
# 代码清单 8-2
#
def create_status(conn, uid, message, **data):
- pipeline = conn.pipeline(True)
- # 根据用户 ID 获取用户的用户名。
- pipeline.hget('user:%s' % uid, 'login')
- # 为这条状态消息创建一个新的 ID 。
- pipeline.incr('status:id:')
- login, id = pipeline.execute()
+ pipeline = conn.pipeline(True)
+ # 根据用户 ID 获取用户的用户名。
+ pipeline.hget('user:%s' % uid, 'login')
+ # 为这条状态消息创建一个新的 ID 。
+ pipeline.incr('status:id:')
+ login, id = pipeline.execute()
- # 在发布状态消息之前,先检查用户的账号是否存在。
- if not login:
- return None
+ # 在发布状态消息之前,先检查用户的账号是否存在。
+ if not login:
+ return None
- # 准备并设置状态消息的各项信息。
- data.update({
- 'message': message,
- 'posted': time.time(),
- 'id': id,
- 'uid': uid,
- 'login': login,
- })
- pipeline.hmset('status:%s' % id, data)
- # 更新用户的已发送状态消息数量。
- pipeline.hincrby('user:%s' % uid, 'posts')
- pipeline.execute()
- # 返回新创建的状态消息的 ID 。
- return id
+ # 准备并设置状态消息的各项信息。
+ data.update({
+ 'message': message,
+ 'posted': time.time(),
+ 'id': id,
+ 'uid': uid,
+ 'login': login,
+ })
+ pipeline.hmset('status:%s' % id, data)
+ # 更新用户的已发送状态消息数量。
+ pipeline.hincrby('user:%s' % uid, 'posts')
+ pipeline.execute()
+ # 返回新创建的状态消息的 ID 。
+ return id
#
@@ -192,17 +192,17 @@ def create_status(conn, uid, message, **data):
# 函数接受三个可选参数,
# 它们分别用于指定函数要获取哪条时间线、要获取多少页时间线、以及每页要有多少条状态消息。
def get_status_messages(conn, uid, timeline='home:', page=1, count=30):
- # 获取时间线上面最新的状态消息的 ID 。
- statuses = conn.zrevrange(
- '%s%s' % (timeline, uid), (page - 1) * count, page * count - 1)
+ # 获取时间线上面最新的状态消息的 ID 。
+ statuses = conn.zrevrange(
+ '%s%s' % (timeline, uid), (page - 1) * count, page * count - 1)
- pipeline = conn.pipeline(True)
- # 获取状态消息本身。
- for id in statuses:
- pipeline.hgetall('status:%s' % id)
+ pipeline = conn.pipeline(True)
+ # 获取状态消息本身。
+ for id in statuses:
+ pipeline.hgetall('status:%s' % id)
- # 使用过滤器移除那些已经被删除了的状态消息。
- return filter(None, pipeline.execute())
+ # 使用过滤器移除那些已经被删除了的状态消息。
+ return filter(None, pipeline.execute())
#
@@ -214,36 +214,36 @@ HOME_TIMELINE_SIZE = 1000
def follow_user(conn, uid, other_uid):
- # 把正在关注有序集合以及关注者有序集合的键名缓存起来。
- fkey1 = 'following:%s' % uid
- fkey2 = 'followers:%s' % other_uid
+ # 把正在关注有序集合以及关注者有序集合的键名缓存起来。
+ fkey1 = 'following:%s' % uid
+ fkey2 = 'followers:%s' % other_uid
- # 如果 uid 指定的用户已经关注了 other_uid 指定的用户,那么函数直接返回。
- if conn.zscore(fkey1, other_uid):
- return None
+ # 如果 uid 指定的用户已经关注了 other_uid 指定的用户,那么函数直接返回。
+ if conn.zscore(fkey1, other_uid):
+ return None
- now = time.time()
+ now = time.time()
- pipeline = conn.pipeline(True)
- # 将两个用户的 ID 分别添加到相应的正在关注有序集合以及关注者有序集合里面。
- pipeline.zadd(fkey1, other_uid, now)
- pipeline.zadd(fkey2, uid, now)
- # 从被关注用户的个人时间线里面获取 HOME_TIMELINE_SIZE 条最新的状态消息。
- pipeline.zrevrange('profile:%s' % other_uid,
- 0, HOME_TIMELINE_SIZE - 1, withscores=True)
- following, followers, status_and_score = pipeline.execute()[-3:]
+ pipeline = conn.pipeline(True)
+ # 将两个用户的 ID 分别添加到相应的正在关注有序集合以及关注者有序集合里面。
+ pipeline.zadd(fkey1, other_uid, now)
+ pipeline.zadd(fkey2, uid, now)
+ # 从被关注用户的个人时间线里面获取 HOME_TIMELINE_SIZE 条最新的状态消息。
+ pipeline.zrevrange('profile:%s' % other_uid,
+ 0, HOME_TIMELINE_SIZE - 1, withscores=True)
+ following, followers, status_and_score = pipeline.execute()[-3:]
- # 修改两个用户的散列,更新他们各自的正在关注数量以及关注者数量。
- pipeline.hincrby('user:%s' % uid, 'following', int(following))
- pipeline.hincrby('user:%s' % other_uid, 'followers', int(followers))
- if status_and_score:
- # 对执行关注操作的用户的定制时间线进行更新,并保留时间线上面的最新 1000 条状态消息。
- pipeline.zadd('home:%s' % uid, **dict(status_and_score))
- pipeline.zremrangebyrank('home:%s' % uid, 0, -HOME_TIMELINE_SIZE - 1)
+ # 修改两个用户的散列,更新他们各自的正在关注数量以及关注者数量。
+ pipeline.hincrby('user:%s' % uid, 'following', int(following))
+ pipeline.hincrby('user:%s' % other_uid, 'followers', int(followers))
+ if status_and_score:
+ # 对执行关注操作的用户的定制时间线进行更新,并保留时间线上面的最新 1000 条状态消息。
+ pipeline.zadd('home:%s' % uid, **dict(status_and_score))
+ pipeline.zremrangebyrank('home:%s' % uid, 0, -HOME_TIMELINE_SIZE - 1)
- pipeline.execute()
- # 返回 True 表示关注操作已经成功执行。
- return True
+ pipeline.execute()
+ # 返回 True 表示关注操作已经成功执行。
+ return True
#
@@ -252,34 +252,34 @@ def follow_user(conn, uid, other_uid):
# 代码清单 8-5
#
def unfollow_user(conn, uid, other_uid):
- # 把正在关注有序集合以及关注者有序集合的键名缓存起来。
- fkey1 = 'following:%s' % uid
- fkey2 = 'followers:%s' % other_uid
+ # 把正在关注有序集合以及关注者有序集合的键名缓存起来。
+ fkey1 = 'following:%s' % uid
+ fkey2 = 'followers:%s' % other_uid
- # 如果 uid 指定的用户并未关注 other_uid 指定的用户,那么函数直接返回。
- if not conn.zscore(fkey1, other_uid):
- return None
+ # 如果 uid 指定的用户并未关注 other_uid 指定的用户,那么函数直接返回。
+ if not conn.zscore(fkey1, other_uid):
+ return None
- pipeline = conn.pipeline(True)
- # 从正在关注有序集合以及关注者有序集合里面移除双方的用户 ID 。
- pipeline.zrem(fkey1, other_uid)
- pipeline.zrem(fkey2, uid)
- # 获取被取消关注的用户最近发布的 HOME_TIMELINE_SIZE 条状态消息。
- pipeline.zrevrange('profile:%s' % other_uid,
- 0, HOME_TIMELINE_SIZE - 1)
- following, followers, statuses = pipeline.execute()[-3:]
+ pipeline = conn.pipeline(True)
+ # 从正在关注有序集合以及关注者有序集合里面移除双方的用户 ID 。
+ pipeline.zrem(fkey1, other_uid)
+ pipeline.zrem(fkey2, uid)
+ # 获取被取消关注的用户最近发布的 HOME_TIMELINE_SIZE 条状态消息。
+ pipeline.zrevrange('profile:%s' % other_uid,
+ 0, HOME_TIMELINE_SIZE - 1)
+ following, followers, statuses = pipeline.execute()[-3:]
- # 对用户信息散列里面的正在关注数量以及关注者数量进行更新。
- pipeline.hincrby('user:%s' % uid, 'following', int(following))
- pipeline.hincrby('user:%s' % other_uid, 'followers', int(followers))
- if statuses:
- # 对执行取消关注操作的用户的定制时间线进行更新,
- # 移除被取消关注的用户发布的所有状态消息。
- pipeline.zrem('home:%s' % uid, *statuses)
+ # 对用户信息散列里面的正在关注数量以及关注者数量进行更新。
+ pipeline.hincrby('user:%s' % uid, 'following', int(following))
+ pipeline.hincrby('user:%s' % other_uid, 'followers', int(followers))
+ if statuses:
+ # 对执行取消关注操作的用户的定制时间线进行更新,
+ # 移除被取消关注的用户发布的所有状态消息。
+ pipeline.zrem('home:%s' % uid, *statuses)
- pipeline.execute()
- # 返回 True 表示取消关注操作执行成功。
- return True
+ pipeline.execute()
+ # 返回 True 表示取消关注操作执行成功。
+ return True
#
@@ -289,120 +289,120 @@ REFILL_USERS_STEP = 50
def refill_timeline(conn, incoming, timeline, start=0):
- if not start and conn.zcard(timeline) >= 750: # 如果时间线已经被填满了 3/4 或以上
- return # 那么不对它进行重新填充
+ if not start and conn.zcard(timeline) >= 750: # 如果时间线已经被填满了 3/4 或以上
+ return # 那么不对它进行重新填充
- users = conn.zrangebyscore(incoming, start, 'inf', # 获取一组用户,这些用户发布的消息将被用于填充时间线
- start=0, num=REFILL_USERS_STEP, withscores=True) #
+ users = conn.zrangebyscore(incoming, start, 'inf', # 获取一组用户,这些用户发布的消息将被用于填充时间线
+ start=0, num=REFILL_USERS_STEP, withscores=True) #
- pipeline = conn.pipeline(False)
- for uid, start in users:
- pipeline.zrevrange('profile:%s' % uid, # 从正在关注的人哪里获取最新的状态消息
- 0, HOME_TIMELINE_SIZE - 1, withscores=True) #
+ pipeline = conn.pipeline(False)
+ for uid, start in users:
+ pipeline.zrevrange('profile:%s' % uid, # 从正在关注的人哪里获取最新的状态消息
+ 0, HOME_TIMELINE_SIZE - 1, withscores=True) #
- messages = []
- for results in pipeline.execute():
- messages.extend(results) # 将取得的所有状态消息放到一起
+ messages = []
+ for results in pipeline.execute():
+ messages.extend(results) # 将取得的所有状态消息放到一起
- messages.sort(key=lambda x: -x[1]) # 根据发布时间对取得的所有状态消息进行排序,
- del messages[HOME_TIMELINE_SIZE:] # 并保留其中最新的 100 条状态消息
+ messages.sort(key=lambda x: -x[1]) # 根据发布时间对取得的所有状态消息进行排序,
+ del messages[HOME_TIMELINE_SIZE:] # 并保留其中最新的 100 条状态消息
- pipeline = conn.pipeline(True)
- if messages:
- pipeline.zadd(timeline, **dict(messages)) # 将挑选出的状态消息添加到用户的主页时间线上面
- pipeline.zremrangebyrank( # 对时间线进行修剪,只保留最新的 100 条状态消息
- timeline, 0, -HOME_TIMELINE_SIZE - 1) #
- pipeline.execute()
+ pipeline = conn.pipeline(True)
+ if messages:
+ pipeline.zadd(timeline, **dict(messages)) # 将挑选出的状态消息添加到用户的主页时间线上面
+ pipeline.zremrangebyrank( # 对时间线进行修剪,只保留最新的 100 条状态消息
+ timeline, 0, -HOME_TIMELINE_SIZE - 1) #
+ pipeline.execute()
- if len(users) >= REFILL_USERS_STEP:
- execute_later(conn, 'default', 'refill_timeline', # 如果还要其他用户的时间线需要进行重新填充,
- [conn, incoming, timeline, start]) # 那么继续执行这个动作
+ if len(users) >= REFILL_USERS_STEP:
+ execute_later(conn, 'default', 'refill_timeline', # 如果还要其他用户的时间线需要进行重新填充,
+ [conn, incoming, timeline, start]) # 那么继续执行这个动作
#
#
def follow_user_list(conn, uid, other_uid, list_id):
- fkey1 = 'list:in:%s' % list_id # 把相关的键名缓存起来
- fkey2 = 'list:out:%s' % other_uid #
- timeline = 'list:statuses:%s' % list_id #
+ fkey1 = 'list:in:%s' % list_id # 把相关的键名缓存起来
+ fkey2 = 'list:out:%s' % other_uid #
+ timeline = 'list:statuses:%s' % list_id #
- if conn.zscore(fkey1, other_uid): # 如果 other_uid 已经包含在列表里面,
- return None # 那么直接返回
+ if conn.zscore(fkey1, other_uid): # 如果 other_uid 已经包含在列表里面,
+ return None # 那么直接返回
- now = time.time()
+ now = time.time()
- pipeline = conn.pipeline(True)
- pipeline.zadd(fkey1, other_uid, now) # 将各个用户ID添加到相应的有序集合里面
- pipeline.zadd(fkey2, list_id, now) #
- pipeline.zcard(fkey1) # 获取有序集合的大小
- pipeline.zrevrange('profile:%s' % other_uid, # 从用户的个人时间线里面获取最新的状态消息
- 0, HOME_TIMELINE_SIZE - 1, withscores=True) #
- following, status_and_score = pipeline.execute()[-2:]
+ pipeline = conn.pipeline(True)
+ pipeline.zadd(fkey1, other_uid, now) # 将各个用户ID添加到相应的有序集合里面
+ pipeline.zadd(fkey2, list_id, now) #
+ pipeline.zcard(fkey1) # 获取有序集合的大小
+ pipeline.zrevrange('profile:%s' % other_uid, # 从用户的个人时间线里面获取最新的状态消息
+ 0, HOME_TIMELINE_SIZE - 1, withscores=True) #
+ following, status_and_score = pipeline.execute()[-2:]
- pipeline.hset('list:%s' % list_id, 'following', following) # 对存储列表信息的散列进行更新,将列表的新大小记录到散列里面
- pipeline.zadd(timeline, **dict(status_and_score)) # 对列表的状态消息进行更新
- pipeline.zremrangebyrank(timeline, 0, -HOME_TIMELINE_SIZE - 1) #
+ pipeline.hset('list:%s' % list_id, 'following', following) # 对存储列表信息的散列进行更新,将列表的新大小记录到散列里面
+ pipeline.zadd(timeline, **dict(status_and_score)) # 对列表的状态消息进行更新
+ pipeline.zremrangebyrank(timeline, 0, -HOME_TIMELINE_SIZE - 1) #
- pipeline.execute()
- return True # 返回 True 值,表示用户已经被添加到列表里面
+ pipeline.execute()
+ return True # 返回 True 值,表示用户已经被添加到列表里面
#
#
def unfollow_user_list(conn, uid, other_uid, list_id):
- fkey1 = 'list:in:%s' % list_id # 把相关的键名缓存起来
- fkey2 = 'list:out:%s' % other_uid #
- timeline = 'list:statuses:%s' % list_id #
+ fkey1 = 'list:in:%s' % list_id # 把相关的键名缓存起来
+ fkey2 = 'list:out:%s' % other_uid #
+ timeline = 'list:statuses:%s' % list_id #
- if not conn.zscore(fkey1, other_uid): # 如果用户并未关注 other_uid ,
- return None # 那么直接返回
+ if not conn.zscore(fkey1, other_uid): # 如果用户并未关注 other_uid ,
+ return None # 那么直接返回
- pipeline = conn.pipeline(True)
- pipeline.zrem(fkey1, other_uid) # 从相应的有序集合里面移除各个用户ID
- pipeline.zrem(fkey2, list_id) #
- pipeline.zcard(fkey1) # 获取有序集合的大小
- pipeline.zrevrange('profile:%s' % other_uid, # 从被取消关注的用户那里获取他最新发布的状态消息
- 0, HOME_TIMELINE_SIZE - 1) #
- following, statuses = pipeline.execute()[-2:]
+ pipeline = conn.pipeline(True)
+ pipeline.zrem(fkey1, other_uid) # 从相应的有序集合里面移除各个用户ID
+ pipeline.zrem(fkey2, list_id) #
+ pipeline.zcard(fkey1) # 获取有序集合的大小
+ pipeline.zrevrange('profile:%s' % other_uid, # 从被取消关注的用户那里获取他最新发布的状态消息
+ 0, HOME_TIMELINE_SIZE - 1) #
+ following, statuses = pipeline.execute()[-2:]
- pipeline.hset('list:%s' % list_id, 'following', following) # 对存储列表信息的散列进行更新,将列表的新大小记录到散列里面
- if statuses:
- pipeline.zrem(timeline, *statuses) # 从时间线里面移除被取消关注的用户所发布的状态消息
- refill_timeline(fkey1, timeline) # 重新填充时间线
+ pipeline.hset('list:%s' % list_id, 'following', following) # 对存储列表信息的散列进行更新,将列表的新大小记录到散列里面
+ if statuses:
+ pipeline.zrem(timeline, *statuses) # 从时间线里面移除被取消关注的用户所发布的状态消息
+ refill_timeline(fkey1, timeline) # 重新填充时间线
- pipeline.execute()
- return True # 返回 True 值,表示用户已经被取消关注
+ pipeline.execute()
+ return True # 返回 True 值,表示用户已经被取消关注
#
#
def create_user_list(conn, uid, name):
- pipeline = conn.pipeline(True)
- pipeline.hget('user:%s' % uid, 'login') # 获取创建列表的用户的用户名
- pipeline.incr('list:id:') # 生成一个新的列表ID
- login, id = pipeline.execute()
+ pipeline = conn.pipeline(True)
+ pipeline.hget('user:%s' % uid, 'login') # 获取创建列表的用户的用户名
+ pipeline.incr('list:id:') # 生成一个新的列表ID
+ login, id = pipeline.execute()
- if not login: # 如果用户不存在,那么直接返回
- return None #
+ if not login: # 如果用户不存在,那么直接返回
+ return None #
- now = time.time()
+ now = time.time()
- pipeline = conn.pipeline(True)
- pipeline.zadd('lists:%s' % uid, **{id: now}) # 将新创建的列表添加到用户已经创建了的有序集合里面
- pipeline.hmset('list:%s' % id, { # 创建记录列表信息的散列
- 'name': name, #
- 'id': id, #
- 'uid': uid, #
- 'login': login, #
- 'following': 0, #
- 'created': now, #
- })
- pipeline.execute()
+ pipeline = conn.pipeline(True)
+ pipeline.zadd('lists:%s' % uid, **{id: now}) # 将新创建的列表添加到用户已经创建了的有序集合里面
+ pipeline.hmset('list:%s' % id, { # 创建记录列表信息的散列
+ 'name': name, #
+ 'id': id, #
+ 'uid': uid, #
+ 'login': login, #
+ 'following': 0, #
+ 'created': now, #
+ })
+ pipeline.execute()
- return id # 返回新列表的ID
+ return id # 返回新列表的ID
#
@@ -411,25 +411,25 @@ def create_user_list(conn, uid, name):
# 代码清单 8-6
#
def post_status(conn, uid, message, **data):
- # 使用之前介绍过的函数来创建一条新的状态消息。
- id = create_status(conn, uid, message, **data)
- # 如果创建状态消息失败,那么直接返回。
- if not id:
- return None
+ # 使用之前介绍过的函数来创建一条新的状态消息。
+ id = create_status(conn, uid, message, **data)
+ # 如果创建状态消息失败,那么直接返回。
+ if not id:
+ return None
- # 获取消息的发布时间。
- posted = conn.hget('status:%s' % id, 'posted')
- # 如果程序未能顺利地获取消息的发布时间,那么直接返回。
- if not posted:
- return None
+ # 获取消息的发布时间。
+ posted = conn.hget('status:%s' % id, 'posted')
+ # 如果程序未能顺利地获取消息的发布时间,那么直接返回。
+ if not posted:
+ return None
- post = {str(id): float(posted)}
- # 将状态消息添加到用户的个人时间线里面。
- conn.zadd('profile:%s' % uid, **post)
+ post = {str(id): float(posted)}
+ # 将状态消息添加到用户的个人时间线里面。
+ conn.zadd('profile:%s' % uid, **post)
- # 将状态消息推送给用户的关注者。
- syndicate_status(conn, uid, post)
- return id
+ # 将状态消息推送给用户的关注者。
+ syndicate_status(conn, uid, post)
+ return id
#
@@ -442,55 +442,55 @@ POSTS_PER_PASS = 1000
def syndicate_status(conn, uid, post, start=0):
- # 以上次被更新的最后一个关注者为起点,获取接下来的一千个关注者。
- followers = conn.zrangebyscore('followers:%s' % uid, start, 'inf',
- start=0, num=POSTS_PER_PASS, withscores=True)
+ # 以上次被更新的最后一个关注者为起点,获取接下来的一千个关注者。
+ followers = conn.zrangebyscore('followers:%s' % uid, start, 'inf',
+ start=0, num=POSTS_PER_PASS, withscores=True)
- pipeline = conn.pipeline(False)
- # 在遍历关注者的同时,
- # 对 start 变量的值进行更新,
- # 这个变量可以在有需要的时候传递给下一个 syndicate_status() 调用。
- for follower, start in followers:
- # 将状态消息添加到所有被获取的关注者的定制时间线里面,
- # 并在有需要的时候对关注者的定制时间线进行修剪,
- # 防止它超过限定的最大长度。
- pipeline.zadd('home:%s' % follower, **post)
- pipeline.zremrangebyrank(
- 'home:%s' % follower, 0, -HOME_TIMELINE_SIZE - 1)
- pipeline.execute()
+ pipeline = conn.pipeline(False)
+ # 在遍历关注者的同时,
+ # 对 start 变量的值进行更新,
+ # 这个变量可以在有需要的时候传递给下一个 syndicate_status() 调用。
+ for follower, start in followers:
+ # 将状态消息添加到所有被获取的关注者的定制时间线里面,
+ # 并在有需要的时候对关注者的定制时间线进行修剪,
+ # 防止它超过限定的最大长度。
+ pipeline.zadd('home:%s' % follower, **post)
+ pipeline.zremrangebyrank(
+ 'home:%s' % follower, 0, -HOME_TIMELINE_SIZE - 1)
+ pipeline.execute()
- # 如果需要更新的关注者数量超过一千人,
- # 那么在延迟任务里面继续执行剩余的更新操作。
- if len(followers) >= POSTS_PER_PASS:
- execute_later(conn, 'default', 'syndicate_status',
- [conn, uid, post, start])
- #
+ # 如果需要更新的关注者数量超过一千人,
+ # 那么在延迟任务里面继续执行剩余的更新操作。
+ if len(followers) >= POSTS_PER_PASS:
+ execute_later(conn, 'default', 'syndicate_status',
+ [conn, uid, post, start])
+ #
#
def syndicate_status_list(conn, uid, post, start=0, on_lists=False):
- key = 'followers:%s' % uid # 根据操作的处理进度(depending on how far along we are),
- base = 'home:%s' # 选择对主页时间线还是对用户时间线进行操作
- if on_lists: #
- key = 'list:out:%s' % uid #
- base = 'list:statuses:%s' #
- followers = conn.zrangebyscore(key, start, 'inf', # 从上次更新时的最后一个用户或者列表作为起点,
- start=0, num=POSTS_PER_PASS, withscores=True) # 获取下一组用户或者列表(数量为 1000 个)
+ key = 'followers:%s' % uid # 根据操作的处理进度(depending on how far along we are),
+ base = 'home:%s' # 选择对主页时间线还是对用户时间线进行操作
+ if on_lists: #
+ key = 'list:out:%s' % uid #
+ base = 'list:statuses:%s' #
+ followers = conn.zrangebyscore(key, start, 'inf', # 从上次更新时的最后一个用户或者列表作为起点,
+ start=0, num=POSTS_PER_PASS, withscores=True) # 获取下一组用户或者列表(数量为 1000 个)
- pipeline = conn.pipeline(False)
- for follower, start in followers: # 将状态消息添加到所有已获取关注者的主页时间线里面
- pipeline.zadd(base % follower, **post) #
- pipeline.zremrangebyrank( #
- base % follower, 0, -HOME_TIMELINE_SIZE - 1) #
- pipeline.execute()
+ pipeline = conn.pipeline(False)
+ for follower, start in followers: # 将状态消息添加到所有已获取关注者的主页时间线里面
+ pipeline.zadd(base % follower, **post) #
+ pipeline.zremrangebyrank( #
+ base % follower, 0, -HOME_TIMELINE_SIZE - 1) #
+ pipeline.execute()
- if len(followers) >= POSTS_PER_PASS: # 如果已经对至少 1000 个用户进行了更新,
- execute_later(conn, 'default', 'syndicate_status', # 那么将后续的更新操作留到下次再进行
- [conn, uid, post, start, on_lists]) #
+ if len(followers) >= POSTS_PER_PASS: # 如果已经对至少 1000 个用户进行了更新,
+ execute_later(conn, 'default', 'syndicate_status', # 那么将后续的更新操作留到下次再进行
+ [conn, uid, post, start, on_lists]) #
- elif not on_lists:
- execute_later(conn, 'default', 'syndicate_status', # 如果针对列表的操作并未完成,那么对列表进行操作
- [conn, uid, post, 0, True]) # 如果操作只是对主页时间线执行的话,那么程序无需执行这一步
+ elif not on_lists:
+ execute_later(conn, 'default', 'syndicate_status', # 如果针对列表的操作并未完成,那么对列表进行操作
+ [conn, uid, post, 0, True]) # 如果操作只是对主页时间线执行的话,那么程序无需执行这一步
#
@@ -499,57 +499,57 @@ def syndicate_status_list(conn, uid, post, start=0, on_lists=False):
# 代码清单 8-8
#
def delete_status(conn, uid, status_id):
- key = 'status:%s' % status_id
- # 对指定的状态消息进行加锁,防止两个程序同时删除同一条状态消息的情况出现。
- lock = acquire_lock_with_timeout(conn, key, 1)
- # 如果加锁失败,那么直接返回。
- if not lock:
- return None
+ key = 'status:%s' % status_id
+ # 对指定的状态消息进行加锁,防止两个程序同时删除同一条状态消息的情况出现。
+ lock = acquire_lock_with_timeout(conn, key, 1)
+ # 如果加锁失败,那么直接返回。
+ if not lock:
+ return None
- # 如果 uid 指定的用户并非状态消息的发布人,那么函数直接返回。
- if conn.hget(key, 'uid') != str(uid):
- release_lock(conn, key, lock)
- return None
+ # 如果 uid 指定的用户并非状态消息的发布人,那么函数直接返回。
+ if conn.hget(key, 'uid') != str(uid):
+ release_lock(conn, key, lock)
+ return None
- pipeline = conn.pipeline(True)
- # 删除指定的状态消息。
- pipeline.delete(key)
- # 从用户的个人时间线里面移除指定的状态消息 ID 。
- pipeline.zrem('profile:%s' % uid, status_id)
- # 从用户的定制时间线里面移除指定的状态消息 ID 。
- pipeline.zrem('home:%s' % uid, status_id)
- # 对储存着用户信息的散列进行更新,减少已发布状态消息的数量。
- pipeline.hincrby('user:%s' % uid, 'posts', -1)
- pipeline.execute()
+ pipeline = conn.pipeline(True)
+ # 删除指定的状态消息。
+ pipeline.delete(key)
+ # 从用户的个人时间线里面移除指定的状态消息 ID 。
+ pipeline.zrem('profile:%s' % uid, status_id)
+ # 从用户的定制时间线里面移除指定的状态消息 ID 。
+ pipeline.zrem('home:%s' % uid, status_id)
+ # 对储存着用户信息的散列进行更新,减少已发布状态消息的数量。
+ pipeline.hincrby('user:%s' % uid, 'posts', -1)
+ pipeline.execute()
- release_lock(conn, key, lock)
- return True
+ release_lock(conn, key, lock)
+ return True
#
#
def clean_timelines(conn, uid, status_id, start=0, on_lists=False):
- key = 'followers:%s' % uid # 根据操作的处理进度,
- base = 'home:%s' # 选择对主页时间线还是对用户时间线进行操作
- if on_lists: #
- key = 'list:out:%s' % uid #
- base = 'list:statuses:%s' #
- followers = conn.zrangebyscore(key, start, 'inf', # 从上次更新时的最后一个用户或者列表作为起点,
- start=0, num=POSTS_PER_PASS, withscores=True) # 获取下一组用户或者列表(数量为 1000 个)
+ key = 'followers:%s' % uid # 根据操作的处理进度,
+ base = 'home:%s' # 选择对主页时间线还是对用户时间线进行操作
+ if on_lists: #
+ key = 'list:out:%s' % uid #
+ base = 'list:statuses:%s' #
+ followers = conn.zrangebyscore(key, start, 'inf', # 从上次更新时的最后一个用户或者列表作为起点,
+ start=0, num=POSTS_PER_PASS, withscores=True) # 获取下一组用户或者列表(数量为 1000 个)
- pipeline = conn.pipeline(False)
- for follower, start in followers: # 从所有已获取的关注者的主页时间线上面,
- pipeline.zrem(base % follower, status_id) # 移除指定的状态消息
- pipeline.execute()
+ pipeline = conn.pipeline(False)
+ for follower, start in followers: # 从所有已获取的关注者的主页时间线上面,
+ pipeline.zrem(base % follower, status_id) # 移除指定的状态消息
+ pipeline.execute()
- if len(followers) >= POSTS_PER_PASS: # 如果本次更新已经处理了至少 1000 个关注者,
- execute_later(conn, 'default', 'clean_timelines', # 那么将后续的工作留到下次再执行
- [conn, uid, status_id, start, on_lists]) #
+ if len(followers) >= POSTS_PER_PASS: # 如果本次更新已经处理了至少 1000 个关注者,
+ execute_later(conn, 'default', 'clean_timelines', # 那么将后续的工作留到下次再执行
+ [conn, uid, status_id, start, on_lists]) #
- elif not on_lists:
- execute_later(conn, 'default', 'clean_timelines', # 如果针对列表的操作并未完成,那么对列表进行操作
- [conn, uid, status_id, 0, True]) # 如果操作只是对主页时间线执行的话,那么程序无需执行这一步
+ elif not on_lists:
+ execute_later(conn, 'default', 'clean_timelines', # 如果针对列表的操作并未完成,那么对列表进行操作
+ [conn, uid, status_id, 0, True]) # 如果操作只是对主页时间线执行的话,那么程序无需执行这一步
#
@@ -559,65 +559,66 @@ def clean_timelines(conn, uid, status_id, start=0, on_lists=False):
#
# 创建一个名为 StreamingAPIServer 的类。
class StreamingAPIServer(
- # 这个类是一个 HTTP 服务器,
- # 并且它具有为每个请求创建一个新线程的能力。
- SocketServer.ThreadingMixIn,
- BaseHTTPServer.HTTPServer):
- # 让线程服务器内部组件在主服务器线程死亡(die)之后,
- # 关闭所有客户端请求线程。
- daemon_threads = True
+ # 这个类是一个 HTTP 服务器,
+ # 并且它具有为每个请求创建一个新线程的能力。
+ SocketServer.ThreadingMixIn,
+ BaseHTTPServer.HTTPServer):
+ # 让线程服务器内部组件在主服务器线程死亡(die)之后,
+ # 关闭所有客户端请求线程。
+ daemon_threads = True
+
# 创建一个名为 StreamingAPIRequestHandler 的类。
class StreamingAPIRequestHandler(
- # 这个新创建的类可以用于处理 HTTP 请求。
- BaseHTTPServer.BaseHTTPRequestHandler):
+ # 这个新创建的类可以用于处理 HTTP 请求。
+ BaseHTTPServer.BaseHTTPRequestHandler):
- # 创建一个名为 do_GET() 的方法,用于处理服务器接收到的 GET 请求。
- def do_GET(self):
- # 调用辅助函数,获取客户端标识符。
- parse_identifier(self)
- # 如果这个 GET 请求访问的不是 sample 流或者 firehose 流,
- # 那么返回“404 页面未找到”错误。
- if self.path != '/statuses/sample.json':
- return self.send_error(404)
+ # 创建一个名为 do_GET() 的方法,用于处理服务器接收到的 GET 请求。
+ def do_GET(self):
+ # 调用辅助函数,获取客户端标识符。
+ parse_identifier(self)
+ # 如果这个 GET 请求访问的不是 sample 流或者 firehose 流,
+ # 那么返回“404 页面未找到”错误。
+ if self.path != '/statuses/sample.json':
+ return self.send_error(404)
- # 如果一切顺利,那么调用辅助函数,执行实际的过滤工作。
- process_filters(self)
+ # 如果一切顺利,那么调用辅助函数,执行实际的过滤工作。
+ process_filters(self)
- # 创建一个名为 do_POST() 的方法,用于处理服务器接收到的 POST 请求。
+ # 创建一个名为 do_POST() 的方法,用于处理服务器接收到的 POST 请求。
- def do_POST(self):
- # 调用辅助函数,获取客户端标识符。
- parse_identifier(self)
- # 如果这个 POST 请求访问的不是用户过滤器、关键字过滤器或者位置过滤器,
- # 那么返回“404 页面未找到”错误。
- if self.path != '/statuses/filter.json':
- return self.send_error(404)
+ def do_POST(self):
+ # 调用辅助函数,获取客户端标识符。
+ parse_identifier(self)
+ # 如果这个 POST 请求访问的不是用户过滤器、关键字过滤器或者位置过滤器,
+ # 那么返回“404 页面未找到”错误。
+ if self.path != '/statuses/filter.json':
+ return self.send_error(404)
- # 如果一切顺利,那么调用辅助函数,执行实际的过滤工作。
- process_filters(self)
- #
+ # 如果一切顺利,那么调用辅助函数,执行实际的过滤工作。
+ process_filters(self)
+ #
# 代码清单 8-11
#
def parse_identifier(handler):
- # 将标识符和查询参数设置为预留值。
- handler.identifier = None
- handler.query = {}
- # 如果请求里面包含了查询参数,那么处理这些参数。
- if '?' in handler.path:
- # 取出路径里面包含查询参数的部分,并对路径进行更新。
- handler.path, _, query = handler.path.partition('?')
- # 通过语法分析得出查询参数。
- handler.query = urlparse.parse_qs(query)
- # 获取名为 identifier 的查询参数列表。
- identifier = handler.query.get('identifier') or [None]
- # 使用第一个传入的标识符。
- handler.identifier = identifier[0]
- #
+ # 将标识符和查询参数设置为预留值。
+ handler.identifier = None
+ handler.query = {}
+ # 如果请求里面包含了查询参数,那么处理这些参数。
+ if '?' in handler.path:
+ # 取出路径里面包含查询参数的部分,并对路径进行更新。
+ handler.path, _, query = handler.path.partition('?')
+ # 通过语法分析得出查询参数。
+ handler.query = urlparse.parse_qs(query)
+ # 获取名为 identifier 的查询参数列表。
+ identifier = handler.query.get('identifier') or [None]
+ # 使用第一个传入的标识符。
+ handler.identifier = identifier[0]
+ #
# 代码清单 8-12
@@ -627,61 +628,61 @@ FILTERS = ('track', 'filter', 'location')
def process_filters(handler):
- id = handler.identifier
- # 如果客户端没有提供标识符,那么返回一个错误。
- if not id:
- return handler.send_error(401, "identifier missing")
+ id = handler.identifier
+ # 如果客户端没有提供标识符,那么返回一个错误。
+ if not id:
+ return handler.send_error(401, "identifier missing")
- # 获取客户端指定的方法,
- # 结果应该是 sample (随机消息)或者 filter (过滤器)这两种的其中一种。
- method = handler.path.rsplit('/')[-1].split('.')[0]
- name = None
- args = None
- # 如果客户端指定的是过滤器方法,那么程序需要获取相应的过滤参数。
- if method == 'filter':
- # 对 POST 请求进行语法分析,从而获知过滤器的类型以及参数。
- data = cgi.FieldStorage(
- fp=handler.rfile,
- headers=handler.headers,
- environ={'REQUEST_METHOD': 'POST',
- 'CONTENT_TYPE': handler.headers['Content-Type'],
- })
+ # 获取客户端指定的方法,
+ # 结果应该是 sample (随机消息)或者 filter (过滤器)这两种的其中一种。
+ method = handler.path.rsplit('/')[-1].split('.')[0]
+ name = None
+ args = None
+ # 如果客户端指定的是过滤器方法,那么程序需要获取相应的过滤参数。
+ if method == 'filter':
+ # 对 POST 请求进行语法分析,从而获知过滤器的类型以及参数。
+ data = cgi.FieldStorage(
+ fp=handler.rfile,
+ headers=handler.headers,
+ environ={'REQUEST_METHOD': 'POST',
+ 'CONTENT_TYPE': handler.headers['Content-Type'],
+ })
- # 找到客户端在请求中指定的过滤器。
- for name in data:
- if name in FILTERS:
- args = data.getfirst(name).lower().split(',')
- break
+ # 找到客户端在请求中指定的过滤器。
+ for name in data:
+ if name in FILTERS:
+ args = data.getfirst(name).lower().split(',')
+ break
- # 如果客户端没有指定任何过滤器,那么返回一个错误。
- if not args:
- return handler.send_error(401, "no filter provided")
- else:
- # 如果客户端指定的是随机消息请求,那么将查询参数用作 args 变量的值。
- args = handler.query
+ # 如果客户端没有指定任何过滤器,那么返回一个错误。
+ if not args:
+ return handler.send_error(401, "no filter provided")
+ else:
+ # 如果客户端指定的是随机消息请求,那么将查询参数用作 args 变量的值。
+ args = handler.query
- # 最后,向客户端返回一个回复,
- # 告知客户端,服务器接下来将向它发送流回复。
- handler.send_response(200)
- handler.send_header('Transfer-Encoding', 'chunked')
- handler.end_headers()
+ # 最后,向客户端返回一个回复,
+ # 告知客户端,服务器接下来将向它发送流回复。
+ handler.send_response(200)
+ handler.send_header('Transfer-Encoding', 'chunked')
+ handler.end_headers()
- # 使用 Python 列表来做引用传递(pass-by-reference)变量的占位符,
- # 用户可以通过这个变量来让内容过滤器停止接收消息。
- quit = [False]
- # 对过滤结果进行迭代。
- for item in filter_content(id, method, name, args, quit):
- try:
- # 使用分块传输编码向客户端发送经过预编码后(pre-encoded)的回复。
- handler.wfile.write('%X\r\n%s\r\n' % (len(item), item))
- # 如果发送操作引发了错误,那么让订阅者停止订阅并关闭自身。
- except socket.error:
- quit[0] = True
- if not quit[0]:
- # 如果服务器与客户端的连接并未断开,
- # 那么向客户端发送表示“分块到此结束”的消息。
- handler.wfile.write('0\r\n\r\n')
- #
+ # 使用 Python 列表来做引用传递(pass-by-reference)变量的占位符,
+ # 用户可以通过这个变量来让内容过滤器停止接收消息。
+ quit = [False]
+ # 对过滤结果进行迭代。
+ for item in filter_content(id, method, name, args, quit):
+ try:
+ # 使用分块传输编码向客户端发送经过预编码后(pre-encoded)的回复。
+ handler.wfile.write('%X\r\n%s\r\n' % (len(item), item))
+ # 如果发送操作引发了错误,那么让订阅者停止订阅并关闭自身。
+ except socket.error:
+ quit[0] = True
+ if not quit[0]:
+ # 如果服务器与客户端的连接并未断开,
+ # 那么向客户端发送表示“分块到此结束”的消息。
+ handler.wfile.write('0\r\n\r\n')
+ #
_create_status = create_status
@@ -690,27 +691,27 @@ _create_status = create_status
# 代码清单 8-13
#
def create_status(conn, uid, message, **data):
- pipeline = conn.pipeline(True)
- pipeline.hget('user:%s' % uid, 'login')
- pipeline.incr('status:id:')
- login, id = pipeline.execute()
+ pipeline = conn.pipeline(True)
+ pipeline.hget('user:%s' % uid, 'login')
+ pipeline.incr('status:id:')
+ login, id = pipeline.execute()
- if not login:
- return None
+ if not login:
+ return None
- data.update({
- 'message': message,
- 'posted': time.time(),
- 'id': id,
- 'uid': uid,
- 'login': login,
- })
- pipeline.hmset('status:%s' % id, data)
- pipeline.hincrby('user:%s' % uid, 'posts')
- # 新添加的这一行代码用于向流过滤器发送消息。
- pipeline.publish('streaming:status:', json.dumps(data))
- pipeline.execute()
- return id
+ data.update({
+ 'message': message,
+ 'posted': time.time(),
+ 'id': id,
+ 'uid': uid,
+ 'login': login,
+ })
+ pipeline.hmset('status:%s' % id, data)
+ pipeline.hincrby('user:%s' % uid, 'posts')
+ # 新添加的这一行代码用于向流过滤器发送消息。
+ pipeline.publish('streaming:status:', json.dumps(data))
+ pipeline.execute()
+ return id
#
@@ -721,31 +722,31 @@ _delete_status = delete_status
# 代码清单 8-14
#
def delete_status(conn, uid, status_id):
- key = 'status:%s' % status_id
- lock = acquire_lock_with_timeout(conn, key, 1)
- if not lock:
- return None
+ key = 'status:%s' % status_id
+ lock = acquire_lock_with_timeout(conn, key, 1)
+ if not lock:
+ return None
- if conn.hget(key, 'uid') != str(uid):
- release_lock(conn, key, lock)
- return None
+ if conn.hget(key, 'uid') != str(uid):
+ release_lock(conn, key, lock)
+ return None
- pipeline = conn.pipeline(True)
- # 获取状态消息,
- # 以便流过滤器可以通过执行相同的过滤器来判断是否需要将被删除的消息传递给客户端。
- status = conn.hgetall(key)
- # 将状态消息标记为“已被删除”。
- status['deleted'] = True
- # 将已被删除的状态消息发送到流里面。
- pipeline.publish('streaming:status:', json.dumps(status))
- pipeline.delete(key)
- pipeline.zrem('profile:%s' % uid, status_id)
- pipeline.zrem('home:%s' % uid, status_id)
- pipeline.hincrby('user:%s' % uid, 'posts', -1)
- pipeline.execute()
+ pipeline = conn.pipeline(True)
+ # 获取状态消息,
+ # 以便流过滤器可以通过执行相同的过滤器来判断是否需要将被删除的消息传递给客户端。
+ status = conn.hgetall(key)
+ # 将状态消息标记为“已被删除”。
+ status['deleted'] = True
+ # 将已被删除的状态消息发送到流里面。
+ pipeline.publish('streaming:status:', json.dumps(status))
+ pipeline.delete(key)
+ pipeline.zrem('profile:%s' % uid, status_id)
+ pipeline.zrem('home:%s' % uid, status_id)
+ pipeline.hincrby('user:%s' % uid, 'posts', -1)
+ pipeline.execute()
- release_lock(conn, key, lock)
- return True
+ release_lock(conn, key, lock)
+ return True
#
@@ -756,37 +757,37 @@ def delete_status(conn, uid, status_id):
# 使用第 5 章介绍的自动连接装饰器。
@redis_connection('social-network')
def filter_content(conn, id, method, name, args, quit):
- # 创建一个过滤器,让它来判断是否应该将消息发送给客户端。
- match = create_filters(id, method, name, args)
+ # 创建一个过滤器,让它来判断是否应该将消息发送给客户端。
+ match = create_filters(id, method, name, args)
- # 执行订阅前的准备工作。
- pubsub = conn.pubsub()
- pubsub.subscribe(['streaming:status:'])
+ # 执行订阅前的准备工作。
+ pubsub = conn.pubsub()
+ pubsub.subscribe(['streaming:status:'])
- # 通过订阅来获取消息。
- for item in pubsub.listen():
- # 从订阅结构中取出状态消息。
- message = item['data']
- decoded = json.loads(message)
+ # 通过订阅来获取消息。
+ for item in pubsub.listen():
+ # 从订阅结构中取出状态消息。
+ message = item['data']
+ decoded = json.loads(message)
- # 检查状态消息是否与过滤器相匹配。
- if match(decoded):
- # 在发送被删除的消息之前,
- # 先给消息添加一个特殊的“已被删除”占位符。
- if decoded.get('deleted'):
- yield json.dumps({
- 'id': decoded['id'], 'deleted': True})
- else:
- # 对于未被删除的消息,程序直接发送消息本身。
- yield message
+ # 检查状态消息是否与过滤器相匹配。
+ if match(decoded):
+ # 在发送被删除的消息之前,
+ # 先给消息添加一个特殊的“已被删除”占位符。
+ if decoded.get('deleted'):
+ yield json.dumps({
+ 'id': decoded['id'], 'deleted': True})
+ else:
+ # 对于未被删除的消息,程序直接发送消息本身。
+ yield message
- # 如果服务器与客户端之间的连接已经断开,那么停止过滤消息。
- if quit[0]:
- break
+ # 如果服务器与客户端之间的连接已经断开,那么停止过滤消息。
+ if quit[0]:
+ break
- # 重置 Redis 连接,
- # 清空因为连接速度不够快而滞留在 Redis 服务器输出缓冲区里面的数据。
- pubsub.reset()
+ # 重置 Redis 连接,
+ # 清空因为连接速度不够快而滞留在 Redis 服务器输出缓冲区里面的数据。
+ pubsub.reset()
#
@@ -795,18 +796,18 @@ def filter_content(conn, id, method, name, args, quit):
# 代码清单 8-16
#
def create_filters(id, method, name, args):
- # sample 方法不需要用到 name 参数,
- # 只需要给定 id 参数和 args 参数即可。
- if method == 'sample':
- return SampleFilter(id, args)
- elif name == 'track': # filter 方法需要创建并返回用户指定的过滤器。
- return TrackFilter(args) #
- elif name == 'follow': #
- return FollowFilter(args) #
- elif name == 'location': #
- return LocationFilter(args) #
- # 如果没有任何过滤器被选中,那么引发一个异常。
- raise Exception("Unknown filter")
+ # sample 方法不需要用到 name 参数,
+ # 只需要给定 id 参数和 args 参数即可。
+ if method == 'sample':
+ return SampleFilter(id, args)
+ elif name == 'track': # filter 方法需要创建并返回用户指定的过滤器。
+ return TrackFilter(args) #
+ elif name == 'follow': #
+ return FollowFilter(args) #
+ elif name == 'location': #
+ return LocationFilter(args) #
+ # 如果没有任何过滤器被选中,那么引发一个异常。
+ raise Exception("Unknown filter")
#
@@ -816,26 +817,26 @@ def create_filters(id, method, name, args):
#
# 定义一个 SampleFilter 函数,它接受 id 和 args 两个参数。
def SampleFilter(id, args):
- # args 参数是一个字典,它来源于 GET 请求传递的参数。
- percent = int(args.get('percent', ['10'])[0], 10)
- # 使用 id 参数来随机地选择其中一部分消息 ID ,
- # 被选中 ID 的数量由传入的 percent 参数决定。
- ids = range(100)
- shuffler = random.Random(id)
- shuffler.shuffle(ids)
- # 使用 Python 集合来快速地判断给定的状态消息是否符合过滤器的标准。
- keep = set(ids[:max(percent, 1)])
+ # args 参数是一个字典,它来源于 GET 请求传递的参数。
+ percent = int(args.get('percent', ['10'])[0], 10)
+ # 使用 id 参数来随机地选择其中一部分消息 ID ,
+ # 被选中 ID 的数量由传入的 percent 参数决定。
+ ids = range(100)
+ shuffler = random.Random(id)
+ shuffler.shuffle(ids)
+ # 使用 Python 集合来快速地判断给定的状态消息是否符合过滤器的标准。
+ keep = set(ids[:max(percent, 1)])
- # 创建并返回一个闭包函数,
- # 这个函数就是被创建出来的随机取样消息过滤器。
- def check(status):
- # 为了对状态消息进行过滤,
- # 程序会获取给定状态消息的 ID ,
- # 并将 ID 的值取模 100 ,
- # 然后通过检查取模结果是否存在于 keep 集合来判断给定的状态消息是否符合过滤器的标准。
- return (status['id'] % 100) in keep
+ # 创建并返回一个闭包函数,
+ # 这个函数就是被创建出来的随机取样消息过滤器。
+ def check(status):
+ # 为了对状态消息进行过滤,
+ # 程序会获取给定状态消息的 ID ,
+ # 并将 ID 的值取模 100 ,
+ # 然后通过检查取模结果是否存在于 keep 集合来判断给定的状态消息是否符合过滤器的标准。
+ return (status['id'] % 100) in keep
- return check
+ return check
#
@@ -844,28 +845,28 @@ def SampleFilter(id, args):
# 代码清单 8-18
#
def TrackFilter(list_of_strings):
- # 函数接受一个由词组构成的列表为参数,
- # 如果一条状态消息包含某个词组里面的所有单词,
- # 那么这条消息就与过滤器相匹配。
- groups = []
- for group in list_of_strings:
- group = set(group.lower().split())
- if group:
- # 每个词组至少需要包含一个单词。
- groups.append(group)
+ # 函数接受一个由词组构成的列表为参数,
+ # 如果一条状态消息包含某个词组里面的所有单词,
+ # 那么这条消息就与过滤器相匹配。
+ groups = []
+ for group in list_of_strings:
+ group = set(group.lower().split())
+ if group:
+ # 每个词组至少需要包含一个单词。
+ groups.append(group)
- def check(status):
- # 以空格为分隔符,从消息里面分割出多个单词。
- message_words = set(status['message'].lower().split())
- # 遍历所有词组。
- for group in groups:
- # 如果某个词组的所有单词都在消息里面出现了,
- # 那么过滤器将接受(accept)这条消息。
- if len(group & message_words) == len(group):
- return True
- return False
+ def check(status):
+ # 以空格为分隔符,从消息里面分割出多个单词。
+ message_words = set(status['message'].lower().split())
+ # 遍历所有词组。
+ for group in groups:
+ # 如果某个词组的所有单词都在消息里面出现了,
+ # 那么过滤器将接受(accept)这条消息。
+ if len(group & message_words) == len(group):
+ return True
+ return False
- return check
+ return check
#
@@ -874,22 +875,22 @@ def TrackFilter(list_of_strings):
# 代码清单 8-19
#
def FollowFilter(names):
- # 过滤器会根据给定的用户名,对消息内容以及消息的发送者进行匹配。
- nset = set()
- # 以“@用户名”的形式储存所有给定用户的名字。
- for name in names:
- nset.add('@' + name.lower().lstrip('@'))
+ # 过滤器会根据给定的用户名,对消息内容以及消息的发送者进行匹配。
+ nset = set()
+ # 以“@用户名”的形式储存所有给定用户的名字。
+ for name in names:
+ nset.add('@' + name.lower().lstrip('@'))
- def check(status):
- # 根据消息内容以及消息发布者的名字,构建一个由空格分割的词组。
- message_words = set(status['message'].lower().split())
- message_words.add('@' + status['login'].lower())
+ def check(status):
+ # 根据消息内容以及消息发布者的名字,构建一个由空格分割的词组。
+ message_words = set(status['message'].lower().split())
+ message_words.add('@' + status['login'].lower())
- # 如果给定的用户名与词组中的某个词语相同,
- # 那么这条消息与过滤器相匹配。
- return message_words & nset
+ # 如果给定的用户名与词组中的某个词语相同,
+ # 那么这条消息与过滤器相匹配。
+ return message_words & nset
- return check
+ return check
#
@@ -898,31 +899,31 @@ def FollowFilter(names):
# 代码清单 8-20
#
def LocationFilter(list_of_boxes):
- # 创建一个区域集合,这个集合定义了过滤器接受的消息来自于哪些区域。
- boxes = []
- for start in xrange(0, len(list_of_boxes) - 3, 4):
- boxes.append(map(float, list_of_boxes[start:start + 4]))
+ # 创建一个区域集合,这个集合定义了过滤器接受的消息来自于哪些区域。
+ boxes = []
+ for start in xrange(0, len(list_of_boxes) - 3, 4):
+ boxes.append(map(float, list_of_boxes[start:start + 4]))
- def check(self, status):
- # 尝试从状态消息里面取出位置数据。
- location = status.get('location')
- # 如果消息未包含任何位置数据,
- # 那么这条消息不在任何区域的范围之内。
- if not location:
- return False
+ def check(self, status):
+ # 尝试从状态消息里面取出位置数据。
+ location = status.get('location')
+ # 如果消息未包含任何位置数据,
+ # 那么这条消息不在任何区域的范围之内。
+ if not location:
+ return False
- # 如果消息包含位置数据,那么取出纬度和经度。
- lat, lon = map(float, location.split(','))
- # 遍历所有区域,尝试进行匹配。
- for box in self.boxes:
- # 如果状态消息的位置在给定区域的经纬度范围之内,
- # 那么这条状态消息与过滤器相匹配。
- if (box[1] <= lat <= box[3] and
- box[0] <= lon <= box[2]):
- return True
- return False
+ # 如果消息包含位置数据,那么取出纬度和经度。
+ lat, lon = map(float, location.split(','))
+ # 遍历所有区域,尝试进行匹配。
+ for box in self.boxes:
+ # 如果状态消息的位置在给定区域的经纬度范围之内,
+ # 那么这条状态消息与过滤器相匹配。
+ if (box[1] <= lat <= box[3] and
+ box[0] <= lon <= box[2]):
+ return True
+ return False
- return check
+ return check
#
@@ -931,12 +932,12 @@ _filter_content = filter_content
def filter_content(identifier, method, name, args, quit):
- print "got:", identifier, method, name, args
- for i in xrange(10):
- yield json.dumps({'id': i})
- if quit[0]:
- break
- time.sleep(.1)
+ print "got:", identifier, method, name, args
+ for i in xrange(10):
+ yield json.dumps({'id': i})
+ if quit[0]:
+ break
+ time.sleep(.1)
'''
@@ -951,100 +952,100 @@ if __name__ == '__main__': # 如果这个模块是以命令行
class TestCh08(unittest.TestCase):
- def setUp(self):
- self.conn = redis.Redis(db=15)
- self.conn.flushdb()
+ def setUp(self):
+ self.conn = redis.Redis(db=15)
+ self.conn.flushdb()
- def tearDown(self):
- self.conn.flushdb()
+ def tearDown(self):
+ self.conn.flushdb()
- def test_create_user_and_status(self):
- self.assertEquals(create_user(self.conn, 'TestUser', 'Test User'), 1)
- self.assertEquals(create_user(self.conn, 'TestUser', 'Test User2'), None)
+ def test_create_user_and_status(self):
+ self.assertEquals(create_user(self.conn, 'TestUser', 'Test User'), 1)
+ self.assertEquals(create_user(self.conn, 'TestUser', 'Test User2'), None)
- self.assertEquals(create_status(self.conn, 1, "This is a new status message"), 1)
- self.assertEquals(self.conn.hget('user:1', 'posts'), '1')
+ self.assertEquals(create_status(self.conn, 1, "This is a new status message"), 1)
+ self.assertEquals(self.conn.hget('user:1', 'posts'), '1')
- def test_follow_unfollow_user(self):
- self.assertEquals(create_user(self.conn, 'TestUser', 'Test User'), 1)
- self.assertEquals(create_user(self.conn, 'TestUser2', 'Test User2'), 2)
+ def test_follow_unfollow_user(self):
+ self.assertEquals(create_user(self.conn, 'TestUser', 'Test User'), 1)
+ self.assertEquals(create_user(self.conn, 'TestUser2', 'Test User2'), 2)
- self.assertTrue(follow_user(self.conn, 1, 2))
- self.assertEquals(self.conn.zcard('followers:2'), 1)
- self.assertEquals(self.conn.zcard('followers:1'), 0)
- self.assertEquals(self.conn.zcard('following:1'), 1)
- self.assertEquals(self.conn.zcard('following:2'), 0)
- self.assertEquals(self.conn.hget('user:1', 'following'), '1')
- self.assertEquals(self.conn.hget('user:2', 'following'), '0')
- self.assertEquals(self.conn.hget('user:1', 'followers'), '0')
- self.assertEquals(self.conn.hget('user:2', 'followers'), '1')
+ self.assertTrue(follow_user(self.conn, 1, 2))
+ self.assertEquals(self.conn.zcard('followers:2'), 1)
+ self.assertEquals(self.conn.zcard('followers:1'), 0)
+ self.assertEquals(self.conn.zcard('following:1'), 1)
+ self.assertEquals(self.conn.zcard('following:2'), 0)
+ self.assertEquals(self.conn.hget('user:1', 'following'), '1')
+ self.assertEquals(self.conn.hget('user:2', 'following'), '0')
+ self.assertEquals(self.conn.hget('user:1', 'followers'), '0')
+ self.assertEquals(self.conn.hget('user:2', 'followers'), '1')
- self.assertEquals(unfollow_user(self.conn, 2, 1), None)
- self.assertEquals(unfollow_user(self.conn, 1, 2), True)
- self.assertEquals(self.conn.zcard('followers:2'), 0)
- self.assertEquals(self.conn.zcard('followers:1'), 0)
- self.assertEquals(self.conn.zcard('following:1'), 0)
- self.assertEquals(self.conn.zcard('following:2'), 0)
- self.assertEquals(self.conn.hget('user:1', 'following'), '0')
- self.assertEquals(self.conn.hget('user:2', 'following'), '0')
- self.assertEquals(self.conn.hget('user:1', 'followers'), '0')
- self.assertEquals(self.conn.hget('user:2', 'followers'), '0')
+ self.assertEquals(unfollow_user(self.conn, 2, 1), None)
+ self.assertEquals(unfollow_user(self.conn, 1, 2), True)
+ self.assertEquals(self.conn.zcard('followers:2'), 0)
+ self.assertEquals(self.conn.zcard('followers:1'), 0)
+ self.assertEquals(self.conn.zcard('following:1'), 0)
+ self.assertEquals(self.conn.zcard('following:2'), 0)
+ self.assertEquals(self.conn.hget('user:1', 'following'), '0')
+ self.assertEquals(self.conn.hget('user:2', 'following'), '0')
+ self.assertEquals(self.conn.hget('user:1', 'followers'), '0')
+ self.assertEquals(self.conn.hget('user:2', 'followers'), '0')
- def test_syndicate_status(self):
- self.assertEquals(create_user(self.conn, 'TestUser', 'Test User'), 1)
- self.assertEquals(create_user(self.conn, 'TestUser2', 'Test User2'), 2)
- self.assertTrue(follow_user(self.conn, 1, 2))
- self.assertEquals(self.conn.zcard('followers:2'), 1)
- self.assertEquals(self.conn.hget('user:1', 'following'), '1')
- self.assertEquals(post_status(self.conn, 2, 'this is some message content'), 1)
- self.assertEquals(len(get_status_messages(self.conn, 1)), 1)
+ def test_syndicate_status(self):
+ self.assertEquals(create_user(self.conn, 'TestUser', 'Test User'), 1)
+ self.assertEquals(create_user(self.conn, 'TestUser2', 'Test User2'), 2)
+ self.assertTrue(follow_user(self.conn, 1, 2))
+ self.assertEquals(self.conn.zcard('followers:2'), 1)
+ self.assertEquals(self.conn.hget('user:1', 'following'), '1')
+ self.assertEquals(post_status(self.conn, 2, 'this is some message content'), 1)
+ self.assertEquals(len(get_status_messages(self.conn, 1)), 1)
- for i in xrange(3, 11):
- self.assertEquals(create_user(self.conn, 'TestUser%s' % i, 'Test User%s' % i), i)
- follow_user(self.conn, i, 2)
+ for i in xrange(3, 11):
+ self.assertEquals(create_user(self.conn, 'TestUser%s' % i, 'Test User%s' % i), i)
+ follow_user(self.conn, i, 2)
- global POSTS_PER_PASS
- POSTS_PER_PASS = 5
+ global POSTS_PER_PASS
+ POSTS_PER_PASS = 5
- self.assertEquals(post_status(self.conn, 2, 'this is some other message content'), 2)
- time.sleep(.1)
- self.assertEquals(len(get_status_messages(self.conn, 9)), 2)
+ self.assertEquals(post_status(self.conn, 2, 'this is some other message content'), 2)
+ time.sleep(.1)
+ self.assertEquals(len(get_status_messages(self.conn, 9)), 2)
- self.assertTrue(unfollow_user(self.conn, 1, 2))
- self.assertEquals(len(get_status_messages(self.conn, 1)), 0)
+ self.assertTrue(unfollow_user(self.conn, 1, 2))
+ self.assertEquals(len(get_status_messages(self.conn, 1)), 0)
- def test_refill_timeline(self):
- self.assertEquals(create_user(self.conn, 'TestUser', 'Test User'), 1)
- self.assertEquals(create_user(self.conn, 'TestUser2', 'Test User2'), 2)
- self.assertEquals(create_user(self.conn, 'TestUser3', 'Test User3'), 3)
+ def test_refill_timeline(self):
+ self.assertEquals(create_user(self.conn, 'TestUser', 'Test User'), 1)
+ self.assertEquals(create_user(self.conn, 'TestUser2', 'Test User2'), 2)
+ self.assertEquals(create_user(self.conn, 'TestUser3', 'Test User3'), 3)
- self.assertTrue(follow_user(self.conn, 1, 2))
- self.assertTrue(follow_user(self.conn, 1, 3))
+ self.assertTrue(follow_user(self.conn, 1, 2))
+ self.assertTrue(follow_user(self.conn, 1, 3))
- global HOME_TIMELINE_SIZE
- HOME_TIMELINE_SIZE = 5
+ global HOME_TIMELINE_SIZE
+ HOME_TIMELINE_SIZE = 5
- for i in xrange(10):
- self.assertTrue(post_status(self.conn, 2, 'message'))
- self.assertTrue(post_status(self.conn, 3, 'message'))
- time.sleep(.05)
+ for i in xrange(10):
+ self.assertTrue(post_status(self.conn, 2, 'message'))
+ self.assertTrue(post_status(self.conn, 3, 'message'))
+ time.sleep(.05)
- self.assertEquals(len(get_status_messages(self.conn, 1)), 5)
- self.assertTrue(unfollow_user(self.conn, 1, 2))
- self.assertTrue(len(get_status_messages(self.conn, 1)) < 5)
+ self.assertEquals(len(get_status_messages(self.conn, 1)), 5)
+ self.assertTrue(unfollow_user(self.conn, 1, 2))
+ self.assertTrue(len(get_status_messages(self.conn, 1)) < 5)
- refill_timeline(self.conn, 'following:1', 'home:1')
- messages = get_status_messages(self.conn, 1)
- self.assertEquals(len(messages), 5)
- for msg in messages:
- self.assertEquals(msg['uid'], '3')
+ refill_timeline(self.conn, 'following:1', 'home:1')
+ messages = get_status_messages(self.conn, 1)
+ self.assertEquals(len(messages), 5)
+ for msg in messages:
+ self.assertEquals(msg['uid'], '3')
- delete_status(self.conn, '3', messages[-1]['id'])
- self.assertEquals(len(get_status_messages(self.conn, 1)), 4)
- self.assertEquals(self.conn.zcard('home:1'), 5)
- clean_timelines(self.conn, '3', messages[-1]['id'])
- self.assertEquals(self.conn.zcard('home:1'), 4)
+ delete_status(self.conn, '3', messages[-1]['id'])
+ self.assertEquals(len(get_status_messages(self.conn, 1)), 4)
+ self.assertEquals(self.conn.zcard('home:1'), 5)
+ clean_timelines(self.conn, '3', messages[-1]['id'])
+ self.assertEquals(self.conn.zcard('home:1'), 4)
if __name__ == '__main__':
- unittest.main()
+ unittest.main()
diff --git a/codes/redis/redis-in-action-py/ch09_listing_source.py b/codes/redis/redis-in-action-py/ch09_listing_source.py
index 94254a3..6dc42d1 100644
--- a/codes/redis/redis-in-action-py/ch09_listing_source.py
+++ b/codes/redis/redis-in-action-py/ch09_listing_source.py
@@ -12,14 +12,14 @@ from datetime import date, timedelta
def readblocks(conn, key, blocksize=2 ** 17):
- lb = blocksize
- pos = 0
- while lb == blocksize: # A
- block = conn.substr(key, pos, pos + blocksize - 1) # B
- yield block # C
- lb = len(block) # C
- pos += lb # C
- yield ''
+ lb = blocksize
+ pos = 0
+ while lb == blocksize: # A
+ block = conn.substr(key, pos, pos + blocksize - 1) # B
+ yield block # C
+ lb = len(block) # C
+ pos += lb # C
+ yield ''
# 代码清单 9-1
@@ -92,27 +92,27 @@ set-max-intset-entries 512 # 集合使用整数集合表示的限制条件
#
# 为了以不同的方式进行性能测试,函数需要对所有测试指标进行参数化处理。
def long_ziplist_performance(conn, key, length, passes, psize):
- # 删除指定的键,确保被测试数据的准确性。
- conn.delete(key)
- # 通过从右端推入指定数量的元素来对列表进行初始化。
- conn.rpush(key, *range(length))
- # 通过流水线来降低网络通信给测试带来的影响。
- pipeline = conn.pipeline(False)
+ # 删除指定的键,确保被测试数据的准确性。
+ conn.delete(key)
+ # 通过从右端推入指定数量的元素来对列表进行初始化。
+ conn.rpush(key, *range(length))
+ # 通过流水线来降低网络通信给测试带来的影响。
+ pipeline = conn.pipeline(False)
- # 启动计时器。
- t = time.time()
- # 根据 passes 参数来决定流水线操作的执行次数。
- for p in xrange(passes):
- # 每个流水线操作都包含了 psize 次 RPOPLPUSH 命令调用。
- for pi in xrange(psize):
- # 每个 rpoplpush() 函数调用都会将列表最右端的元素弹出,
- # 并将它推入到同一个列表的左端。
- pipeline.rpoplpush(key, key)
- # 执行 psize 次 RPOPLPUSH 命令。
- pipeline.execute()
+ # 启动计时器。
+ t = time.time()
+ # 根据 passes 参数来决定流水线操作的执行次数。
+ for p in xrange(passes):
+ # 每个流水线操作都包含了 psize 次 RPOPLPUSH 命令调用。
+ for pi in xrange(psize):
+ # 每个 rpoplpush() 函数调用都会将列表最右端的元素弹出,
+ # 并将它推入到同一个列表的左端。
+ pipeline.rpoplpush(key, key)
+ # 执行 psize 次 RPOPLPUSH 命令。
+ pipeline.execute()
- # 计算每秒钟执行的 RPOPLPUSH 调用数量。
- return (passes * psize) / (time.time() - t or .001)
+ # 计算每秒钟执行的 RPOPLPUSH 调用数量。
+ return (passes * psize) / (time.time() - t or .001)
#
@@ -138,31 +138,31 @@ def long_ziplist_performance(conn, key, length, passes, psize):
def long_ziplist_index(conn, key, length, passes, psize): # A
- conn.delete(key) # B
- conn.rpush(key, *range(length)) # C
- length >>= 1
- pipeline = conn.pipeline(False) # D
- t = time.time() # E
- for p in xrange(passes): # F
- for pi in xrange(psize): # G
- pipeline.lindex(key, length) # H
- pipeline.execute() # I
- return (passes * psize) / (time.time() - t or .001) # J
+ conn.delete(key) # B
+ conn.rpush(key, *range(length)) # C
+ length >>= 1
+ pipeline = conn.pipeline(False) # D
+ t = time.time() # E
+ for p in xrange(passes): # F
+ for pi in xrange(psize): # G
+ pipeline.lindex(key, length) # H
+ pipeline.execute() # I
+ return (passes * psize) / (time.time() - t or .001) # J
def long_intset_performance(conn, key, length, passes, psize): # A
- conn.delete(key) # B
- conn.sadd(key, *range(1000000, 1000000 + length)) # C
- cur = 1000000 - 1
- pipeline = conn.pipeline(False) # D
- t = time.time() # E
- for p in xrange(passes): # F
- for pi in xrange(psize): # G
- pipeline.spop(key) # H
- pipeline.sadd(key, cur)
- cur -= 1
- pipeline.execute() # I
- return (passes * psize) / (time.time() - t or .001) # J
+ conn.delete(key) # B
+ conn.sadd(key, *range(1000000, 1000000 + length)) # C
+ cur = 1000000 - 1
+ pipeline = conn.pipeline(False) # D
+ t = time.time() # E
+ for p in xrange(passes): # F
+ for pi in xrange(psize): # G
+ pipeline.spop(key) # H
+ pipeline.sadd(key, cur)
+ cur -= 1
+ pipeline.execute() # I
+ return (passes * psize) / (time.time() - t or .001) # J
# 代码清单 9-7
@@ -170,24 +170,24 @@ def long_intset_performance(conn, key, length, passes, psize): # A
# 在调用 shard_key() 函数时,
# 用户需要给定基础散列的名字、将要被储存到分片散列里面的键、预计的元素总数量以及请求的分片数量。
def shard_key(base, key, total_elements, shard_size):
- # 如果值是一个整数或者一个看上去像是整数的字符串,
- # 那么它将被直接用于计算分片 ID 。
- if isinstance(key, (int, long)) or key.isdigit():
- # 整数键将被程序假定为连续指派的 ID ,
- # 并基于这个整数 ID 的二进制位的高位来选择分片 ID 。
- # 此外,程序在进行整数转换的时候还使用了显式的基数(以及 str()`` 函数),
- # 使得键 010 可以被转换为 10 ,而不是 8 。
- shard_id = int(str(key), 10) // shard_size
- else:
- # 对于不是整数的键,
- # 程序将基于预计的元素总数量以及请求的分片数量,
- # 计算出实际所需的分片总数量。
- shards = 2 * total_elements // shard_size
- # 在得知了分片的数量之后,
- # 程序就可以通过计算键的散列值与分片数量之间的模数来得到分片 ID 。
- shard_id = binascii.crc32(key) % shards
- # 最后,程序会把基础键和分片 ID 组合在一起,得出分片键。
- return "%s:%s" % (base, shard_id)
+ # 如果值是一个整数或者一个看上去像是整数的字符串,
+ # 那么它将被直接用于计算分片 ID 。
+ if isinstance(key, (int, long)) or key.isdigit():
+ # 整数键将被程序假定为连续指派的 ID ,
+ # 并基于这个整数 ID 的二进制位的高位来选择分片 ID 。
+ # 此外,程序在进行整数转换的时候还使用了显式的基数(以及 str()`` 函数),
+ # 使得键 010 可以被转换为 10 ,而不是 8 。
+ shard_id = int(str(key), 10) // shard_size
+ else:
+ # 对于不是整数的键,
+ # 程序将基于预计的元素总数量以及请求的分片数量,
+ # 计算出实际所需的分片总数量。
+ shards = 2 * total_elements // shard_size
+ # 在得知了分片的数量之后,
+ # 程序就可以通过计算键的散列值与分片数量之间的模数来得到分片 ID 。
+ shard_id = binascii.crc32(key) % shards
+ # 最后,程序会把基础键和分片 ID 组合在一起,得出分片键。
+ return "%s:%s" % (base, shard_id)
#
@@ -196,17 +196,17 @@ def shard_key(base, key, total_elements, shard_size):
# 代码清单 9-8
#
def shard_hset(conn, base, key, value, total_elements, shard_size):
- # 计算出应该由哪个分片来储存值。
- shard = shard_key(base, key, total_elements, shard_size)
- # 将值储存到分片里面。
- return conn.hset(shard, key, value)
+ # 计算出应该由哪个分片来储存值。
+ shard = shard_key(base, key, total_elements, shard_size)
+ # 将值储存到分片里面。
+ return conn.hset(shard, key, value)
def shard_hget(conn, base, key, total_elements, shard_size):
- # 计算出值可能被储存到了哪个分片里面。
- shard = shard_key(base, key, total_elements, shard_size)
- # 取得储存在分片里面的值。
- return conn.hget(shard, key)
+ # 计算出值可能被储存到了哪个分片里面。
+ shard = shard_key(base, key, total_elements, shard_size)
+ # 取得储存在分片里面的值。
+ return conn.hget(shard, key)
#
@@ -237,12 +237,12 @@ def find_city_by_ip(conn, ip_address):
# 代码清单 9-10
#
def shard_sadd(conn, base, member, total_elements, shard_size):
- shard = shard_key(base,
- # 计算成员应该被储存到哪个分片集合里面;
- # 因为成员并非连续 ID ,所以程序在计算成员所属的分片之前,会先将成员转换为字符串。
- 'x' + str(member), total_elements, shard_size)
- # 将成员储存到分片里面。
- return conn.sadd(shard, member)
+ shard = shard_key(base,
+ # 计算成员应该被储存到哪个分片集合里面;
+ # 因为成员并非连续 ID ,所以程序在计算成员所属的分片之前,会先将成员转换为字符串。
+ 'x' + str(member), total_elements, shard_size)
+ # 将成员储存到分片里面。
+ return conn.sadd(shard, member)
#
@@ -255,19 +255,19 @@ SHARD_SIZE = 512
def count_visit(conn, session_id):
- # 取得当天的日期,并生成唯一访客计数器的键。
- today = date.today()
- key = 'unique:%s' % today.isoformat()
- # 计算或者获取当天的预计唯一访客人数。
- expected = get_expected(conn, key, today)
+ # 取得当天的日期,并生成唯一访客计数器的键。
+ today = date.today()
+ key = 'unique:%s' % today.isoformat()
+ # 计算或者获取当天的预计唯一访客人数。
+ expected = get_expected(conn, key, today)
- # 根据 128 位的 UUID ,计算出一个 56 位的 ID 。
- id = int(session_id.replace('-', '')[:15], 16)
- # 将 ID 添加到分片集合里面。
- if shard_sadd(conn, key, id, expected, SHARD_SIZE):
- # 如果 ID 在分片集合里面并不存在,那么对唯一访客计数器执行加一操作。
- conn.incr(key)
- #
+ # 根据 128 位的 UUID ,计算出一个 56 位的 ID 。
+ id = int(session_id.replace('-', '')[:15], 16)
+ # 将 ID 添加到分片集合里面。
+ if shard_sadd(conn, key, id, expected, SHARD_SIZE):
+ # 如果 ID 在分片集合里面并不存在,那么对唯一访客计数器执行加一操作。
+ conn.incr(key)
+ #
# 代码清单 9-12
@@ -279,35 +279,35 @@ EXPECTED = {}
def get_expected(conn, key, today):
- # 如果程序已经计算出或者获取到了当日的预计访客人数,
- # 那么直接使用已计算出的数字。
- if key in EXPECTED:
- return EXPECTED[key]
+ # 如果程序已经计算出或者获取到了当日的预计访客人数,
+ # 那么直接使用已计算出的数字。
+ if key in EXPECTED:
+ return EXPECTED[key]
- exkey = key + ':expected'
- # 如果其他客户端已经计算出了当日的预计访客人数,
- # 那么直接使用已计算出的数字。
- expected = conn.get(exkey)
+ exkey = key + ':expected'
+ # 如果其他客户端已经计算出了当日的预计访客人数,
+ # 那么直接使用已计算出的数字。
+ expected = conn.get(exkey)
- if not expected:
- # 获取昨天的唯一访客人数,如果该数值不存在就使用默认值一百万。
- yesterday = (today - timedelta(days=1)).isoformat()
- expected = conn.get('unique:%s' % yesterday)
- expected = int(expected or DAILY_EXPECTED)
+ if not expected:
+ # 获取昨天的唯一访客人数,如果该数值不存在就使用默认值一百万。
+ yesterday = (today - timedelta(days=1)).isoformat()
+ expected = conn.get('unique:%s' % yesterday)
+ expected = int(expected or DAILY_EXPECTED)
- # 基于“明天的访客人数至少会比今天的访客人数多 50%”这一假设,
- # 给昨天的访客人数加上 50% ,然后向上舍入至下一个底数为 2 的幂。
- expected = 2 ** int(math.ceil(math.log(expected * 1.5, 2)))
- # 将计算出的预计访客人数写入到 Redis 里面,以便其他程序在有需要时使用。
- if not conn.setnx(exkey, expected):
- # 如果在我们之前,
- # 已经有其他客户端储存了当日的预计访客人数,
- # 那么直接使用已储存的数字。
- expected = conn.get(exkey)
+ # 基于“明天的访客人数至少会比今天的访客人数多 50%”这一假设,
+ # 给昨天的访客人数加上 50% ,然后向上舍入至下一个底数为 2 的幂。
+ expected = 2 ** int(math.ceil(math.log(expected * 1.5, 2)))
+ # 将计算出的预计访客人数写入到 Redis 里面,以便其他程序在有需要时使用。
+ if not conn.setnx(exkey, expected):
+ # 如果在我们之前,
+ # 已经有其他客户端储存了当日的预计访客人数,
+ # 那么直接使用已储存的数字。
+ expected = conn.get(exkey)
- # 将当日的预计访客人数记录到本地副本里面,并将它返回给调用者。
- EXPECTED[key] = int(expected)
- return EXPECTED[key]
+ # 将当日的预计访客人数记录到本地副本里面,并将它返回给调用者。
+ EXPECTED[key] = int(expected)
+ return EXPECTED[key]
#
@@ -335,10 +335,10 @@ TCD TGO THA TJK TKL TKM TLS TON TTO TUN TUR TUV TWN TZA UGA UKR UMI URY
USA UZB VAT VCT VEN VGB VIR VNM VUT WLF WSM YEM ZAF ZMB ZWE'''.split()
STATES = {
- # 加拿大的省信息和属地信息。
- 'CAN': '''AB BC MB NB NL NS NT NU ON PE QC SK YT'''.split(),
- # 美国各个州的信息。
- 'USA': '''AA AE AK AL AP AR AS AZ CA CO CT DC DE FL FM GA GU HI IA ID
+ # 加拿大的省信息和属地信息。
+ 'CAN': '''AB BC MB NB NL NS NT NU ON PE QC SK YT'''.split(),
+ # 美国各个州的信息。
+ 'USA': '''AA AE AK AL AP AR AS AZ CA CO CT DC DE FL FM GA GU HI IA ID
IL IN KS KY LA MA MD ME MH MI MN MO MP MS MT NC ND NE NH NJ NM NV NY OH
OK OR PA PR PW RI SC SD TN TX UT VA VI VT WA WI WV WY'''.split(),
}
@@ -350,31 +350,31 @@ OK OR PA PR PW RI SC SD TN TX UT VA VI VT WA WI WV WY'''.split(),
# 代码清单 9-14
#
def get_code(country, state):
- # 寻找国家对应的偏移量。
- cindex = bisect.bisect_left(COUNTRIES, country)
- # 没有找到指定的国家时,将索引设置为 -1 。
- if cindex > len(COUNTRIES) or COUNTRIES[cindex] != country:
- cindex = -1
- # 因为 Redis 里面的未初始化数据在返回时会被转换为空值,
- # 所以我们要将“未找到指定国家”时的返回值改为 0 ,
- # 并将第一个国家的索引变为 1 ,以此类推。
- cindex += 1
+ # 寻找国家对应的偏移量。
+ cindex = bisect.bisect_left(COUNTRIES, country)
+ # 没有找到指定的国家时,将索引设置为 -1 。
+ if cindex > len(COUNTRIES) or COUNTRIES[cindex] != country:
+ cindex = -1
+ # 因为 Redis 里面的未初始化数据在返回时会被转换为空值,
+ # 所以我们要将“未找到指定国家”时的返回值改为 0 ,
+ # 并将第一个国家的索引变为 1 ,以此类推。
+ cindex += 1
- sindex = -1
- if state and country in STATES:
- # 尝试取出国家对应的州信息。
- states = STATES[country]
- # 寻找州对应的偏移量。
- sindex = bisect.bisect_left(states, state)
- # 像处理“未找到指定国家”时的情况一样,处理“未找到指定州”的情况。
- if sindex > len(states) or states[sindex] != state:
- sindex = -1
- # 如果没有找到指定的州,那么索引为 0 ;
- # 如果找到了指定的州,那么索引大于 0 。
- sindex += 1
+ sindex = -1
+ if state and country in STATES:
+ # 尝试取出国家对应的州信息。
+ states = STATES[country]
+ # 寻找州对应的偏移量。
+ sindex = bisect.bisect_left(states, state)
+ # 像处理“未找到指定国家”时的情况一样,处理“未找到指定州”的情况。
+ if sindex > len(states) or states[sindex] != state:
+ sindex = -1
+ # 如果没有找到指定的州,那么索引为 0 ;
+ # 如果找到了指定的州,那么索引大于 0 。
+ sindex += 1
- # chr() 函数会将介于 0 至 255 之间的整数值转换为对应的 ASCII 字符。
- return chr(cindex) + chr(sindex)
+ # chr() 函数会将介于 0 至 255 之间的整数值转换为对应的 ASCII 字符。
+ return chr(cindex) + chr(sindex)
#
@@ -387,26 +387,26 @@ USERS_PER_SHARD = 2 ** 20
def set_location(conn, user_id, country, state):
- # 取得用户所在位置的编码。
- code = get_code(country, state)
+ # 取得用户所在位置的编码。
+ code = get_code(country, state)
- # 查找分片 ID 以及用户在指定分片中的位置(position)。
- shard_id, position = divmod(user_id, USERS_PER_SHARD)
- # 计算用户数据的偏移量。
- offset = position * 2
+ # 查找分片 ID 以及用户在指定分片中的位置(position)。
+ shard_id, position = divmod(user_id, USERS_PER_SHARD)
+ # 计算用户数据的偏移量。
+ offset = position * 2
- pipe = conn.pipeline(False)
- # 将用户的位置信息储存到分片后的位置表格里面。
- pipe.setrange('location:%s' % shard_id, offset, code)
+ pipe = conn.pipeline(False)
+ # 将用户的位置信息储存到分片后的位置表格里面。
+ pipe.setrange('location:%s' % shard_id, offset, code)
- # 对记录目前已知最大用户 ID 的有序集合进行更新。
- tkey = str(uuid.uuid4())
- pipe.zadd(tkey, 'max', user_id)
- pipe.zunionstore('location:max',
- [tkey, 'location:max'], aggregate='max')
- pipe.delete(tkey)
+ # 对记录目前已知最大用户 ID 的有序集合进行更新。
+ tkey = str(uuid.uuid4())
+ pipe.zadd(tkey, 'max', user_id)
+ pipe.zunionstore('location:max',
+ [tkey, 'location:max'], aggregate='max')
+ pipe.delete(tkey)
- pipe.execute()
+ pipe.execute()
#
@@ -415,29 +415,29 @@ def set_location(conn, user_id, country, state):
# 代码清单 9-16
#
def aggregate_location(conn):
- # 初始化两个特殊结构,
- # 以便快速地对已存在的计数器以及缺失的计数器进行更新。
- countries = defaultdict(int)
- states = defaultdict(lambda: defaultdict(int))
+ # 初始化两个特殊结构,
+ # 以便快速地对已存在的计数器以及缺失的计数器进行更新。
+ countries = defaultdict(int)
+ states = defaultdict(lambda: defaultdict(int))
- # 获取目前已知的最大用户 ID ,
- # 并使用它来计算出程序需要访问的最大分片 ID 。
- max_id = int(conn.zscore('location:max', 'max'))
- max_block = max_id // USERS_PER_SHARD
+ # 获取目前已知的最大用户 ID ,
+ # 并使用它来计算出程序需要访问的最大分片 ID 。
+ max_id = int(conn.zscore('location:max', 'max'))
+ max_block = max_id // USERS_PER_SHARD
- # 按顺序地处理每个分片……
- for shard_id in xrange(max_block + 1):
- # 读取每个块……
- for block in readblocks(conn, 'location:%s' % shard_id):
- # 从块里面提取出每个编码,
- # 并根据编码查找原始的位置信息,
- # 然后对这些位置信息进行聚合计算。
- for offset in xrange(0, len(block) - 1, 2):
- code = block[offset:offset + 2]
- # 对聚合数据进行更新。
- update_aggregates(countries, states, [code])
+ # 按顺序地处理每个分片……
+ for shard_id in xrange(max_block + 1):
+ # 读取每个块……
+ for block in readblocks(conn, 'location:%s' % shard_id):
+ # 从块里面提取出每个编码,
+ # 并根据编码查找原始的位置信息,
+ # 然后对这些位置信息进行聚合计算。
+ for offset in xrange(0, len(block) - 1, 2):
+ code = block[offset:offset + 2]
+ # 对聚合数据进行更新。
+ update_aggregates(countries, states, [code])
- return countries, states
+ return countries, states
#
@@ -446,145 +446,145 @@ def aggregate_location(conn):
# 代码清单 9-17
#
def update_aggregates(countries, states, codes):
- for code in codes:
- # 只对合法的编码进行查找。
- if len(code) != 2:
- continue
+ for code in codes:
+ # 只对合法的编码进行查找。
+ if len(code) != 2:
+ continue
- # 计算出国家和州在查找表格中的实际偏移量。
- country = ord(code[0]) - 1
- state = ord(code[1]) - 1
+ # 计算出国家和州在查找表格中的实际偏移量。
+ country = ord(code[0]) - 1
+ state = ord(code[1]) - 1
- # 如果国家所处的偏移量不在合法范围之内,那么跳过这个编码。
- if country < 0 or country >= len(COUNTRIES):
- continue
+ # 如果国家所处的偏移量不在合法范围之内,那么跳过这个编码。
+ if country < 0 or country >= len(COUNTRIES):
+ continue
- # 获取 ISO3 国家编码。
- country = COUNTRIES[country]
- # 在对国家信息进行解码之后,
- # 把用户计入到这个国家对应的计数器里面。
- countries[country] += 1
+ # 获取 ISO3 国家编码。
+ country = COUNTRIES[country]
+ # 在对国家信息进行解码之后,
+ # 把用户计入到这个国家对应的计数器里面。
+ countries[country] += 1
- # 如果程序没有找到指定的州信息,
- # 或者查找州信息时的偏移量不在合法的范围之内,
- # 那么跳过这个编码。
- if country not in STATES:
- continue
- if state < 0 or state >= STATES[country]:
- continue
+ # 如果程序没有找到指定的州信息,
+ # 或者查找州信息时的偏移量不在合法的范围之内,
+ # 那么跳过这个编码。
+ if country not in STATES:
+ continue
+ if state < 0 or state >= STATES[country]:
+ continue
- # 根据编码获取州名。
- state = STATES[country][state]
- # 对州计数器执行加一操作。
- states[country][state] += 1
- #
+ # 根据编码获取州名。
+ state = STATES[country][state]
+ # 对州计数器执行加一操作。
+ states[country][state] += 1
+ #
# 代码清单 9-18
#
def aggregate_location_list(conn, user_ids):
- # 设置流水线,减少操作执行过程中与 Redis 的通信往返次数。
- pipe = conn.pipeline(False)
- # 和之前一样,设置好基本的聚合数据。
- countries = defaultdict(int)
- states = defaultdict(lambda: defaultdict(int))
+ # 设置流水线,减少操作执行过程中与 Redis 的通信往返次数。
+ pipe = conn.pipeline(False)
+ # 和之前一样,设置好基本的聚合数据。
+ countries = defaultdict(int)
+ states = defaultdict(lambda: defaultdict(int))
- for i, user_id in enumerate(user_ids):
- # 查找用户位置信息所在分片的 ID ,以及信息在分片中的偏移量。
- shard_id, position = divmod(user_id, USERS_PER_SHARD)
- offset = position * 2
+ for i, user_id in enumerate(user_ids):
+ # 查找用户位置信息所在分片的 ID ,以及信息在分片中的偏移量。
+ shard_id, position = divmod(user_id, USERS_PER_SHARD)
+ offset = position * 2
- # 发送另一个被流水线包裹的命令,获取用户的位置信息。
- pipe.substr('location:%s' % shard_id, offset, offset + 1)
+ # 发送另一个被流水线包裹的命令,获取用户的位置信息。
+ pipe.substr('location:%s' % shard_id, offset, offset + 1)
- # 每处理 1000 个请求,
- # 程序就会调用之前定义的辅助函数对聚合数据进行一次更新。
- if (i + 1) % 1000 == 0:
- update_aggregates(countries, states, pipe.execute())
+ # 每处理 1000 个请求,
+ # 程序就会调用之前定义的辅助函数对聚合数据进行一次更新。
+ if (i + 1) % 1000 == 0:
+ update_aggregates(countries, states, pipe.execute())
- # 对遍历余下的最后一批用户进行处理。
- update_aggregates(countries, states, pipe.execute())
+ # 对遍历余下的最后一批用户进行处理。
+ update_aggregates(countries, states, pipe.execute())
- # 返回聚合数据。
- return countries, states
+ # 返回聚合数据。
+ return countries, states
#
class TestCh09(unittest.TestCase):
- def setUp(self):
- self.conn = redis.Redis(db=15)
- self.conn.flushdb()
+ def setUp(self):
+ self.conn = redis.Redis(db=15)
+ self.conn.flushdb()
- def tearDown(self):
- self.conn.flushdb()
+ def tearDown(self):
+ self.conn.flushdb()
- def test_long_ziplist_performance(self):
- long_ziplist_performance(self.conn, 'test', 5, 10, 10)
- self.assertEquals(self.conn.llen('test'), 5)
+ def test_long_ziplist_performance(self):
+ long_ziplist_performance(self.conn, 'test', 5, 10, 10)
+ self.assertEquals(self.conn.llen('test'), 5)
- def test_shard_key(self):
- base = 'test'
- self.assertEquals(shard_key(base, 1, 2, 2), 'test:0')
- self.assertEquals(shard_key(base, '1', 2, 2), 'test:0')
- self.assertEquals(shard_key(base, 125, 1000, 100), 'test:1')
- self.assertEquals(shard_key(base, '125', 1000, 100), 'test:1')
+ def test_shard_key(self):
+ base = 'test'
+ self.assertEquals(shard_key(base, 1, 2, 2), 'test:0')
+ self.assertEquals(shard_key(base, '1', 2, 2), 'test:0')
+ self.assertEquals(shard_key(base, 125, 1000, 100), 'test:1')
+ self.assertEquals(shard_key(base, '125', 1000, 100), 'test:1')
- for i in xrange(50):
- self.assertTrue(0 <= int(shard_key(base, 'hello:%s' % i, 1000, 100).partition(':')[-1]) < 20)
- self.assertTrue(0 <= int(shard_key(base, i, 1000, 100).partition(':')[-1]) < 10)
+ for i in xrange(50):
+ self.assertTrue(0 <= int(shard_key(base, 'hello:%s' % i, 1000, 100).partition(':')[-1]) < 20)
+ self.assertTrue(0 <= int(shard_key(base, i, 1000, 100).partition(':')[-1]) < 10)
- def test_sharded_hash(self):
- for i in xrange(50):
- shard_hset(self.conn, 'test', 'keyname:%s' % i, i, 1000, 100)
- self.assertEquals(shard_hget(self.conn, 'test', 'keyname:%s' % i, 1000, 100), str(i))
- shard_hset(self.conn, 'test2', i, i, 1000, 100)
- self.assertEquals(shard_hget(self.conn, 'test2', i, 1000, 100), str(i))
+ def test_sharded_hash(self):
+ for i in xrange(50):
+ shard_hset(self.conn, 'test', 'keyname:%s' % i, i, 1000, 100)
+ self.assertEquals(shard_hget(self.conn, 'test', 'keyname:%s' % i, 1000, 100), str(i))
+ shard_hset(self.conn, 'test2', i, i, 1000, 100)
+ self.assertEquals(shard_hget(self.conn, 'test2', i, 1000, 100), str(i))
- def test_sharded_sadd(self):
- for i in xrange(50):
- shard_sadd(self.conn, 'testx', i, 50, 50)
- self.assertEquals(self.conn.scard('testx:0') + self.conn.scard('testx:1'), 50)
+ def test_sharded_sadd(self):
+ for i in xrange(50):
+ shard_sadd(self.conn, 'testx', i, 50, 50)
+ self.assertEquals(self.conn.scard('testx:0') + self.conn.scard('testx:1'), 50)
- def test_unique_visitors(self):
- global DAILY_EXPECTED
- DAILY_EXPECTED = 10000
+ def test_unique_visitors(self):
+ global DAILY_EXPECTED
+ DAILY_EXPECTED = 10000
- for i in xrange(179):
- count_visit(self.conn, str(uuid.uuid4()))
- self.assertEquals(self.conn.get('unique:%s' % (date.today().isoformat())), '179')
+ for i in xrange(179):
+ count_visit(self.conn, str(uuid.uuid4()))
+ self.assertEquals(self.conn.get('unique:%s' % (date.today().isoformat())), '179')
- self.conn.flushdb()
- self.conn.set('unique:%s' % ((date.today() - timedelta(days=1)).isoformat()), 1000)
- for i in xrange(183):
- count_visit(self.conn, str(uuid.uuid4()))
- self.assertEquals(self.conn.get('unique:%s' % (date.today().isoformat())), '183')
+ self.conn.flushdb()
+ self.conn.set('unique:%s' % ((date.today() - timedelta(days=1)).isoformat()), 1000)
+ for i in xrange(183):
+ count_visit(self.conn, str(uuid.uuid4()))
+ self.assertEquals(self.conn.get('unique:%s' % (date.today().isoformat())), '183')
- def test_user_location(self):
- i = 0
- for country in COUNTRIES:
- if country in STATES:
- for state in STATES[country]:
- set_location(self.conn, i, country, state)
- i += 1
- else:
- set_location(self.conn, i, country, '')
- i += 1
+ def test_user_location(self):
+ i = 0
+ for country in COUNTRIES:
+ if country in STATES:
+ for state in STATES[country]:
+ set_location(self.conn, i, country, state)
+ i += 1
+ else:
+ set_location(self.conn, i, country, '')
+ i += 1
- _countries, _states = aggregate_location(self.conn)
- countries, states = aggregate_location_list(self.conn, range(i + 1))
+ _countries, _states = aggregate_location(self.conn)
+ countries, states = aggregate_location_list(self.conn, range(i + 1))
- self.assertEquals(_countries, countries)
- self.assertEquals(_states, states)
+ self.assertEquals(_countries, countries)
+ self.assertEquals(_states, states)
- for c in countries:
- if c in STATES:
- self.assertEquals(len(STATES[c]), countries[c])
- for s in STATES[c]:
- self.assertEquals(states[c][s], 1)
- else:
- self.assertEquals(countries[c], 1)
+ for c in countries:
+ if c in STATES:
+ self.assertEquals(len(STATES[c]), countries[c])
+ for s in STATES[c]:
+ self.assertEquals(states[c][s], 1)
+ else:
+ self.assertEquals(countries[c], 1)
if __name__ == '__main__':
- unittest.main()
+ unittest.main()
diff --git a/codes/redis/redis-in-action-py/ch10_listing_source.py b/codes/redis/redis-in-action-py/ch10_listing_source.py
index 0eea17b..32a6d5b 100644
--- a/codes/redis/redis-in-action-py/ch10_listing_source.py
+++ b/codes/redis/redis-in-action-py/ch10_listing_source.py
@@ -18,18 +18,18 @@ CHECKED = {}
def get_config(conn, type, component, wait=1):
- key = 'config:%s:%s' % (type, component)
+ key = 'config:%s:%s' % (type, component)
- if CHECKED.get(key) < time.time() - wait: # A
- CHECKED[key] = time.time() # B
- config = json.loads(conn.get(key) or '{}') # C
- config = dict((str(k), config[k]) for k in config)
- old_config = CONFIGS.get(key) # D
+ if CHECKED.get(key) < time.time() - wait: # A
+ CHECKED[key] = time.time() # B
+ config = json.loads(conn.get(key) or '{}') # C
+ config = dict((str(k), config[k]) for k in config)
+ old_config = CONFIGS.get(key) # D
- if config != old_config: # E
- CONFIGS[key] = config # F
+ if config != old_config: # E
+ CONFIGS[key] = config # F
- return CONFIGS.get(key)
+ return CONFIGS.get(key)
REDIS_CONNECTIONS = {}
@@ -37,107 +37,107 @@ config_connection = None
def redis_connection(component, wait=1): # A
- key = 'config:redis:' + component # B
+ key = 'config:redis:' + component # B
- def wrapper(function): # C
- @functools.wraps(function) # D
- def call(*args, **kwargs): # E
- old_config = CONFIGS.get(key, object()) # F
- _config = get_config( # G
- config_connection, 'redis', component, wait) # G
+ def wrapper(function): # C
+ @functools.wraps(function) # D
+ def call(*args, **kwargs): # E
+ old_config = CONFIGS.get(key, object()) # F
+ _config = get_config( # G
+ config_connection, 'redis', component, wait) # G
- config = {}
- for k, v in _config.iteritems(): # L
- config[k.encode('utf-8')] = v # L
+ config = {}
+ for k, v in _config.iteritems(): # L
+ config[k.encode('utf-8')] = v # L
- if config != old_config: # H
- REDIS_CONNECTIONS[key] = redis.Redis(**config) # H
+ if config != old_config: # H
+ REDIS_CONNECTIONS[key] = redis.Redis(**config) # H
- return function( # I
- REDIS_CONNECTIONS.get(key), *args, **kwargs) # I
+ return function( # I
+ REDIS_CONNECTIONS.get(key), *args, **kwargs) # I
- return call # J
+ return call # J
- return wrapper # K
+ return wrapper # K
def index_document(conn, docid, words, scores):
- pipeline = conn.pipeline(True)
- for word in words: # I
- pipeline.sadd('idx:' + word, docid) # I
- pipeline.hmset('kb:doc:%s' % docid, scores)
- return len(pipeline.execute()) # J
+ pipeline = conn.pipeline(True)
+ for word in words: # I
+ pipeline.sadd('idx:' + word, docid) # I
+ pipeline.hmset('kb:doc:%s' % docid, scores)
+ return len(pipeline.execute()) # J
def parse_and_search(conn, query, ttl):
- id = str(uuid.uuid4())
- conn.sinterstore('idx:' + id,
- ['idx:' + key for key in query])
- conn.expire('idx:' + id, ttl)
- return id
+ id = str(uuid.uuid4())
+ conn.sinterstore('idx:' + id,
+ ['idx:' + key for key in query])
+ conn.expire('idx:' + id, ttl)
+ return id
def search_and_sort(conn, query, id=None, ttl=300, sort="-updated", # A
- start=0, num=20): # A
- desc = sort.startswith('-') # B
- sort = sort.lstrip('-') # B
- by = "kb:doc:*->" + sort # B
- alpha = sort not in ('updated', 'id', 'created') # I
+ start=0, num=20): # A
+ desc = sort.startswith('-') # B
+ sort = sort.lstrip('-') # B
+ by = "kb:doc:*->" + sort # B
+ alpha = sort not in ('updated', 'id', 'created') # I
- if id and not conn.expire(id, ttl): # C
- id = None # C
+ if id and not conn.expire(id, ttl): # C
+ id = None # C
- if not id: # D
- id = parse_and_search(conn, query, ttl=ttl) # D
+ if not id: # D
+ id = parse_and_search(conn, query, ttl=ttl) # D
- pipeline = conn.pipeline(True)
- pipeline.scard('idx:' + id) # E
- pipeline.sort('idx:' + id, by=by, alpha=alpha, # F
- desc=desc, start=start, num=num) # F
- results = pipeline.execute()
+ pipeline = conn.pipeline(True)
+ pipeline.scard('idx:' + id) # E
+ pipeline.sort('idx:' + id, by=by, alpha=alpha, # F
+ desc=desc, start=start, num=num) # F
+ results = pipeline.execute()
- return results[0], results[1], id # G
+ return results[0], results[1], id # G
def zintersect(conn, keys, ttl):
- id = str(uuid.uuid4())
- conn.zinterstore('idx:' + id,
- dict(('idx:' + k, v) for k, v in keys.iteritems()))
- conn.expire('idx:' + id, ttl)
- return id
+ id = str(uuid.uuid4())
+ conn.zinterstore('idx:' + id,
+ dict(('idx:' + k, v) for k, v in keys.iteritems()))
+ conn.expire('idx:' + id, ttl)
+ return id
def search_and_zsort(conn, query, id=None, ttl=300, update=1, vote=0, # A
- start=0, num=20, desc=True): # A
+ start=0, num=20, desc=True): # A
- if id and not conn.expire(id, ttl): # B
- id = None # B
+ if id and not conn.expire(id, ttl): # B
+ id = None # B
- if not id: # C
- id = parse_and_search(conn, query, ttl=ttl) # C
+ if not id: # C
+ id = parse_and_search(conn, query, ttl=ttl) # C
- scored_search = { # D
- id: 0, # D
- 'sort:update': update, # D
- 'sort:votes': vote # D
- }
- id = zintersect(conn, scored_search, ttl) # E
+ scored_search = { # D
+ id: 0, # D
+ 'sort:update': update, # D
+ 'sort:votes': vote # D
+ }
+ id = zintersect(conn, scored_search, ttl) # E
- pipeline = conn.pipeline(True)
- pipeline.zcard('idx:' + id) # F
- if desc: # G
- pipeline.zrevrange('idx:' + id, start, start + num - 1) # G
- else: # G
- pipeline.zrange('idx:' + id, start, start + num - 1) # G
- results = pipeline.execute()
+ pipeline = conn.pipeline(True)
+ pipeline.zcard('idx:' + id) # F
+ if desc: # G
+ pipeline.zrevrange('idx:' + id, start, start + num - 1) # G
+ else: # G
+ pipeline.zrange('idx:' + id, start, start + num - 1) # G
+ results = pipeline.execute()
- return results[0], results[1], id # H
+ return results[0], results[1], id # H
def execute_later(conn, queue, name, args):
- t = threading.Thread(target=globals()[name], args=tuple(args))
- t.setDaemon(1)
- t.start()
+ t = threading.Thread(target=globals()[name], args=tuple(args))
+ t.setDaemon(1)
+ t.start()
HOME_TIMELINE_SIZE = 1000
@@ -145,18 +145,18 @@ POSTS_PER_PASS = 1000
def shard_key(base, key, total_elements, shard_size): # A
- if isinstance(key, (int, long)) or key.isdigit(): # B
- shard_id = int(str(key), 10) // shard_size # C
- else:
- shards = 2 * total_elements // shard_size # D
- shard_id = binascii.crc32(key) % shards # E
- return "%s:%s" % (base, shard_id) # F
+ if isinstance(key, (int, long)) or key.isdigit(): # B
+ shard_id = int(str(key), 10) // shard_size # C
+ else:
+ shards = 2 * total_elements // shard_size # D
+ shard_id = binascii.crc32(key) % shards # E
+ return "%s:%s" % (base, shard_id) # F
def shard_sadd(conn, base, member, total_elements, shard_size):
- shard = shard_key(base,
- 'x' + str(member), total_elements, shard_size) # A
- return conn.sadd(shard, member) # B
+ shard = shard_key(base,
+ 'x' + str(member), total_elements, shard_size) # A
+ return conn.sadd(shard, member) # B
SHARD_SIZE = 512
@@ -166,19 +166,19 @@ EXPECTED = defaultdict(lambda: 1000000)
# 代码清单 10-1
#
def get_redis_connection(component, wait=1):
- key = 'config:redis:' + component
- # 尝试获取旧的配置。
- old_config = CONFIGS.get(key, object())
- # 尝试获取新的配置。
- config = get_config(
- config_connection, 'redis', component, wait)
+ key = 'config:redis:' + component
+ # 尝试获取旧的配置。
+ old_config = CONFIGS.get(key, object())
+ # 尝试获取新的配置。
+ config = get_config(
+ config_connection, 'redis', component, wait)
- # 如果新旧配置不相同,那么创建一个新的连接。
- if config != old_config:
- REDIS_CONNECTIONS[key] = redis.Redis(**config)
+ # 如果新旧配置不相同,那么创建一个新的连接。
+ if config != old_config:
+ REDIS_CONNECTIONS[key] = redis.Redis(**config)
- # 返回用户指定的连接对象。
- return REDIS_CONNECTIONS.get(key)
+ # 返回用户指定的连接对象。
+ return REDIS_CONNECTIONS.get(key)
#
@@ -187,10 +187,10 @@ def get_redis_connection(component, wait=1):
# 代码清单 10-2
#
def get_sharded_connection(component, key, shard_count, wait=1):
- # 计算出 “<组件名>:<分片数字>” 格式的分片 ID 。
- shard = shard_key(component, 'x' + str(key), shard_count, 2)
- # 返回连接。
- return get_redis_connection(shard, wait)
+ # 计算出 “<组件名>:<分片数字>” 格式的分片 ID 。
+ shard = shard_key(component, 'x' + str(key), shard_count, 2)
+ # 返回连接。
+ return get_redis_connection(shard, wait)
#
@@ -198,7 +198,7 @@ def get_sharded_connection(component, key, shard_count, wait=1):
#
def log_recent(conn, app, message):
- 'the old log_recent() code'
+ 'the old log_recent() code'
log_recent = redis_connection('logs')(log_recent) # 通过反复执行 3 次这行代码,可以达到和装饰器一样的效果
@@ -210,23 +210,23 @@ log_recent = redis_connection('logs')(log_recent) # 通过反复执行 3 次这
#
# 装饰器接受组件名以及预期的分片数量作为参数。
def sharded_connection(component, shard_count, wait=1):
- # 创建一个包装器,使用它去装饰传入的函数。
- def wrapper(function):
- # 从原始函数里面复制一些有用的元信息到配置处理器。
- @functools.wraps(function)
- # 创建一个函数,它负责计算键的分片 ID ,并对连接管理器进行设置。
- def call(key, *args, **kwargs):
- # 获取分片连接。
- conn = get_sharded_connection(
- component, key, shard_count, wait)
- # 实际地调用被装饰的函数,并将分片连接以及其他参数传递给它。
- return function(conn, key, *args, **kwargs)
- # 返回被包装后的函数。
+ # 创建一个包装器,使用它去装饰传入的函数。
+ def wrapper(function):
+ # 从原始函数里面复制一些有用的元信息到配置处理器。
+ @functools.wraps(function)
+ # 创建一个函数,它负责计算键的分片 ID ,并对连接管理器进行设置。
+ def call(key, *args, **kwargs):
+ # 获取分片连接。
+ conn = get_sharded_connection(
+ component, key, shard_count, wait)
+ # 实际地调用被装饰的函数,并将分片连接以及其他参数传递给它。
+ return function(conn, key, *args, **kwargs)
+ # 返回被包装后的函数。
- return call
- # 返回一个函数,它可以对需要分片连接的函数进行包装。
+ return call
+ # 返回一个函数,它可以对需要分片连接的函数进行包装。
- return wrapper
+ return wrapper
#
@@ -238,27 +238,27 @@ def sharded_connection(component, shard_count, wait=1):
# 执行所得的结果将被自动地分片到每台机器的多个数据库键上面。
@sharded_connection('unique', 16)
def count_visit(conn, session_id):
- today = date.today()
- key = 'unique:%s' % today.isoformat()
- # 经过修改的 get_expected() 调用。
- conn2, expected = get_expected(key, today)
+ today = date.today()
+ key = 'unique:%s' % today.isoformat()
+ # 经过修改的 get_expected() 调用。
+ conn2, expected = get_expected(key, today)
- id = int(session_id.replace('-', '')[:15], 16)
- if shard_sadd(conn, key, id, expected, SHARD_SIZE):
- # 使用 get_expected() 函数返回的非分片(nonsharded)连接,
- # 对唯一计数器执行自增操作。
- conn2.incr(key)
+ id = int(session_id.replace('-', '')[:15], 16)
+ if shard_sadd(conn, key, id, expected, SHARD_SIZE):
+ # 使用 get_expected() 函数返回的非分片(nonsharded)连接,
+ # 对唯一计数器执行自增操作。
+ conn2.incr(key)
- # 对 get_expected() 函数使用非分片连接。
+ # 对 get_expected() 函数使用非分片连接。
@redis_connection('unique')
def get_expected(conn, key, today):
- 'all of the same function body as before, except the last line'
- # 返回非分片连接,
- # 使得 count_visit() 函数可以在有需要的时候,
- # 对唯一计数器执行自增操作。
- return conn, EXPECTED[key]
+ 'all of the same function body as before, except the last line'
+ # 返回非分片连接,
+ # 使得 count_visit() 函数可以在有需要的时候,
+ # 对唯一计数器执行自增操作。
+ return conn, EXPECTED[key]
#
@@ -268,24 +268,24 @@ def get_expected(conn, key, today):
#
# 这个函数接受的参数与 search_and_sort() 函数接受的参数完全相同。
def search_get_values(conn, query, id=None, ttl=300, sort="-updated",
- start=0, num=20):
- # 首先取得搜索操作和排序操作的执行结果。
- count, docids, id = search_and_sort(
- conn, query, id, ttl, sort, 0, start + num)
+ start=0, num=20):
+ # 首先取得搜索操作和排序操作的执行结果。
+ count, docids, id = search_and_sort(
+ conn, query, id, ttl, sort, 0, start + num)
- key = "kb:doc:%s"
- sort = sort.lstrip('-')
+ key = "kb:doc:%s"
+ sort = sort.lstrip('-')
- pipe = conn.pipeline(False)
- # 根据结果的排序方式来获取数据。
- for docid in docids:
- pipe.hget(key % docid, sort)
- sort_column = pipe.execute()
+ pipe = conn.pipeline(False)
+ # 根据结果的排序方式来获取数据。
+ for docid in docids:
+ pipe.hget(key % docid, sort)
+ sort_column = pipe.execute()
- # 将文档 ID 以及对文档进行排序产生的数据进行配对(pair up)。
- data_pairs = zip(docids, sort_column)
- # 返回结果包含的文档数量、排序之后的搜索结果以及结果的缓存 ID 。
- return count, data_pairs, id
+ # 将文档 ID 以及对文档进行排序产生的数据进行配对(pair up)。
+ data_pairs = zip(docids, sort_column)
+ # 返回结果包含的文档数量、排序之后的搜索结果以及结果的缓存 ID 。
+ return count, data_pairs, id
#
@@ -296,110 +296,110 @@ def search_get_values(conn, query, id=None, ttl=300, sort="-updated",
# 程序为了获知自己要连接的服务器,
# 会假定所有分片服务器的信息都记录在一个标准的配置位置里面。
def get_shard_results(component, shards, query, ids=None, ttl=300,
- sort="-updated", start=0, num=20, wait=1):
- # 准备一些结构,用于储存之后获取的数据。
- count = 0
- data = []
- # 尝试使用已被缓存的搜索结果;
- # 如果没有缓存结果可用,那么重新执行查询。
- ids = ids or shards * [None]
- for shard in xrange(shards):
- # 获取或者创建一个连向指定分片的连接。
- conn = get_redis_connection('%s:%s' % (component, shard), wait)
- # 获取搜索结果以及它们的排序数据。
- c, d, i = search_get_values(
- conn, query, ids[shard], ttl, sort, start, num)
+ sort="-updated", start=0, num=20, wait=1):
+ # 准备一些结构,用于储存之后获取的数据。
+ count = 0
+ data = []
+ # 尝试使用已被缓存的搜索结果;
+ # 如果没有缓存结果可用,那么重新执行查询。
+ ids = ids or shards * [None]
+ for shard in xrange(shards):
+ # 获取或者创建一个连向指定分片的连接。
+ conn = get_redis_connection('%s:%s' % (component, shard), wait)
+ # 获取搜索结果以及它们的排序数据。
+ c, d, i = search_get_values(
+ conn, query, ids[shard], ttl, sort, start, num)
- # 将这个分片的计算结果与其他分片的计算结果进行合并。
- count += c
- data.extend(d)
- ids[shard] = i
+ # 将这个分片的计算结果与其他分片的计算结果进行合并。
+ count += c
+ data.extend(d)
+ ids[shard] = i
- # 把所有分片的原始(raw)计算结果返回给调用者。
- return count, data, ids
+ # 把所有分片的原始(raw)计算结果返回给调用者。
+ return count, data, ids
#
def get_values_thread(component, shard, wait, rqueue, *args, **kwargs):
- conn = get_redis_connection('%s:%s' % (component, shard), wait)
- count, results, id = search_get_values(conn, *args, **kwargs)
- rqueue.put((shard, count, results, id))
+ conn = get_redis_connection('%s:%s' % (component, shard), wait)
+ count, results, id = search_get_values(conn, *args, **kwargs)
+ rqueue.put((shard, count, results, id))
def get_shard_results_thread(component, shards, query, ids=None, ttl=300,
- sort="-updated", start=0, num=20, wait=1, timeout=.5):
- ids = ids or shards * [None]
- rqueue = Queue()
+ sort="-updated", start=0, num=20, wait=1, timeout=.5):
+ ids = ids or shards * [None]
+ rqueue = Queue()
- for shard in xrange(shards):
- t = threading.Thread(target=get_values_thread, args=(
- component, shard, wait, rqueue, query, ids[shard],
- ttl, sort, start, num))
- t.setDaemon(1)
- t.start()
+ for shard in xrange(shards):
+ t = threading.Thread(target=get_values_thread, args=(
+ component, shard, wait, rqueue, query, ids[shard],
+ ttl, sort, start, num))
+ t.setDaemon(1)
+ t.start()
- received = 0
- count = 0
- data = []
- deadline = time.time() + timeout
- while received < shards and time.time() < deadline:
- try:
- sh, c, r, i = rqueue.get(timeout=max(deadline - time.time(), .001))
- except Empty:
- break
- else:
- count += c
- data.extend(r)
- ids[sh] = i
+ received = 0
+ count = 0
+ data = []
+ deadline = time.time() + timeout
+ while received < shards and time.time() < deadline:
+ try:
+ sh, c, r, i = rqueue.get(timeout=max(deadline - time.time(), .001))
+ except Empty:
+ break
+ else:
+ count += c
+ data.extend(r)
+ ids[sh] = i
- return count, data, ids
+ return count, data, ids
# 代码清单 10-7
#
def to_numeric_key(data):
- try:
- # 这里之所以使用 Decimal 数字类型,
- # 是因为这种类型可以合理地对整数和浮点数进行转换,
- # 并在值缺失或者不是数字值的时候,
- # 返回默认值 0 。
- return Decimal(data[1] or '0')
- except:
- return Decimal('0')
+ try:
+ # 这里之所以使用 Decimal 数字类型,
+ # 是因为这种类型可以合理地对整数和浮点数进行转换,
+ # 并在值缺失或者不是数字值的时候,
+ # 返回默认值 0 。
+ return Decimal(data[1] or '0')
+ except:
+ return Decimal('0')
def to_string_key(data):
- # 总是返回一个字符串,即使在值缺失的情况下,也是如此。
- return data[1] or ''
+ # 总是返回一个字符串,即使在值缺失的情况下,也是如此。
+ return data[1] or ''
# 这个函数需要接受所有分片参数和搜索参数,
# 这些参数大部分都会被传给底层的函数,
# 而这个函数本身只会用到 sort 参数以及搜索偏移量。
def search_shards(component, shards, query, ids=None, ttl=300,
- sort="-updated", start=0, num=20, wait=1):
- # 获取未经排序的分片搜索结果。
- count, data, ids = get_shard_results(
- component, shards, query, ids, ttl, sort, start, num, wait)
+ sort="-updated", start=0, num=20, wait=1):
+ # 获取未经排序的分片搜索结果。
+ count, data, ids = get_shard_results(
+ component, shards, query, ids, ttl, sort, start, num, wait)
- # 准备好进行排序所需的各个参数。
- reversed = sort.startswith('-')
- sort = sort.strip('-')
- key = to_numeric_key
- if sort not in ('updated', 'id', 'created'):
- key = to_string_key
+ # 准备好进行排序所需的各个参数。
+ reversed = sort.startswith('-')
+ sort = sort.strip('-')
+ key = to_numeric_key
+ if sort not in ('updated', 'id', 'created'):
+ key = to_string_key
- # 根据 sort 参数对搜索结果进行排序。
- data.sort(key=key, reverse=reversed)
+ # 根据 sort 参数对搜索结果进行排序。
+ data.sort(key=key, reverse=reversed)
- results = []
- # 只获取用户指定的那一页搜索结果。
- for docid, score in data[start:start + num]:
- results.append(docid)
+ results = []
+ # 只获取用户指定的那一页搜索结果。
+ for docid, score in data[start:start + num]:
+ results.append(docid)
- # 返回被选中的结果,其中包括由每个分片的缓存 ID 组成的序列。
- return count, results, ids
+ # 返回被选中的结果,其中包括由每个分片的缓存 ID 组成的序列。
+ return count, results, ids
#
@@ -409,20 +409,20 @@ def search_shards(component, shards, query, ids=None, ttl=300,
#
# 这个函数接受 search_and_zsort() 函数所需的全部参数。
def search_get_zset_values(conn, query, id=None, ttl=300, update=1,
- vote=0, start=0, num=20, desc=True):
- # 调用底层的 search_and_zsort() 函数,
- # 获取搜索结果的缓存 ID 以及结果包含的文档数量。
- count, r, id = search_and_zsort(
- conn, query, id, ttl, update, vote, 0, 1, desc)
+ vote=0, start=0, num=20, desc=True):
+ # 调用底层的 search_and_zsort() 函数,
+ # 获取搜索结果的缓存 ID 以及结果包含的文档数量。
+ count, r, id = search_and_zsort(
+ conn, query, id, ttl, update, vote, 0, 1, desc)
- # 获取指定的搜索结果以及这些结果的分值。
- if desc:
- data = conn.zrevrange(id, 0, start + num - 1, withscores=True)
- else:
- data = conn.zrange(id, 0, start + num - 1, withscores=True)
+ # 获取指定的搜索结果以及这些结果的分值。
+ if desc:
+ data = conn.zrevrange(id, 0, start + num - 1, withscores=True)
+ else:
+ data = conn.zrange(id, 0, start + num - 1, withscores=True)
- # 返回搜索结果的数量、搜索结果本身、搜索结果的分值以及搜索结果的缓存 ID 。
- return count, data, id
+ # 返回搜索结果的数量、搜索结果本身、搜索结果的分值以及搜索结果的缓存 ID 。
+ return count, data, id
#
@@ -432,40 +432,40 @@ def search_get_zset_values(conn, query, id=None, ttl=300, update=1,
#
# 函数需要接受所有分片参数以及所有搜索参数。
def search_shards_zset(component, shards, query, ids=None, ttl=300,
- update=1, vote=0, start=0, num=20, desc=True, wait=1):
- # 准备一些结构,用于储存之后获取到的数据。
- count = 0
- data = []
- # 尝试使用已有的缓存结果;
- # 如果没有缓存结果可用,那么开始一次新的搜索。
- ids = ids or shards * [None]
- for shard in xrange(shards):
- # 获取或者创建指向每个分片的连接。
- conn = get_redis_connection('%s:%s' % (component, shard), wait)
- # 在分片上面进行搜索,并取得搜索结果的分值。
- c, d, i = search_get_zset_values(conn, query, ids[shard],
- ttl, update, vote, start, num, desc)
+ update=1, vote=0, start=0, num=20, desc=True, wait=1):
+ # 准备一些结构,用于储存之后获取到的数据。
+ count = 0
+ data = []
+ # 尝试使用已有的缓存结果;
+ # 如果没有缓存结果可用,那么开始一次新的搜索。
+ ids = ids or shards * [None]
+ for shard in xrange(shards):
+ # 获取或者创建指向每个分片的连接。
+ conn = get_redis_connection('%s:%s' % (component, shard), wait)
+ # 在分片上面进行搜索,并取得搜索结果的分值。
+ c, d, i = search_get_zset_values(conn, query, ids[shard],
+ ttl, update, vote, start, num, desc)
- # 对每个分片的搜索结果进行合并。
- count += c
- data.extend(d)
- ids[shard] = i
+ # 对每个分片的搜索结果进行合并。
+ count += c
+ data.extend(d)
+ ids[shard] = i
- # 定义一个简单的排序辅助函数,让它只返回与分值有关的信息。
+ # 定义一个简单的排序辅助函数,让它只返回与分值有关的信息。
- def key(result):
- return result[1]
+ def key(result):
+ return result[1]
- # 对所有搜索结果进行排序。
+ # 对所有搜索结果进行排序。
- data.sort(key=key, reversed=desc)
- results = []
- # 从结果里面提取出文档 ID ,并丢弃与之关联的分值。
- for docid, score in data[start:start + num]:
- results.append(docid)
+ data.sort(key=key, reversed=desc)
+ results = []
+ # 从结果里面提取出文档 ID ,并丢弃与之关联的分值。
+ for docid, score in data[start:start + num]:
+ results.append(docid)
- # 将搜索结果返回给调用者。
- return count, results, ids
+ # 将搜索结果返回给调用者。
+ return count, results, ids
#
@@ -474,20 +474,20 @@ def search_shards_zset(component, shards, query, ids=None, ttl=300,
# 代码清单 10-11
#
class KeyShardedConnection(object):
- # 对象使用组件名字以及分片数量进行初始化。
- def __init__(self, component, shards):
- self.component = component
- self.shards = shards
- # 当用户尝试从对象里面获取一个元素的时候,
+ # 对象使用组件名字以及分片数量进行初始化。
+ def __init__(self, component, shards):
+ self.component = component
+ self.shards = shards
+ # 当用户尝试从对象里面获取一个元素的时候,
- # 这个方法就会被调用,
- # 而调用这个方法时传入的参数就是用户请求的元素。
- def __getitem__(self, key):
- # 根据传入的键以及之前已知的组件名字和分片数量,
- # 获取分片连接。
- return get_sharded_connection(
- self.component, key, self.shards)
- #
+ # 这个方法就会被调用,
+ # 而调用这个方法时传入的参数就是用户请求的元素。
+ def __getitem__(self, key):
+ # 根据传入的键以及之前已知的组件名字和分片数量,
+ # 获取分片连接。
+ return get_sharded_connection(
+ self.component, key, self.shards)
+ #
# 代码清单 10-10
@@ -497,42 +497,42 @@ sharded_timelines = KeyShardedConnection('timelines', 8)
def follow_user(conn, uid, other_uid):
- fkey1 = 'following:%s' % uid
- fkey2 = 'followers:%s' % other_uid
+ fkey1 = 'following:%s' % uid
+ fkey2 = 'followers:%s' % other_uid
- if conn.zscore(fkey1, other_uid):
- print "already followed", uid, other_uid
- return None
+ if conn.zscore(fkey1, other_uid):
+ print "already followed", uid, other_uid
+ return None
- now = time.time()
+ now = time.time()
- pipeline = conn.pipeline(True)
- pipeline.zadd(fkey1, other_uid, now)
- pipeline.zadd(fkey2, uid, now)
- pipeline.zcard(fkey1)
- pipeline.zcard(fkey2)
- following, followers = pipeline.execute()[-2:]
- pipeline.hset('user:%s' % uid, 'following', following)
- pipeline.hset('user:%s' % other_uid, 'followers', followers)
- pipeline.execute()
+ pipeline = conn.pipeline(True)
+ pipeline.zadd(fkey1, other_uid, now)
+ pipeline.zadd(fkey2, uid, now)
+ pipeline.zcard(fkey1)
+ pipeline.zcard(fkey2)
+ following, followers = pipeline.execute()[-2:]
+ pipeline.hset('user:%s' % uid, 'following', following)
+ pipeline.hset('user:%s' % other_uid, 'followers', followers)
+ pipeline.execute()
- pkey = 'profile:%s' % other_uid
- # 从正在关注的用户的个人时间线里面,取出最新的状态消息。
- status_and_score = sharded_timelines[pkey].zrevrange(
- pkey, 0, HOME_TIMELINE_SIZE - 1, withscores=True)
+ pkey = 'profile:%s' % other_uid
+ # 从正在关注的用户的个人时间线里面,取出最新的状态消息。
+ status_and_score = sharded_timelines[pkey].zrevrange(
+ pkey, 0, HOME_TIMELINE_SIZE - 1, withscores=True)
- if status_and_score:
- hkey = 'home:%s' % uid
- # 根据被分片的键获取一个连接,然后通过连接获取一个流水线对象。
- pipe = sharded_timelines[hkey].pipeline(True)
- # 将一系列状态消息添加到位于分片上面的定制时间线有序集合里面,
- # 并在添加操作完成之后,对有序集合进行修剪。
- pipe.zadd(hkey, **dict(status_and_score))
- pipe.zremrangebyrank(hkey, 0, -HOME_TIMELINE_SIZE - 1)
- # 执行事务。
- pipe.execute()
+ if status_and_score:
+ hkey = 'home:%s' % uid
+ # 根据被分片的键获取一个连接,然后通过连接获取一个流水线对象。
+ pipe = sharded_timelines[hkey].pipeline(True)
+ # 将一系列状态消息添加到位于分片上面的定制时间线有序集合里面,
+ # 并在添加操作完成之后,对有序集合进行修剪。
+ pipe.zadd(hkey, **dict(status_and_score))
+ pipe.zremrangebyrank(hkey, 0, -HOME_TIMELINE_SIZE - 1)
+ # 执行事务。
+ pipe.execute()
- return True
+ return True
#
@@ -541,28 +541,28 @@ def follow_user(conn, uid, other_uid):
# 代码清单 10-13
#
class KeyDataShardedConnection(object):
- # 对象使用组件名和分片数量进行初始化。
- def __init__(self, component, shards):
- self.component = component
- self.shards = shards
- # 当一对 ID 作为字典查找操作的其中一个参数被传入时,
+ # 对象使用组件名和分片数量进行初始化。
+ def __init__(self, component, shards):
+ self.component = component
+ self.shards = shards
+ # 当一对 ID 作为字典查找操作的其中一个参数被传入时,
- # 这个方法将被调用。
- def __getitem__(self, ids):
- # 取出两个 ID ,并确保它们都是整数。
- id1, id2 = map(int, ids)
- # 如果第二个 ID 比第一个 ID 要小,
- # 那么对调两个 ID 的位置,
- # 从而确保第一个 ID 总是小于或等于第二个 ID 。
- if id2 < id1:
- id1, id2 = id2, id1
- # 基于两个 ID 构建出一个键。
- key = "%s:%s" % (id1, id2)
- # 使用构建出的键以及之前已知的组件名和分片数量,
- # 获取分片连接。
- return get_sharded_connection(
- self.component, key, self.shards)
- #
+ # 这个方法将被调用。
+ def __getitem__(self, ids):
+ # 取出两个 ID ,并确保它们都是整数。
+ id1, id2 = map(int, ids)
+ # 如果第二个 ID 比第一个 ID 要小,
+ # 那么对调两个 ID 的位置,
+ # 从而确保第一个 ID 总是小于或等于第二个 ID 。
+ if id2 < id1:
+ id1, id2 = id2, id1
+ # 基于两个 ID 构建出一个键。
+ key = "%s:%s" % (id1, id2)
+ # 使用构建出的键以及之前已知的组件名和分片数量,
+ # 获取分片连接。
+ return get_sharded_connection(
+ self.component, key, self.shards)
+ #
_follow_user = follow_user
@@ -575,40 +575,40 @@ sharded_followers = KeyDataShardedConnection('followers', 16)
def follow_user(conn, uid, other_uid):
- fkey1 = 'following:%s' % uid
- fkey2 = 'followers:%s' % other_uid
+ fkey1 = 'following:%s' % uid
+ fkey2 = 'followers:%s' % other_uid
- # 根据 uid 和 other_uid 获取连接对象。
- sconn = sharded_followers[uid, other_uid]
- # 检查 other_uid 代表的用户是否已经关注了 uid 代表的用户。
- if sconn.zscore(fkey1, other_uid):
- return None
+ # 根据 uid 和 other_uid 获取连接对象。
+ sconn = sharded_followers[uid, other_uid]
+ # 检查 other_uid 代表的用户是否已经关注了 uid 代表的用户。
+ if sconn.zscore(fkey1, other_uid):
+ return None
- now = time.time()
- spipe = sconn.pipeline(True)
- # 把关注者的信息以及被关注者的信息添加到有序集合里面。
- spipe.zadd(fkey1, other_uid, now)
- spipe.zadd(fkey2, uid, now)
- following, followers = spipe.execute()
+ now = time.time()
+ spipe = sconn.pipeline(True)
+ # 把关注者的信息以及被关注者的信息添加到有序集合里面。
+ spipe.zadd(fkey1, other_uid, now)
+ spipe.zadd(fkey2, uid, now)
+ following, followers = spipe.execute()
- pipeline = conn.pipeline(True)
- # 为执行关注操作的用户以及被关注的用户更新关注者信息和正在关注信息。
- pipeline.hincrby('user:%s' % uid, 'following', int(following))
- pipeline.hincrby('user:%s' % other_uid, 'followers', int(followers))
- pipeline.execute()
+ pipeline = conn.pipeline(True)
+ # 为执行关注操作的用户以及被关注的用户更新关注者信息和正在关注信息。
+ pipeline.hincrby('user:%s' % uid, 'following', int(following))
+ pipeline.hincrby('user:%s' % other_uid, 'followers', int(followers))
+ pipeline.execute()
- pkey = 'profile:%s' % other_uid
- status_and_score = sharded_timelines[pkey].zrevrange(
- pkey, 0, HOME_TIMELINE_SIZE - 1, withscores=True)
+ pkey = 'profile:%s' % other_uid
+ status_and_score = sharded_timelines[pkey].zrevrange(
+ pkey, 0, HOME_TIMELINE_SIZE - 1, withscores=True)
- if status_and_score:
- hkey = 'home:%s' % uid
- pipe = sharded_timelines[hkey].pipeline(True)
- pipe.zadd(hkey, **dict(status_and_score))
- pipe.zremrangebyrank(hkey, 0, -HOME_TIMELINE_SIZE - 1)
- pipe.execute()
+ if status_and_score:
+ hkey = 'home:%s' % uid
+ pipe = sharded_timelines[hkey].pipeline(True)
+ pipe.zadd(hkey, **dict(status_and_score))
+ pipe.zremrangebyrank(hkey, 0, -HOME_TIMELINE_SIZE - 1)
+ pipe.execute()
- return True
+ return True
#
@@ -618,23 +618,23 @@ def follow_user(conn, uid, other_uid):
#
# 函数接受组件名称、分片数量以及那些可以在分片环境下产生正确行为的参数作为参数。
def sharded_zrangebyscore(component, shards, key, min, max, num):
- data = []
- for shard in xrange(shards):
- # 获取指向当前分片的分片连接。
- conn = get_redis_connection("%s:%s" % (component, shard))
- # 从 Redis 分片上面取出数据。
- data.extend(conn.zrangebyscore(
- key, min, max, start=0, num=num, withscores=True))
+ data = []
+ for shard in xrange(shards):
+ # 获取指向当前分片的分片连接。
+ conn = get_redis_connection("%s:%s" % (component, shard))
+ # 从 Redis 分片上面取出数据。
+ data.extend(conn.zrangebyscore(
+ key, min, max, start=0, num=num, withscores=True))
- # 首先基于分值对数据进行排序,然后再基于成员进行排序。
+ # 首先基于分值对数据进行排序,然后再基于成员进行排序。
- def key(pair):
- return pair[1], pair[0]
+ def key(pair):
+ return pair[1], pair[0]
- data.sort(key=key)
+ data.sort(key=key)
- # 根据用户请求的数量返回元素。
- return data[:num]
+ # 根据用户请求的数量返回元素。
+ return data[:num]
#
@@ -643,196 +643,196 @@ def sharded_zrangebyscore(component, shards, key, min, max, num):
# 代码清单 10-15
#
def syndicate_status(uid, post, start=0, on_lists=False):
- root = 'followers'
- key = 'followers:%s' % uid
- base = 'home:%s'
- if on_lists:
- root = 'list:out'
- key = 'list:out:%s' % uid
- base = 'list:statuses:%s'
+ root = 'followers'
+ key = 'followers:%s' % uid
+ base = 'home:%s'
+ if on_lists:
+ root = 'list:out'
+ key = 'list:out:%s' % uid
+ base = 'list:statuses:%s'
- # 通过 ZRANGEBYSCORE 调用,找出下一组关注者。
- followers = sharded_zrangebyscore(root,
- sharded_followers.shards, key, start, 'inf', POSTS_PER_PASS)
+ # 通过 ZRANGEBYSCORE 调用,找出下一组关注者。
+ followers = sharded_zrangebyscore(root,
+ sharded_followers.shards, key, start, 'inf', POSTS_PER_PASS)
- # 基于预先分片的结果对个人信息进行分组,
- # 并把分组后的信息储存到预先准备好的结构里面。
- to_send = defaultdict(list)
- for follower, start in followers:
- # 构造出储存时间线的键。
- timeline = base % follower
- # 找到负责储存这个时间线的分片。
- shard = shard_key('timelines',
- timeline, sharded_timelines.shards, 2)
- # 把时间线的键添加到位于同一个分片的其他时间线的后面。
- to_send[shard].append(timeline)
+ # 基于预先分片的结果对个人信息进行分组,
+ # 并把分组后的信息储存到预先准备好的结构里面。
+ to_send = defaultdict(list)
+ for follower, start in followers:
+ # 构造出储存时间线的键。
+ timeline = base % follower
+ # 找到负责储存这个时间线的分片。
+ shard = shard_key('timelines',
+ timeline, sharded_timelines.shards, 2)
+ # 把时间线的键添加到位于同一个分片的其他时间线的后面。
+ to_send[shard].append(timeline)
- for timelines in to_send.itervalues():
- # 根据储存这组时间线的服务器,
- # 找出连向它的连接,
- # 然后创建一个流水线对象。
- pipe = sharded_timelines[timelines[0]].pipeline(False)
- for timeline in timelines:
- # 把新发送的消息添加到时间线上面,
- # 并移除过于陈旧的消息。
- pipe.zadd(timeline, **post)
- pipe.zremrangebyrank(
- timeline, 0, -HOME_TIMELINE_SIZE - 1)
- pipe.execute()
+ for timelines in to_send.itervalues():
+ # 根据储存这组时间线的服务器,
+ # 找出连向它的连接,
+ # 然后创建一个流水线对象。
+ pipe = sharded_timelines[timelines[0]].pipeline(False)
+ for timeline in timelines:
+ # 把新发送的消息添加到时间线上面,
+ # 并移除过于陈旧的消息。
+ pipe.zadd(timeline, **post)
+ pipe.zremrangebyrank(
+ timeline, 0, -HOME_TIMELINE_SIZE - 1)
+ pipe.execute()
- conn = redis.Redis()
- if len(followers) >= POSTS_PER_PASS:
- execute_later(conn, 'default', 'syndicate_status',
- [uid, post, start, on_lists])
+ conn = redis.Redis()
+ if len(followers) >= POSTS_PER_PASS:
+ execute_later(conn, 'default', 'syndicate_status',
+ [uid, post, start, on_lists])
- elif not on_lists:
- execute_later(conn, 'default', 'syndicate_status',
- [uid, post, 0, True])
+ elif not on_lists:
+ execute_later(conn, 'default', 'syndicate_status',
+ [uid, post, 0, True])
#
def _fake_shards_for(conn, component, count, actual):
- assert actual <= 4
- for i in xrange(count):
- m = i % actual
- conn.set('config:redis:%s:%i' % (component, i), json.dumps({'db': 14 - m}))
+ assert actual <= 4
+ for i in xrange(count):
+ m = i % actual
+ conn.set('config:redis:%s:%i' % (component, i), json.dumps({'db': 14 - m}))
class TestCh10(unittest.TestCase):
- def _flush(self):
- self.conn.flushdb()
- redis.Redis(db=14).flushdb()
- redis.Redis(db=13).flushdb()
- redis.Redis(db=12).flushdb()
- redis.Redis(db=11).flushdb()
+ def _flush(self):
+ self.conn.flushdb()
+ redis.Redis(db=14).flushdb()
+ redis.Redis(db=13).flushdb()
+ redis.Redis(db=12).flushdb()
+ redis.Redis(db=11).flushdb()
- def setUp(self):
- self.conn = redis.Redis(db=15)
- self._flush()
- global config_connection
- config_connection = self.conn
- self.conn.set('config:redis:test', json.dumps({'db': 15}))
+ def setUp(self):
+ self.conn = redis.Redis(db=15)
+ self._flush()
+ global config_connection
+ config_connection = self.conn
+ self.conn.set('config:redis:test', json.dumps({'db': 15}))
- def tearDown(self):
- self._flush()
+ def tearDown(self):
+ self._flush()
- def test_get_sharded_connections(self):
- _fake_shards_for(self.conn, 'shard', 2, 2)
+ def test_get_sharded_connections(self):
+ _fake_shards_for(self.conn, 'shard', 2, 2)
- for i in xrange(10):
- get_sharded_connection('shard', i, 2).sadd('foo', i)
+ for i in xrange(10):
+ get_sharded_connection('shard', i, 2).sadd('foo', i)
- s0 = redis.Redis(db=14).scard('foo')
- s1 = redis.Redis(db=13).scard('foo')
- self.assertTrue(s0 < 10)
- self.assertTrue(s1 < 10)
- self.assertEquals(s0 + s1, 10)
+ s0 = redis.Redis(db=14).scard('foo')
+ s1 = redis.Redis(db=13).scard('foo')
+ self.assertTrue(s0 < 10)
+ self.assertTrue(s1 < 10)
+ self.assertEquals(s0 + s1, 10)
- def test_count_visit(self):
- shards = {'db': 13}, {'db': 14}
- self.conn.set('config:redis:unique', json.dumps({'db': 15}))
- for i in xrange(16):
- self.conn.set('config:redis:unique:%s' % i, json.dumps(shards[i & 1]))
+ def test_count_visit(self):
+ shards = {'db': 13}, {'db': 14}
+ self.conn.set('config:redis:unique', json.dumps({'db': 15}))
+ for i in xrange(16):
+ self.conn.set('config:redis:unique:%s' % i, json.dumps(shards[i & 1]))
- for i in xrange(100):
- count_visit(str(uuid.uuid4()))
- base = 'unique:%s' % date.today().isoformat()
- total = 0
- for c in shards:
- conn = redis.Redis(**c)
- keys = conn.keys(base + ':*')
- for k in keys:
- cnt = conn.scard(k)
- total += cnt
- self.assertTrue(cnt < k)
- self.assertEquals(total, 100)
- self.assertEquals(self.conn.get(base), '100')
+ for i in xrange(100):
+ count_visit(str(uuid.uuid4()))
+ base = 'unique:%s' % date.today().isoformat()
+ total = 0
+ for c in shards:
+ conn = redis.Redis(**c)
+ keys = conn.keys(base + ':*')
+ for k in keys:
+ cnt = conn.scard(k)
+ total += cnt
+ self.assertTrue(cnt < k)
+ self.assertEquals(total, 100)
+ self.assertEquals(self.conn.get(base), '100')
- def test_sharded_search(self):
- _fake_shards_for(self.conn, 'search', 2, 2)
+ def test_sharded_search(self):
+ _fake_shards_for(self.conn, 'search', 2, 2)
- docs = 'hello world how are you doing'.split(), 'this world is doing fine'.split()
- for i in xrange(50):
- c = get_sharded_connection('search', i, 2)
- index_document(c, i, docs[i & 1], {'updated': time.time() + i, 'id': i, 'created': time.time() + i})
- r = search_and_sort(c, docs[i & 1], sort='-id')
- self.assertEquals(r[1][0], str(i))
+ docs = 'hello world how are you doing'.split(), 'this world is doing fine'.split()
+ for i in xrange(50):
+ c = get_sharded_connection('search', i, 2)
+ index_document(c, i, docs[i & 1], {'updated': time.time() + i, 'id': i, 'created': time.time() + i})
+ r = search_and_sort(c, docs[i & 1], sort='-id')
+ self.assertEquals(r[1][0], str(i))
- total = 0
- for shard in (0, 1):
- count = search_get_values(get_redis_connection('search:%s' % shard), ['this', 'world'], num=50)[0]
- total += count
- self.assertTrue(count < 50)
- self.assertTrue(count > 0)
+ total = 0
+ for shard in (0, 1):
+ count = search_get_values(get_redis_connection('search:%s' % shard), ['this', 'world'], num=50)[0]
+ total += count
+ self.assertTrue(count < 50)
+ self.assertTrue(count > 0)
- self.assertEquals(total, 25)
+ self.assertEquals(total, 25)
- count, r, id = get_shard_results('search', 2, ['world', 'doing'], num=50)
- self.assertEquals(count, 50)
- self.assertEquals(count, len(r))
+ count, r, id = get_shard_results('search', 2, ['world', 'doing'], num=50)
+ self.assertEquals(count, 50)
+ self.assertEquals(count, len(r))
- self.assertEquals(get_shard_results('search', 2, ['this', 'doing'], num=50)[0], 25)
+ self.assertEquals(get_shard_results('search', 2, ['this', 'doing'], num=50)[0], 25)
- count, r, id = get_shard_results_thread('search', 2, ['this', 'doing'], num=50)
- self.assertEquals(count, 25)
- self.assertEquals(count, len(r))
- r.sort(key=lambda x: x[1], reverse=True)
- r = list(zip(*r)[0])
+ count, r, id = get_shard_results_thread('search', 2, ['this', 'doing'], num=50)
+ self.assertEquals(count, 25)
+ self.assertEquals(count, len(r))
+ r.sort(key=lambda x: x[1], reverse=True)
+ r = list(zip(*r)[0])
- count, r2, id = search_shards('search', 2, ['this', 'doing'])
- self.assertEquals(count, 25)
- self.assertEquals(len(r2), 20)
- self.assertEquals(r2, r[:20])
+ count, r2, id = search_shards('search', 2, ['this', 'doing'])
+ self.assertEquals(count, 25)
+ self.assertEquals(len(r2), 20)
+ self.assertEquals(r2, r[:20])
- def test_sharded_follow_user(self):
- _fake_shards_for(self.conn, 'timelines', 8, 4)
+ def test_sharded_follow_user(self):
+ _fake_shards_for(self.conn, 'timelines', 8, 4)
- sharded_timelines['profile:1'].zadd('profile:1', 1, time.time())
- for u2 in xrange(2, 11):
- sharded_timelines['profile:%i' % u2].zadd('profile:%i' % u2, u2, time.time() + u2)
- _follow_user(self.conn, 1, u2)
- _follow_user(self.conn, u2, 1)
+ sharded_timelines['profile:1'].zadd('profile:1', 1, time.time())
+ for u2 in xrange(2, 11):
+ sharded_timelines['profile:%i' % u2].zadd('profile:%i' % u2, u2, time.time() + u2)
+ _follow_user(self.conn, 1, u2)
+ _follow_user(self.conn, u2, 1)
- self.assertEquals(self.conn.zcard('followers:1'), 9)
- self.assertEquals(self.conn.zcard('following:1'), 9)
- self.assertEquals(sharded_timelines['home:1'].zcard('home:1'), 9)
+ self.assertEquals(self.conn.zcard('followers:1'), 9)
+ self.assertEquals(self.conn.zcard('following:1'), 9)
+ self.assertEquals(sharded_timelines['home:1'].zcard('home:1'), 9)
- for db in xrange(14, 10, -1):
- self.assertTrue(len(redis.Redis(db=db).keys()) > 0)
- for u2 in xrange(2, 11):
- self.assertEquals(self.conn.zcard('followers:%i' % u2), 1)
- self.assertEquals(self.conn.zcard('following:%i' % u2), 1)
- self.assertEquals(sharded_timelines['home:%i' % u2].zcard('home:%i' % u2), 1)
+ for db in xrange(14, 10, -1):
+ self.assertTrue(len(redis.Redis(db=db).keys()) > 0)
+ for u2 in xrange(2, 11):
+ self.assertEquals(self.conn.zcard('followers:%i' % u2), 1)
+ self.assertEquals(self.conn.zcard('following:%i' % u2), 1)
+ self.assertEquals(sharded_timelines['home:%i' % u2].zcard('home:%i' % u2), 1)
- def test_sharded_follow_user_and_syndicate_status(self):
- _fake_shards_for(self.conn, 'timelines', 8, 4)
- _fake_shards_for(self.conn, 'followers', 4, 4)
- sharded_followers.shards = 4
+ def test_sharded_follow_user_and_syndicate_status(self):
+ _fake_shards_for(self.conn, 'timelines', 8, 4)
+ _fake_shards_for(self.conn, 'followers', 4, 4)
+ sharded_followers.shards = 4
- sharded_timelines['profile:1'].zadd('profile:1', 1, time.time())
- for u2 in xrange(2, 11):
- sharded_timelines['profile:%i' % u2].zadd('profile:%i' % u2, u2, time.time() + u2)
- follow_user(self.conn, 1, u2)
- follow_user(self.conn, u2, 1)
+ sharded_timelines['profile:1'].zadd('profile:1', 1, time.time())
+ for u2 in xrange(2, 11):
+ sharded_timelines['profile:%i' % u2].zadd('profile:%i' % u2, u2, time.time() + u2)
+ follow_user(self.conn, 1, u2)
+ follow_user(self.conn, u2, 1)
- allkeys = defaultdict(int)
- for db in xrange(14, 10, -1):
- c = redis.Redis(db=db)
- for k in c.keys():
- allkeys[k] += c.zcard(k)
+ allkeys = defaultdict(int)
+ for db in xrange(14, 10, -1):
+ c = redis.Redis(db=db)
+ for k in c.keys():
+ allkeys[k] += c.zcard(k)
- for k, v in allkeys.iteritems():
- part, _, owner = k.partition(':')
- if part in ('following', 'followers', 'home'):
- self.assertEquals(v, 9 if owner == '1' else 1)
- elif part == 'profile':
- self.assertEquals(v, 1)
+ for k, v in allkeys.iteritems():
+ part, _, owner = k.partition(':')
+ if part in ('following', 'followers', 'home'):
+ self.assertEquals(v, 9 if owner == '1' else 1)
+ elif part == 'profile':
+ self.assertEquals(v, 1)
- self.assertEquals(len(sharded_zrangebyscore('followers', 4, 'followers:1', '0', 'inf', 100)), 9)
- syndicate_status(1, {'11': time.time()})
- self.assertEquals(len(sharded_zrangebyscore('timelines', 4, 'home:2', '0', 'inf', 100)), 2)
+ self.assertEquals(len(sharded_zrangebyscore('followers', 4, 'followers:1', '0', 'inf', 100)), 9)
+ syndicate_status(1, {'11': time.time()})
+ self.assertEquals(len(sharded_zrangebyscore('timelines', 4, 'home:2', '0', 'inf', 100)), 2)
if __name__ == '__main__':
- unittest.main()
+ unittest.main()
diff --git a/codes/redis/redis-in-action-py/ch11_listing_source.py b/codes/redis/redis-in-action-py/ch11_listing_source.py
index bd25c58..0a0b657 100644
--- a/codes/redis/redis-in-action-py/ch11_listing_source.py
+++ b/codes/redis/redis-in-action-py/ch11_listing_source.py
@@ -12,42 +12,42 @@ import uuid
# 代码清单 11-1
#
def script_load(script):
- # 将 SCRIPT LOAD 命令返回的已缓存脚本 SHA1 校验和储存到一个列表里面,
- # 以便之后在 call() 函数内部对其进行修改。
- sha = [None]
+ # 将 SCRIPT LOAD 命令返回的已缓存脚本 SHA1 校验和储存到一个列表里面,
+ # 以便之后在 call() 函数内部对其进行修改。
+ sha = [None]
- # 在调用已载入脚本的时候,
- # 用户需要将 Redis 连接、脚本要处理的键以及脚本的其他参数传递给脚本。
- def call(conn, keys=[], args=[], force_eval=False):
- if not force_eval:
- # 程序只会在 SHA1 校验和未被缓存的情况下尝试载入脚本。
- if not sha[0]:
- # 如果 SHA1 校验和未被缓存,那么载入给定的脚本
- sha[0] = conn.execute_command(
- "SCRIPT", "LOAD", script, parse="LOAD")
+ # 在调用已载入脚本的时候,
+ # 用户需要将 Redis 连接、脚本要处理的键以及脚本的其他参数传递给脚本。
+ def call(conn, keys=[], args=[], force_eval=False):
+ if not force_eval:
+ # 程序只会在 SHA1 校验和未被缓存的情况下尝试载入脚本。
+ if not sha[0]:
+ # 如果 SHA1 校验和未被缓存,那么载入给定的脚本
+ sha[0] = conn.execute_command(
+ "SCRIPT", "LOAD", script, parse="LOAD")
- try:
- # 使用已缓存的 SHA1 校验和执行命令。
- return conn.execute_command(
- "EVALSHA", sha[0], len(keys), *(keys + args))
+ try:
+ # 使用已缓存的 SHA1 校验和执行命令。
+ return conn.execute_command(
+ "EVALSHA", sha[0], len(keys), *(keys + args))
- except redis.exceptions.ResponseError as msg:
- # 如果错误与脚本缺失无关,那么重新抛出异常。
- if not msg.args[0].startswith("NOSCRIPT"):
- raise
+ except redis.exceptions.ResponseError as msg:
+ # 如果错误与脚本缺失无关,那么重新抛出异常。
+ if not msg.args[0].startswith("NOSCRIPT"):
+ raise
- # 当程序接收到脚本错误的时候,
- # 又或者程序需要强制执行脚本的时候,
- # 它会使用 EVAL 命令直接执行给定的脚本。
- # EVAL 命令在执行完脚本之后,
- # 会自动地把脚本缓存起来,
- # 而缓存产生的 SHA1 校验和跟使用 EVALSHA 命令缓存脚本产生的 SHA1 校验和是完全相同的。
- return conn.execute_command(
- "EVAL", script, len(keys), *(keys + args))
+ # 当程序接收到脚本错误的时候,
+ # 又或者程序需要强制执行脚本的时候,
+ # 它会使用 EVAL 命令直接执行给定的脚本。
+ # EVAL 命令在执行完脚本之后,
+ # 会自动地把脚本缓存起来,
+ # 而缓存产生的 SHA1 校验和跟使用 EVALSHA 命令缓存脚本产生的 SHA1 校验和是完全相同的。
+ return conn.execute_command(
+ "EVAL", script, len(keys), *(keys + args))
- # 返回一个函数,这个函数在被调用的时候会自动载入并执行脚本。
+ # 返回一个函数,这个函数在被调用的时候会自动载入并执行脚本。
- return call
+ return call
#
@@ -65,31 +65,31 @@ def script_load(script):
# 代码清单 11-2
#
def create_status(conn, uid, message, **data):
- pipeline = conn.pipeline(True)
- # 根据用户 ID 获取用户的用户名。
- pipeline.hget('user:%s' % uid, 'login')
- # 为这条状态消息创建一个新的 ID 。
- pipeline.incr('status:id:')
- login, id = pipeline.execute()
+ pipeline = conn.pipeline(True)
+ # 根据用户 ID 获取用户的用户名。
+ pipeline.hget('user:%s' % uid, 'login')
+ # 为这条状态消息创建一个新的 ID 。
+ pipeline.incr('status:id:')
+ login, id = pipeline.execute()
- # 在发布状态消息之前,先检查用户的账号是否存在。
- if not login:
- return None
+ # 在发布状态消息之前,先检查用户的账号是否存在。
+ if not login:
+ return None
- # 准备并设置状态消息的各项信息。
- data.update({
- 'message': message,
- 'posted': time.time(),
- 'id': id,
- 'uid': uid,
- 'login': login,
- })
- pipeline.hmset('status:%s' % id, data)
- # 更新用户的已发送状态消息数量。
- pipeline.hincrby('user:%s' % uid, 'posts')
- pipeline.execute()
- # 返回新创建的状态消息的 ID 。
- return id
+ # 准备并设置状态消息的各项信息。
+ data.update({
+ 'message': message,
+ 'posted': time.time(),
+ 'id': id,
+ 'uid': uid,
+ 'login': login,
+ })
+ pipeline.hmset('status:%s' % id, data)
+ # 更新用户的已发送状态消息数量。
+ pipeline.hincrby('user:%s' % uid, 'posts')
+ pipeline.execute()
+ # 返回新创建的状态消息的 ID 。
+ return id
#
@@ -102,18 +102,18 @@ _create_status = create_status
#
# 这个函数接受的参数和原版消息发布函数接受的参数一样。
def create_status(conn, uid, message, **data):
- # 准备好对状态消息进行设置所需的各个参数和属性。
- args = [
- 'message', message,
- 'posted', time.time(),
- 'uid', uid,
- ]
- for key, value in data.iteritems():
- args.append(key)
- args.append(value)
+ # 准备好对状态消息进行设置所需的各个参数和属性。
+ args = [
+ 'message', message,
+ 'posted', time.time(),
+ 'uid', uid,
+ ]
+ for key, value in data.iteritems():
+ args.append(key)
+ args.append(value)
- return create_status_lua(
- conn, ['user:%s' % uid, 'status:id:'], args)
+ return create_status_lua(
+ conn, ['user:%s' % uid, 'status:id:'], args)
create_status_lua = script_load('''
@@ -149,26 +149,26 @@ return id
# 代码清单 11-4
#
def acquire_lock_with_timeout(
- conn, lockname, acquire_timeout=10, lock_timeout=10):
- # 128 位随机标识符。
- identifier = str(uuid.uuid4())
- lockname = 'lock:' + lockname
- # 确保传给 EXPIRE 的都是整数。
- lock_timeout = int(math.ceil(lock_timeout))
+ conn, lockname, acquire_timeout=10, lock_timeout=10):
+ # 128 位随机标识符。
+ identifier = str(uuid.uuid4())
+ lockname = 'lock:' + lockname
+ # 确保传给 EXPIRE 的都是整数。
+ lock_timeout = int(math.ceil(lock_timeout))
- end = time.time() + acquire_timeout
- while time.time() < end:
- # 获取锁并设置过期时间。
- if conn.setnx(lockname, identifier):
- conn.expire(lockname, lock_timeout)
- return identifier
- # 检查过期时间,并在有需要时对其进行更新。
- elif not conn.ttl(lockname):
- conn.expire(lockname, lock_timeout)
+ end = time.time() + acquire_timeout
+ while time.time() < end:
+ # 获取锁并设置过期时间。
+ if conn.setnx(lockname, identifier):
+ conn.expire(lockname, lock_timeout)
+ return identifier
+ # 检查过期时间,并在有需要时对其进行更新。
+ elif not conn.ttl(lockname):
+ conn.expire(lockname, lock_timeout)
- time.sleep(.001)
+ time.sleep(.001)
- return False
+ return False
#
@@ -180,21 +180,21 @@ _acquire_lock_with_timeout = acquire_lock_with_timeout
# 代码清单 11-5
#
def acquire_lock_with_timeout(
- conn, lockname, acquire_timeout=10, lock_timeout=10):
- identifier = str(uuid.uuid4())
- lockname = 'lock:' + lockname
- lock_timeout = int(math.ceil(lock_timeout))
+ conn, lockname, acquire_timeout=10, lock_timeout=10):
+ identifier = str(uuid.uuid4())
+ lockname = 'lock:' + lockname
+ lock_timeout = int(math.ceil(lock_timeout))
- acquired = False
- end = time.time() + acquire_timeout
- while time.time() < end and not acquired:
- # 执行实际的锁获取操作,通过检查确保 Lua 调用已经执行成功。
- acquired = acquire_lock_with_timeout_lua(
- conn, [lockname], [lock_timeout, identifier]) == 'OK'
+ acquired = False
+ end = time.time() + acquire_timeout
+ while time.time() < end and not acquired:
+ # 执行实际的锁获取操作,通过检查确保 Lua 调用已经执行成功。
+ acquired = acquire_lock_with_timeout_lua(
+ conn, [lockname], [lock_timeout, identifier]) == 'OK'
- time.sleep(.001 * (not acquired))
+ time.sleep(.001 * (not acquired))
- return acquired and identifier
+ return acquired and identifier
acquire_lock_with_timeout_lua = script_load('''
@@ -210,25 +210,25 @@ end
def release_lock(conn, lockname, identifier):
- pipe = conn.pipeline(True)
- lockname = 'lock:' + lockname
+ pipe = conn.pipeline(True)
+ lockname = 'lock:' + lockname
- while True:
- try:
- pipe.watch(lockname) # A
- if pipe.get(lockname) == identifier: # A
- pipe.multi() # B
- pipe.delete(lockname) # B
- pipe.execute() # B
- return True # B
+ while True:
+ try:
+ pipe.watch(lockname) # A
+ if pipe.get(lockname) == identifier: # A
+ pipe.multi() # B
+ pipe.delete(lockname) # B
+ pipe.execute() # B
+ return True # B
- pipe.unwatch()
- break
+ pipe.unwatch()
+ break
- except redis.exceptions.WatchError: # C
- pass # C
+ except redis.exceptions.WatchError: # C
+ pass # C
- return False # D
+ return False # D
_release_lock = release_lock
@@ -237,9 +237,9 @@ _release_lock = release_lock
# 代码清单 11-6
#
def release_lock(conn, lockname, identifier):
- lockname = 'lock:' + lockname
- # 调用负责释放锁的 Lua 函数。
- return release_lock_lua(conn, [lockname], [identifier])
+ lockname = 'lock:' + lockname
+ # 调用负责释放锁的 Lua 函数。
+ return release_lock_lua(conn, [lockname], [identifier])
release_lock_lua = script_load('''
@@ -256,23 +256,23 @@ end
# 代码清单 11-7
#
def acquire_semaphore(conn, semname, limit, timeout=10):
- # 128 位随机标识符。
- identifier = str(uuid.uuid4())
- now = time.time()
+ # 128 位随机标识符。
+ identifier = str(uuid.uuid4())
+ now = time.time()
- pipeline = conn.pipeline(True)
- # 清理过期的信号量持有者。
- pipeline.zremrangebyscore(semname, '-inf', now - timeout)
- # 尝试获取信号量。
- pipeline.zadd(semname, identifier, now)
- # 检查是否成功取得了信号量。
- pipeline.zrank(semname, identifier)
- if pipeline.execute()[-1] < limit:
- return identifier
+ pipeline = conn.pipeline(True)
+ # 清理过期的信号量持有者。
+ pipeline.zremrangebyscore(semname, '-inf', now - timeout)
+ # 尝试获取信号量。
+ pipeline.zadd(semname, identifier, now)
+ # 检查是否成功取得了信号量。
+ pipeline.zrank(semname, identifier)
+ if pipeline.execute()[-1] < limit:
+ return identifier
- # 获取信号量失败,删除之前添加的标识符。
- conn.zrem(semname, identifier)
- return None
+ # 获取信号量失败,删除之前添加的标识符。
+ conn.zrem(semname, identifier)
+ return None
#
@@ -284,11 +284,11 @@ _acquire_semaphore = acquire_semaphore
# 代码清单 11-8
#
def acquire_semaphore(conn, semname, limit, timeout=10):
- # 取得当前时间戳,用于处理超时信号量。
- now = time.time()
- # 把所有必须的参数传递给 Lua 函数,实际地执行信号量获取操作。
- return acquire_semaphore_lua(conn, [semname],
- [now - timeout, limit, now, str(uuid.uuid4())])
+ # 取得当前时间戳,用于处理超时信号量。
+ now = time.time()
+ # 把所有必须的参数传递给 Lua 函数,实际地执行信号量获取操作。
+ return acquire_semaphore_lua(conn, [semname],
+ [now - timeout, limit, now, str(uuid.uuid4())])
acquire_semaphore_lua = script_load('''
@@ -307,16 +307,16 @@ end
#
def release_semaphore(conn, semname, identifier):
- return conn.zrem(semname, identifier)
+ return conn.zrem(semname, identifier)
# 代码清单 11-9
#
def refresh_semaphore(conn, semname, identifier):
- return refresh_semaphore_lua(conn, [semname],
- # 如果信号量没有被刷新,那么 Lua 脚本将返回空值,
- # 而 Python 会将这个空值转换成 None 并返回给调用者。
- [identifier, time.time()]) != None
+ return refresh_semaphore_lua(conn, [semname],
+ # 如果信号量没有被刷新,那么 Lua 脚本将返回空值,
+ # 而 Python 会将这个空值转换成 None 并返回给调用者。
+ [identifier, time.time()]) != None
refresh_semaphore_lua = script_load('''
@@ -331,45 +331,45 @@ valid_characters = '`abcdefghijklmnopqrstuvwxyz{'
def find_prefix_range(prefix):
- posn = bisect.bisect_left(valid_characters, prefix[-1:])
- suffix = valid_characters[(posn or 1) - 1]
- return prefix[:-1] + suffix + '{', prefix + '{'
+ posn = bisect.bisect_left(valid_characters, prefix[-1:])
+ suffix = valid_characters[(posn or 1) - 1]
+ return prefix[:-1] + suffix + '{', prefix + '{'
# 代码清单 11-10
#
def autocomplete_on_prefix(conn, guild, prefix):
- # 根据给定的前缀计算出查找范围的起点和终点。
- start, end = find_prefix_range(prefix)
- identifier = str(uuid.uuid4())
- start += identifier
- end += identifier
- zset_name = 'members:' + guild
+ # 根据给定的前缀计算出查找范围的起点和终点。
+ start, end = find_prefix_range(prefix)
+ identifier = str(uuid.uuid4())
+ start += identifier
+ end += identifier
+ zset_name = 'members:' + guild
- # 将范围的起始元素和结束元素添加到有序集合里面。
- conn.zadd(zset_name, start, 0, end, 0)
- pipeline = conn.pipeline(True)
- while 1:
- try:
- pipeline.watch(zset_name)
- # 找到两个被插入元素在有序集合中的排名。
- sindex = pipeline.zrank(zset_name, start)
- eindex = pipeline.zrank(zset_name, end)
- erange = min(sindex + 9, eindex - 2)
- pipeline.multi()
- # 获取范围内的值,然后删除之前插入的起始元素和结束元素。
- pipeline.zrem(zset_name, start, end)
- pipeline.zrange(zset_name, sindex, erange)
- items = pipeline.execute()[-1]
- break
- # 如果自动补完有序集合已经被其他客户端修改过了,
- # 那么进行重试。
- except redis.exceptions.WatchError:
- continue
+ # 将范围的起始元素和结束元素添加到有序集合里面。
+ conn.zadd(zset_name, start, 0, end, 0)
+ pipeline = conn.pipeline(True)
+ while 1:
+ try:
+ pipeline.watch(zset_name)
+ # 找到两个被插入元素在有序集合中的排名。
+ sindex = pipeline.zrank(zset_name, start)
+ eindex = pipeline.zrank(zset_name, end)
+ erange = min(sindex + 9, eindex - 2)
+ pipeline.multi()
+ # 获取范围内的值,然后删除之前插入的起始元素和结束元素。
+ pipeline.zrem(zset_name, start, end)
+ pipeline.zrange(zset_name, sindex, erange)
+ items = pipeline.execute()[-1]
+ break
+ # 如果自动补完有序集合已经被其他客户端修改过了,
+ # 那么进行重试。
+ except redis.exceptions.WatchError:
+ continue
- # 如果有其他自动补完操作正在执行,
- # 那么从获取到的元素里面移除起始元素和终结元素。
- return [item for item in items if '{' not in item]
+ # 如果有其他自动补完操作正在执行,
+ # 那么从获取到的元素里面移除起始元素和终结元素。
+ return [item for item in items if '{' not in item]
#
@@ -381,17 +381,17 @@ _autocomplete_on_prefix = autocomplete_on_prefix
# 代码清单 11-11
#
def autocomplete_on_prefix(conn, guild, prefix):
- # 取得范围和标识符。
- start, end = find_prefix_range(prefix)
- identifier = str(uuid.uuid4())
+ # 取得范围和标识符。
+ start, end = find_prefix_range(prefix)
+ identifier = str(uuid.uuid4())
- # 使用 Lua 脚本从 Redis 里面获取数据。
- items = autocomplete_on_prefix_lua(conn,
- ['members:' + guild],
- [start + identifier, end + identifier])
+ # 使用 Lua 脚本从 Redis 里面获取数据。
+ items = autocomplete_on_prefix_lua(conn,
+ ['members:' + guild],
+ [start + identifier, end + identifier])
- # 过滤掉所有不想要的元素。
- return [item for item in items if '{' not in item]
+ # 过滤掉所有不想要的元素。
+ return [item for item in items if '{' not in item]
autocomplete_on_prefix_lua = script_load('''
@@ -416,49 +416,49 @@ return redis.call('zrange', KEYS[1], sindex, eindex)
# 代码清单 11-12
#
def purchase_item_with_lock(conn, buyerid, itemid, sellerid):
- buyer = "users:%s" % buyerid
- seller = "users:%s" % sellerid
- item = "%s.%s" % (itemid, sellerid)
- inventory = "inventory:%s" % buyerid
+ buyer = "users:%s" % buyerid
+ seller = "users:%s" % sellerid
+ item = "%s.%s" % (itemid, sellerid)
+ inventory = "inventory:%s" % buyerid
- # 尝试获取锁。
- locked = acquire_lock(conn, 'market:')
- if not locked:
- return False
+ # 尝试获取锁。
+ locked = acquire_lock(conn, 'market:')
+ if not locked:
+ return False
- pipe = conn.pipeline(True)
- try:
- # 检查物品是否已经售出,以及买家是否有足够的金钱来购买物品。
- pipe.zscore("market:", item)
- pipe.hget(buyer, 'funds')
- price, funds = pipe.execute()
- if price is None or price > funds:
- return None
+ pipe = conn.pipeline(True)
+ try:
+ # 检查物品是否已经售出,以及买家是否有足够的金钱来购买物品。
+ pipe.zscore("market:", item)
+ pipe.hget(buyer, 'funds')
+ price, funds = pipe.execute()
+ if price is None or price > funds:
+ return None
- # 将买家支付的货款转移给卖家,并将售出的物品转移给买家。
- pipe.hincrby(seller, 'funds', int(price))
- pipe.hincrby(buyer, 'funds', int(-price))
- pipe.sadd(inventory, itemid)
- pipe.zrem("market:", item)
- pipe.execute()
- return True
- finally:
- # 释放锁
- release_lock(conn, 'market:', locked)
- #
+ # 将买家支付的货款转移给卖家,并将售出的物品转移给买家。
+ pipe.hincrby(seller, 'funds', int(price))
+ pipe.hincrby(buyer, 'funds', int(-price))
+ pipe.sadd(inventory, itemid)
+ pipe.zrem("market:", item)
+ pipe.execute()
+ return True
+ finally:
+ # 释放锁
+ release_lock(conn, 'market:', locked)
+ #
# 代码清单 11-13
#
def purchase_item(conn, buyerid, itemid, sellerid):
- # 准备好执行 Lua 脚本所需的所有键和参数。
- buyer = "users:%s" % buyerid
- seller = "users:%s" % sellerid
- item = "%s.%s" % (itemid, sellerid)
- inventory = "inventory:%s" % buyerid
+ # 准备好执行 Lua 脚本所需的所有键和参数。
+ buyer = "users:%s" % buyerid
+ seller = "users:%s" % sellerid
+ item = "%s.%s" % (itemid, sellerid)
+ inventory = "inventory:%s" % buyerid
- return purchase_item_lua(conn,
- ['market:', buyer, seller, inventory], [item, itemid])
+ return purchase_item_lua(conn,
+ ['market:', buyer, seller, inventory], [item, itemid])
purchase_item_lua = script_load('''
@@ -481,9 +481,9 @@ end
#
def list_item(conn, itemid, sellerid, price):
- inv = "inventory:%s" % sellerid
- item = "%s.%s" % (itemid, sellerid)
- return list_item_lua(conn, [inv, 'market:'], [itemid, item, price])
+ inv = "inventory:%s" % sellerid
+ item = "%s.%s" % (itemid, sellerid)
+ return list_item_lua(conn, [inv, 'market:'], [itemid, item, price])
list_item_lua = script_load('''
@@ -498,35 +498,35 @@ end
# 代码清单 11-14
#
def sharded_push_helper(conn, key, *items, **kwargs):
- # 把元素组成的序列转换成列表。
- items = list(items)
- total = 0
- # 仍然有元素需要推入……
- while items:
- # ……通过调用 Lua 脚本,把元素推入到分片列表里面。
- pushed = sharded_push_lua(conn,
- [key + ':', key + ':first', key + ':last'],
- # 这个程序目前每次最多只会推入 64 个元素,
- # 读者可以根据自己的压缩列表最大长度来调整这个数值。
- [kwargs['cmd']] + items[:64])
- # 计算被推入的元素数量。
- total += pushed
- # 移除那些已经被推入到分片列表里面的元素。
- del items[:pushed]
- # 返回被推入元素的总数量。
- return total
+ # 把元素组成的序列转换成列表。
+ items = list(items)
+ total = 0
+ # 仍然有元素需要推入……
+ while items:
+ # ……通过调用 Lua 脚本,把元素推入到分片列表里面。
+ pushed = sharded_push_lua(conn,
+ [key + ':', key + ':first', key + ':last'],
+ # 这个程序目前每次最多只会推入 64 个元素,
+ # 读者可以根据自己的压缩列表最大长度来调整这个数值。
+ [kwargs['cmd']] + items[:64])
+ # 计算被推入的元素数量。
+ total += pushed
+ # 移除那些已经被推入到分片列表里面的元素。
+ del items[:pushed]
+ # 返回被推入元素的总数量。
+ return total
def sharded_lpush(conn, key, *items):
- # 调用 sharded_push_helper() 函数,
- # 并通过指定的参数告诉它应该执行左端推入操作还是右端推入操作。
- return sharded_push_helper(conn, key, *items, cmd='lpush')
+ # 调用 sharded_push_helper() 函数,
+ # 并通过指定的参数告诉它应该执行左端推入操作还是右端推入操作。
+ return sharded_push_helper(conn, key, *items, cmd='lpush')
def sharded_rpush(conn, key, *items):
- # 调用 sharded_push_helper() 函数,
- # 并通过指定的参数告诉它应该执行左端推入操作还是右端推入操作。
- return sharded_push_helper(conn, key, *items, cmd='rpush')
+ # 调用 sharded_push_helper() 函数,
+ # 并通过指定的参数告诉它应该执行左端推入操作还是右端推入操作。
+ return sharded_push_helper(conn, key, *items, cmd='rpush')
sharded_push_lua = script_load('''
@@ -560,7 +560,7 @@ end
#
def sharded_llen(conn, key):
- return sharded_llen_lua(conn, [key + ':', key + ':first', key + ':last'])
+ return sharded_llen_lua(conn, [key + ':', key + ':first', key + ':last'])
sharded_llen_lua = script_load('''
@@ -584,13 +584,13 @@ return total
# 代码清单 11-15
#
def sharded_lpop(conn, key):
- return sharded_list_pop_lua(
- conn, [key + ':', key + ':first', key + ':last'], ['lpop'])
+ return sharded_list_pop_lua(
+ conn, [key + ':', key + ':first', key + ':last'], ['lpop'])
def sharded_rpop(conn, key):
- return sharded_list_pop_lua(
- conn, [key + ':', key + ':first', key + ':last'], ['rpop'])
+ return sharded_list_pop_lua(
+ conn, [key + ':', key + ':first', key + ':last'], ['rpop'])
sharded_list_pop_lua = script_load('''
@@ -639,49 +639,49 @@ DUMMY = str(uuid.uuid4())
# 定义一个辅助函数,
# 这个函数会为左端阻塞弹出操作以及右端阻塞弹出操作执行实际的弹出动作。
def sharded_bpop_helper(conn, key, timeout, pop, bpop, endp, push):
- # 准备好流水线对象和超时信息。
- pipe = conn.pipeline(False)
- timeout = max(timeout, 0) or 2 ** 64
- end = time.time() + timeout
+ # 准备好流水线对象和超时信息。
+ pipe = conn.pipeline(False)
+ timeout = max(timeout, 0) or 2 ** 64
+ end = time.time() + timeout
- while time.time() < end:
- # 尝试执行一次非阻塞弹出,
- # 如果这个操作成功取得了一个弹出值,
- # 并且这个值并不是伪元素,那么返回这个值。
- result = pop(conn, key)
- if result not in (None, DUMMY):
- return result
+ while time.time() < end:
+ # 尝试执行一次非阻塞弹出,
+ # 如果这个操作成功取得了一个弹出值,
+ # 并且这个值并不是伪元素,那么返回这个值。
+ result = pop(conn, key)
+ if result not in (None, DUMMY):
+ return result
- # 取得程序认为需要对其执行弹出操作的分片。
- shard = conn.get(key + endp) or '0'
- # 运行 Lua 脚本辅助程序,
- # 它会在程序尝试从错误的分片里面弹出元素的时候,
- # 将一个伪元素推入到那个分片里面。
- sharded_bpop_helper_lua(pipe, [key + ':', key + endp],
- # 因为程序不能在流水线里面执行一个可能会失败的 EVALSHA 调用,
- # 所以这里需要使用 force_eval 参数,
- # 确保程序调用的是 EVAL 命令而不是 EVALSHA 命令。
- [shard, push, DUMMY], force_eval=True)
- # 使用用户传入的 BLPOP 命令或 BRPOP 命令,对列表执行阻塞弹出操作。
- getattr(pipe, bpop)(key + ':' + shard, 1)
+ # 取得程序认为需要对其执行弹出操作的分片。
+ shard = conn.get(key + endp) or '0'
+ # 运行 Lua 脚本辅助程序,
+ # 它会在程序尝试从错误的分片里面弹出元素的时候,
+ # 将一个伪元素推入到那个分片里面。
+ sharded_bpop_helper_lua(pipe, [key + ':', key + endp],
+ # 因为程序不能在流水线里面执行一个可能会失败的 EVALSHA 调用,
+ # 所以这里需要使用 force_eval 参数,
+ # 确保程序调用的是 EVAL 命令而不是 EVALSHA 命令。
+ [shard, push, DUMMY], force_eval=True)
+ # 使用用户传入的 BLPOP 命令或 BRPOP 命令,对列表执行阻塞弹出操作。
+ getattr(pipe, bpop)(key + ':' + shard, 1)
- # 如果命令返回了一个元素,那么程序执行完毕;否则的话,进行重试。
- result = (pipe.execute()[-1] or [None])[-1]
- if result not in (None, DUMMY):
- return result
+ # 如果命令返回了一个元素,那么程序执行完毕;否则的话,进行重试。
+ result = (pipe.execute()[-1] or [None])[-1]
+ if result not in (None, DUMMY):
+ return result
- # 这个函数负责调用底层的阻塞弹出操作。
+ # 这个函数负责调用底层的阻塞弹出操作。
def sharded_blpop(conn, key, timeout=0):
- return sharded_bpop_helper(
- conn, key, timeout, sharded_lpop, 'blpop', ':first', 'lpush')
+ return sharded_bpop_helper(
+ conn, key, timeout, sharded_lpop, 'blpop', ':first', 'lpush')
# 这个函数负责调用底层的阻塞弹出操作。
def sharded_brpop(conn, key, timeout=0):
- return sharded_bpop_helper(
- conn, key, timeout, sharded_rpop, 'brpop', ':last', 'rpush')
+ return sharded_bpop_helper(
+ conn, key, timeout, sharded_rpop, 'brpop', ':last', 'rpush')
sharded_bpop_helper_lua = script_load('''
@@ -697,102 +697,102 @@ end
#
class TestCh11(unittest.TestCase):
- def setUp(self):
- self.conn = redis.Redis(db=15)
- self.conn.flushdb()
+ def setUp(self):
+ self.conn = redis.Redis(db=15)
+ self.conn.flushdb()
- def tearDown(self):
- self.conn.flushdb()
+ def tearDown(self):
+ self.conn.flushdb()
- def test_load_script(self):
- self.assertEquals(script_load("return 1")(self.conn), 1)
+ def test_load_script(self):
+ self.assertEquals(script_load("return 1")(self.conn), 1)
- def test_create_status(self):
- self.conn.hset('user:1', 'login', 'test')
- sid = _create_status(self.conn, 1, 'hello')
- sid2 = create_status(self.conn, 1, 'hello')
+ def test_create_status(self):
+ self.conn.hset('user:1', 'login', 'test')
+ sid = _create_status(self.conn, 1, 'hello')
+ sid2 = create_status(self.conn, 1, 'hello')
- self.assertEquals(self.conn.hget('user:1', 'posts'), '2')
- data = self.conn.hgetall('status:%s' % sid)
- data2 = self.conn.hgetall('status:%s' % sid2)
- data.pop('posted');
- data.pop('id')
- data2.pop('posted');
- data2.pop('id')
- self.assertEquals(data, data2)
+ self.assertEquals(self.conn.hget('user:1', 'posts'), '2')
+ data = self.conn.hgetall('status:%s' % sid)
+ data2 = self.conn.hgetall('status:%s' % sid2)
+ data.pop('posted');
+ data.pop('id')
+ data2.pop('posted');
+ data2.pop('id')
+ self.assertEquals(data, data2)
- def test_locking(self):
- identifier = acquire_lock_with_timeout(self.conn, 'test', 1, 5)
- self.assertTrue(identifier)
- self.assertFalse(acquire_lock_with_timeout(self.conn, 'test', 1, 5))
- release_lock(self.conn, 'test', identifier)
- self.assertTrue(acquire_lock_with_timeout(self.conn, 'test', 1, 5))
+ def test_locking(self):
+ identifier = acquire_lock_with_timeout(self.conn, 'test', 1, 5)
+ self.assertTrue(identifier)
+ self.assertFalse(acquire_lock_with_timeout(self.conn, 'test', 1, 5))
+ release_lock(self.conn, 'test', identifier)
+ self.assertTrue(acquire_lock_with_timeout(self.conn, 'test', 1, 5))
- def test_semaphore(self):
- ids = []
- for i in xrange(5):
- ids.append(acquire_semaphore(self.conn, 'test', 5, timeout=1))
- self.assertTrue(None not in ids)
- self.assertFalse(acquire_semaphore(self.conn, 'test', 5, timeout=1))
- time.sleep(.01)
- id = acquire_semaphore(self.conn, 'test', 5, timeout=0)
- self.assertTrue(id)
- self.assertFalse(refresh_semaphore(self.conn, 'test', ids[-1]))
- self.assertFalse(release_semaphore(self.conn, 'test', ids[-1]))
+ def test_semaphore(self):
+ ids = []
+ for i in xrange(5):
+ ids.append(acquire_semaphore(self.conn, 'test', 5, timeout=1))
+ self.assertTrue(None not in ids)
+ self.assertFalse(acquire_semaphore(self.conn, 'test', 5, timeout=1))
+ time.sleep(.01)
+ id = acquire_semaphore(self.conn, 'test', 5, timeout=0)
+ self.assertTrue(id)
+ self.assertFalse(refresh_semaphore(self.conn, 'test', ids[-1]))
+ self.assertFalse(release_semaphore(self.conn, 'test', ids[-1]))
- self.assertTrue(refresh_semaphore(self.conn, 'test', id))
- self.assertTrue(release_semaphore(self.conn, 'test', id))
- self.assertFalse(release_semaphore(self.conn, 'test', id))
+ self.assertTrue(refresh_semaphore(self.conn, 'test', id))
+ self.assertTrue(release_semaphore(self.conn, 'test', id))
+ self.assertFalse(release_semaphore(self.conn, 'test', id))
- def test_autocomplet_on_prefix(self):
- for word in 'these are some words that we will be autocompleting on'.split():
- self.conn.zadd('members:test', word, 0)
+ def test_autocomplet_on_prefix(self):
+ for word in 'these are some words that we will be autocompleting on'.split():
+ self.conn.zadd('members:test', word, 0)
- self.assertEquals(autocomplete_on_prefix(self.conn, 'test', 'th'), ['that', 'these'])
- self.assertEquals(autocomplete_on_prefix(self.conn, 'test', 'w'), ['we', 'will', 'words'])
- self.assertEquals(autocomplete_on_prefix(self.conn, 'test', 'autocompleting'), ['autocompleting'])
+ self.assertEquals(autocomplete_on_prefix(self.conn, 'test', 'th'), ['that', 'these'])
+ self.assertEquals(autocomplete_on_prefix(self.conn, 'test', 'w'), ['we', 'will', 'words'])
+ self.assertEquals(autocomplete_on_prefix(self.conn, 'test', 'autocompleting'), ['autocompleting'])
- def test_marketplace(self):
- self.conn.sadd('inventory:1', '1')
- self.conn.hset('users:2', 'funds', 5)
- self.assertFalse(list_item(self.conn, 2, 1, 10))
- self.assertTrue(list_item(self.conn, 1, 1, 10))
- self.assertFalse(purchase_item(self.conn, 2, '1', 1))
- self.conn.zadd('market:', '1.1', 4)
- self.assertTrue(purchase_item(self.conn, 2, '1', 1))
+ def test_marketplace(self):
+ self.conn.sadd('inventory:1', '1')
+ self.conn.hset('users:2', 'funds', 5)
+ self.assertFalse(list_item(self.conn, 2, 1, 10))
+ self.assertTrue(list_item(self.conn, 1, 1, 10))
+ self.assertFalse(purchase_item(self.conn, 2, '1', 1))
+ self.conn.zadd('market:', '1.1', 4)
+ self.assertTrue(purchase_item(self.conn, 2, '1', 1))
- def test_sharded_list(self):
- self.assertEquals(sharded_lpush(self.conn, 'lst', *range(100)), 100)
- self.assertEquals(sharded_llen(self.conn, 'lst'), 100)
+ def test_sharded_list(self):
+ self.assertEquals(sharded_lpush(self.conn, 'lst', *range(100)), 100)
+ self.assertEquals(sharded_llen(self.conn, 'lst'), 100)
- self.assertEquals(sharded_lpush(self.conn, 'lst2', *range(1000)), 1000)
- self.assertEquals(sharded_llen(self.conn, 'lst2'), 1000)
- self.assertEquals(sharded_rpush(self.conn, 'lst2', *range(-1, -1001, -1)), 1000)
- self.assertEquals(sharded_llen(self.conn, 'lst2'), 2000)
+ self.assertEquals(sharded_lpush(self.conn, 'lst2', *range(1000)), 1000)
+ self.assertEquals(sharded_llen(self.conn, 'lst2'), 1000)
+ self.assertEquals(sharded_rpush(self.conn, 'lst2', *range(-1, -1001, -1)), 1000)
+ self.assertEquals(sharded_llen(self.conn, 'lst2'), 2000)
- self.assertEquals(sharded_lpop(self.conn, 'lst2'), '999')
- self.assertEquals(sharded_rpop(self.conn, 'lst2'), '-1000')
+ self.assertEquals(sharded_lpop(self.conn, 'lst2'), '999')
+ self.assertEquals(sharded_rpop(self.conn, 'lst2'), '-1000')
- for i in xrange(999):
- r = sharded_lpop(self.conn, 'lst2')
- self.assertEquals(r, '0')
+ for i in xrange(999):
+ r = sharded_lpop(self.conn, 'lst2')
+ self.assertEquals(r, '0')
- results = []
+ results = []
- def pop_some(conn, fcn, lst, count, timeout):
- for i in xrange(count):
- results.append(sharded_blpop(conn, lst, timeout))
+ def pop_some(conn, fcn, lst, count, timeout):
+ for i in xrange(count):
+ results.append(sharded_blpop(conn, lst, timeout))
- t = threading.Thread(target=pop_some, args=(self.conn, sharded_blpop, 'lst3', 10, 1))
- t.setDaemon(1)
- t.start()
+ t = threading.Thread(target=pop_some, args=(self.conn, sharded_blpop, 'lst3', 10, 1))
+ t.setDaemon(1)
+ t.start()
- self.assertEquals(sharded_rpush(self.conn, 'lst3', *range(4)), 4)
- time.sleep(2)
- self.assertEquals(sharded_rpush(self.conn, 'lst3', *range(4, 8)), 4)
- time.sleep(2)
- self.assertEquals(results, ['0', '1', '2', '3', None, '4', '5', '6', '7', None])
+ self.assertEquals(sharded_rpush(self.conn, 'lst3', *range(4)), 4)
+ time.sleep(2)
+ self.assertEquals(sharded_rpush(self.conn, 'lst3', *range(4, 8)), 4)
+ time.sleep(2)
+ self.assertEquals(results, ['0', '1', '2', '3', None, '4', '5', '6', '7', None])
if __name__ == '__main__':
- unittest.main()
+ unittest.main()
diff --git a/codes/redis/redis-in-action/pom.xml b/codes/redis/redis-in-action/pom.xml
index 2984951..e2e007d 100644
--- a/codes/redis/redis-in-action/pom.xml
+++ b/codes/redis/redis-in-action/pom.xml
@@ -1,65 +1,66 @@
- 4.0.0
- io.github.dunwu
- redis-in-action
- 1.0.0
- jar
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
+ xmlns="http://maven.apache.org/POM/4.0.0">
+ 4.0.0
+ io.github.dunwu
+ redis-in-action
+ 1.0.0
+ jar
-
- UTF-8
- 1.8
- ${java.version}
- ${java.version}
+
+ UTF-8
+ 1.8
+ ${java.version}
+ ${java.version}
- 1.2.3
- 2.9.0
- 4.12
-
+ 1.2.3
+ 2.9.0
+ 4.12
+
-
-
-
- redis.clients
- jedis
- ${jedis.version}
-
-
+
+
+
+ redis.clients
+ jedis
+ ${jedis.version}
+
+
-
-
- ch.qos.logback
- logback-parent
- ${logback.version}
- pom
- import
-
-
+
+
+ ch.qos.logback
+ logback-parent
+ ${logback.version}
+ pom
+ import
+
+
-
-
- junit
- junit
- ${junit.version}
- test
-
-
+
+
+ junit
+ junit
+ ${junit.version}
+ test
+
+
-
- com.google.code.gson
- gson
- 2.8.5
-
-
- org.apache.commons
- commons-csv
- 1.5
-
-
- org.javatuples
- javatuples
- 1.1
-
-
+
+ com.google.code.gson
+ gson
+ 2.8.5
+
+
+ org.apache.commons
+ commons-csv
+ 1.5
+
+
+ org.javatuples
+ javatuples
+ 1.1
+
+
diff --git a/codes/redis/redis-in-action/src/main/java/Chapter01.java b/codes/redis/redis-in-action/src/main/java/Chapter01.java
index 19316e6..95fa59b 100644
--- a/codes/redis/redis-in-action/src/main/java/Chapter01.java
+++ b/codes/redis/redis-in-action/src/main/java/Chapter01.java
@@ -40,38 +40,13 @@ public class Chapter01 {
printArticles(articles);
assert articles.size() >= 1;
- addRemoveGroups(conn, articleId, new String[] { "new-group" }, new String[] {});
+ addRemoveGroups(conn, articleId, new String[] {"new-group"}, new String[] {});
System.out.println("We added the article to a new group, other articles include:");
articles = getGroupArticles(conn, "new-group", 1);
printArticles(articles);
assert articles.size() >= 1;
}
- /**
- * 代码清单 1-6 对文章进行投票
- */
- public void articleVote(Jedis conn, String user, String article) {
- // 计算文章的投票截止时间。
- long cutoff = (System.currentTimeMillis() / 1000) - ONE_WEEK_IN_SECONDS;
-
- // 检查是否还可以对文章进行投票
- // (虽然使用散列也可以获取文章的发布时间,
- // 但有序集合返回的文章发布时间为浮点数,
- // 可以不进行转换直接使用)。
- if (conn.zscore("time:", article) < cutoff) {
- return;
- }
-
- // 从article:id标识符(identifier)里面取出文章的ID。
- String articleId = article.substring(article.indexOf(':') + 1);
-
- // 如果用户是第一次为这篇文章投票,那么增加这篇文章的投票数量和评分。
- if (conn.sadd("voted:" + articleId, user) == 1) {
- conn.zincrby("score:", VOTE_SCORE, article);
- conn.hincrBy(article, "votes", 1);
- }
- }
-
/**
* 代码清单 1-7 发布文章
*/
@@ -103,10 +78,67 @@ public class Chapter01 {
return articleId;
}
+ /**
+ * 代码清单 1-6 对文章进行投票
+ */
+ public void articleVote(Jedis conn, String user, String article) {
+ // 计算文章的投票截止时间。
+ long cutoff = (System.currentTimeMillis() / 1000) - ONE_WEEK_IN_SECONDS;
+
+ // 检查是否还可以对文章进行投票
+ // (虽然使用散列也可以获取文章的发布时间,
+ // 但有序集合返回的文章发布时间为浮点数,
+ // 可以不进行转换直接使用)。
+ if (conn.zscore("time:", article) < cutoff) {
+ return;
+ }
+
+ // 从article:id标识符(identifier)里面取出文章的ID。
+ String articleId = article.substring(article.indexOf(':') + 1);
+
+ // 如果用户是第一次为这篇文章投票,那么增加这篇文章的投票数量和评分。
+ if (conn.sadd("voted:" + articleId, user) == 1) {
+ conn.zincrby("score:", VOTE_SCORE, article);
+ conn.hincrBy(article, "votes", 1);
+ }
+ }
+
public List