使用 itsdangerous 签名校验

一般在开发网站时使用 session 或者 cookie 来处理用户登陆等等权限问题,而在移动应用中要验证用户身份采用登录时给用户生成一个 token(令牌)的方式。每次用户发出需要身份认证的请求时,就需要验证一次 token 是否有效,无效的情况包括 token 无法被解析等。在向不可信环境发送数据时,确保数据经过签名,使用只有自己知道的密钥来签名数据,加密后发送,在取回数据时,确保没有人篡改过。

Python 有个 itsdangerous 包含了很多安全校验 token 验证相关的方案。 itsdangerous 就是这样一个签名校验的工具,内部使用 HMAC 和 SHA1 来签名。基于 Django 签名模块,支持 JSON Web 签名 (JWS), 这个库采用 BSD 协议。

给定字符串签名

发送方和接收方拥有相同的密钥 secret-key ,发送方使用密钥对发送内容进行签名,接收方使用相同的密钥对接收到的内容进行验证,看是否是发送方发送的内容。

>>> from itsdangerous import Signer
>>> s = Signer('secret-key')
>>> s.sign('my string')
'my string.wh6tMHxLgJqB6oY1uT73iMlyrOA'

签名过的字符串使用 . 分割。验证使用 unsign

>>> s.unsign('my string.wh6tMHxLgJqB6oY1uT73iMlyrOA')

带时间戳的签名

签名有一定的时效性,发送方发送时,带上时间信息,接收方判断多长时间内是否失效

>>> from itsdangerous import TimestampSigner
>>> s = TimestampSigner('secret-key')
>>> string = s.sign('foo')
foo.DlGDsw.dpJ37ffyfNAVufH21lH_yoelnKA
>>> s.unsign(string, max_age=5)

如果验证时间不对会抛出异常

序列化

>>> from itsdangerous import Serializer
>>> s = Serializer('secret-key')
>>> s.dumps([1, 2, 3, 4])
>>> s.loads('[1, 2, 3, 4].r7R9RhGgDPvvWl3iNzLuIIfELmo')

带时间戳的序列化

>>> from itsdangerous import TimedSerializer
>>> s=TimedSerializer('secret-key')
>>> s.dumps([1,2,3,4])
>>> s.loads('[1, 2, 3, 4].DlGEjg.1yG-U7iBk92FBYAZLezoBv2mfJs')

URL 安全序列化

如果加密过的字符串需要在 URL 中传输,可以使用这种方式。常见的就是在邮件验证 token 中。

>>> from itsdangerous import URLSafeSerializer
>>> s = URLSafeSerializer('secret-key')
>>> s.dumps([1, 2, 3, 4])
'WzEsMiwzLDRd.wSPHqC0gR7VUqivlSukJ0IeTDgo'
>>> s.loads('WzEsMiwzLDRd.wSPHqC0gR7VUqivlSukJ0IeTDgo')
[1, 2, 3, 4]

JSON Web Signatures

>>> from itsdangerous import JSONWebSignatureSerializer
>>> s = JSONWebSignatureSerializer('secret-key')
>>> s.dumps({'x': 42})
'eyJhbGciOiJIUzI1NiJ9.eyJ4Ijo0Mn0.ZdTn1YyGz9Yx5B5wNpWRL221G1WpVE5fPCPKNuc6UAo'

带时间戳的 JSON Web 签名

from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
s = Serializer('secret-key', expires_in=60)
s.dumps({'id': user.id}) # user 为 model 中封装过的对象

在 Flask 中应用

from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
from itsdangerous import SignatureExpired, BadSignature
from config import config

def gen_token(user, expiration=1440*31*60):  # 单位为秒,设定 31 天过期
    s = Serializer(config.SECRET_KEY, expires_in=expiration)
    return s.dumps({'id': user.id})  # user 为 model 中封装过的对象

装饰器

from functools import wraps
def token_required(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        token = request.form['token']
        s = Serializer(config.SECRET_KEY)
        try:
            data = s.loads(token)
        except SignatureExpired:
            return jsonify({'status': 'fail', 'data': {'msg': 'expired token'}})
        except BadSignature:
            return jsonify({'status': 'fail', 'data': {'msg': 'useless token'}})
        kwargs['user_id'] = data['id']
        return func(*args, **kwargs)
    return wrapper

盐值

不同的盐值,生成的签名或者序列化的数值不一样,这里的盐 (SALT)不同于加密算法中的盐值,这里的盐值是用来避免彩虹表破解

Hash

哈希(Hash)算法就是单向散列算法,它把某个较大的集合 P 映射到另一个较小的集合 Q 中,假如这个算法叫 H,那么就有 Q = H(P)。对于 P 中任何一个值 p 都有唯一确定的 q 与之对应,但是一个 q 可以对应多个 p。作为一个有用的 Hash 算法,H 还应该满足:H(p) 速度比较快; 给出一个 q,很难算出一个 p 满足 q = H(p);给出一个 p1,很难算出一个不等于 p1 的 p2 使得 H(p1)=H(p2)。正因为有这样的特性,Hash 算法经常被用来保存密码————这样不会泄露密码明文,又可以校验输入的密码是否正确。常用的 Hash 算法有 MD5、SHA1 等。

破解 Hash 的任务就是,对于给出的一个 q,反算出一个 p 来满足 q = H(p)。通常我们能想到的两种办法,一种就是暴力破解法,把 P 中的每一个 p 都算一下 H(p),直到结果等于 q;另一种办法是查表法,搞一个很大的数据 库,把每个 p 和对应的 q 都记录下来,按 q 做一下索引,到时候查一下就知道了。这两种办法理论上都是可以的,但是前一种可能需要海量的时间,后一种需要海量 的存储空间,以至于以目前的人类资源无法实现。

扩展

JWT 是一次性认证完毕加载信息到 token 里的,token 的信息内含过期信息。过期时间过长则被重放攻击的风险太大,而过期时间太短则请求端体验太差(动不动就要重新登录)

第三方认证协议 Oauth2.0 RFC6749 ,它采取了另一种方法:refresh_token,一个用于更新令牌的令牌。在用户首次认证后,签发两个 token: 一个为 access_token,用于用户后续的各个请求中携带的认证信息;另一个是 refresh_token,为 access_token 过期后,用于申请一个新的 access_token

由此可以给两类不同 token 设置不同的有效期,例如给 access_token 仅 1 小时的有效时间,而 refresh_token 则可以是一个月。api 的登出通过 access token 的过期来实现(前端则可直接抛弃此 token 实现登出),在 refresh token 的存续期内,访问 api 时可执 refresh token 申请新的 access token(前端可存此 refresh token,access token 过其实进行更新,达到自动延期的效果)。refresh token 不可再延期,过期需重新使用用户名密码登录。

这种方式的理念在于,将证书分为三种级别:

  • access token 短期证书,用于最终鉴权
  • refresh token 较长期的证书,用于产生短期证书,不可直接用于服务请求
  • 用户名密码 几乎永久的证书,用于产生长期证书和短期证书,不可直接用于服务请求

reference


2017-08-12 itsdangerous , python , sign

Java enum 相等比较 == or equal

能否使用 == 来针对 enum 来比较?

答案是:YES, 枚举谨慎的实例化管理允许使用 == 来进行比较,JLS 8.9 Enums 中有Java 语言的规范定义:

枚举类型除了定义时的枚举常量外没有其他实例

如果显示的实例化枚举类型,会产生编译时异常。final clone 方法保证了 Enum 变量不会被 clone, 序列化的机制也保证了重复的实例在反序列化时不会创建额外的枚举变量。通过反射实例化 Enum 类型是被禁止的。所有这四种方式确保了 enum 类型不存在额外的实例,除了定义时的常量

因为每一个枚举常量只有一个实例,因此使用 == 来代替 equals 方法来比较两个枚举是被允许的.

== 和 equals 的区别

使用 == 来代替 equals 也存在两点需要注意的问题。

== 不会抛出空指针异常

enum Color { BLACK, WHITE };

Color nothing = null;
if (nothing == Color.BLACK);      // runs fine
if (nothing.equals(Color.BLACK)); // throws NullPointerException

不过 == 有编译时类型检查,这一点还是很不错的

enum Color { BLACK, WHITE };
enum Chiral { LEFT, RIGHT };

if (Color.BLACK.equals(Chiral.LEFT)); // compiles fine
if (Color.BLACK == Chiral.LEFT);      // DOESN'T COMPILE!!! Incompatible types!

总结

对于不可变类,并且该类能够有效的控制实例数量, == 就是可用的。

在 Effective Java 一书中指出:

考虑静态工厂方法代替构造器,它使得不可变类可以确保不存在两个相等的实例,即当且仅当 a==b 时才有 a.equals(b) 成立,如果类能够保证这一点,它的客户端可以使用 == 来代替 equals 方法,这样可以提升性能,而枚举类型可以保证这一点。

所以在枚举比较中可以使用 ==

  1. 可以正常工作
  2. 更快
  3. 运行时安全
  4. 编译期也是安全的。

reference


2017-08-10 Java , enum

在 Ubuntu 下安装并使用 Cinnamon

Ubuntu 16.04 LTS 或者 Ubuntu 17.04 下可以通过 PPA 来安装 Cinnamon,感谢维护者

命令如下:

sudo add-apt-repository ppa:embrosyn/cinnamon
sudo apt update && sudo apt install cinnamon

当安装完成之后,Log out 或者 重启,在登录界面选择 Cinnamon 来使用。

我在使用一段时间之后才发现没有安装 Nemo 的插件,以至于右击都没有压缩的选项,通过一下步骤安装 Nemo 以及相关套件。

安装 Nemo

sudo add-apt-repository ppa:noobslab/mint
sudo apt-get update
sudo apt-get install nemo

安装插件

sudo apt-get install nemo-compare nemo-dropbox nemo-fileroller nemo-pastebin nemo-seahorse nemo-share nemo-preview nemo-rabbitvcs

安装完成之后退出 nemo :

nemo -q

然后重启 nemo 即可。

关于 Nemo 更多的使用,可以参考我博客上另外的文章。

reference


2017-08-07 Ubuntu , Linux , Cinnamon , LinuxMint

给常用的 git 命令添加 alias 提升效率

之前有写过 Git alias 的文章, 不过过去很多时间,现在对 Git 命令越来越熟悉就希望更快的提高输入效率,也越来越感受到 alias 的重要性,不管是直接在 bash 中的 alias 还是 Git 的 alias。所以准备找一些合适的 alias 添加到自己的 gitconfig 文件中长期使用。

添加 alias

使用命令的方式添加

git config --global alias.co checkout
git config --global alias.ci commit
git config --global alias.st status
git config --global alias.br branch
git config --global alias.hist "log --pretty=format:'%h %ad | %s%d [%an]' --graph --date=short"
git config --global alias.type 'cat-file -t'
git config --global alias.dump 'cat-file -p'

git status, git add, git commit, git checkoutgit branch 是最常见的 git 命令,给他们设置 alias 能提高不少效率。使用以上命令添加 alias ,其实作用等同于直接编辑 HOME 目录下的 gitconfig 文件, vim ~/.gitconfig:

[alias]
  co = checkout
  ci = commit
  st = status
  br = branch
  hist = log --pretty=format:\"%h %ad | %s%d [%an]\" --graph --date=short
  type = cat-file -t
  dump = cat-file -p

如果这样设置之后就可以使用 git co <branch> 来切换分支了。

bash alias

可以在 ~/.bashrc 或者 ~/.zshrc 中设置 alias g=git 这样就可以使用 g checkout 来代替 git checkout 了。

reference


2017-08-04 git , alias , linux

每天学习一个命令:使用 split 分割文件

split 命令可以用来分割文件,支持按文本分割,也支持二进制分割。常见的格式

split [OPTION]... [FILE [PREFIX]]

输出的结果为多个文件 PREFIXaa,PREFIXab … 默认的大小是 1000 行,默认的 PREFIX 是 x

如果没有 FILE 文件,或者当 FILE 参数是 - 时,会从标准输入读取。

举例

按文件大小

-C 指定分割文件大小:

split -C 10M large_file.mp4 small

将大文件 large_file.mp4 按照 10M 大小进行分割,并指定分割后文件前缀 small,不指定前缀时, 自动对分割文件名命名,一般以 x 开头

按行分割

-l 选项后接分割行

split -l 1000 large_file.txt small

二进制分割

split -b 100M data.bak smail

合并

cat small* > new_file.txt

2017-08-04 split , command , linux

Python logging 模块使用

基本使用

# -*- coding: utf-8 -*-

import logging
import sys

# 获取 logger 实例,如果参数为空则返回 root logger
# 最基本的入口,该方法参数可以为空,默认的 logger 名称是 root,如果在同一个程序中一直都使用同名的 logger,其实会拿到同一个实例,使用这个技巧就可以跨模块调用同样的 logger 来记录日志
logger = logging.getLogger("AppName")

# 指定 logger 输出格式
formatter = logging.Formatter('%(asctime)s %(levelname)-8s: %(message)s')

# 文件日志
file_handler = logging.FileHandler("test.log")
file_handler.setFormatter(formatter)  # 可以通过 setFormatter 指定输出格式

# 控制台日志
console_handler = logging.StreamHandler(sys.stdout)
console_handler.formatter = formatter  # 也可以直接给 formatter 赋值

# 为 logger 添加的日志处理器
logger.addHandler(file_handler)
logger.addHandler(console_handler)

# 指定日志的最低输出级别,默认为 WARN 级别
logger.setLevel(logging.INFO)

# 输出不同级别的 log
logger.debug('this is debug info')
logger.info('this is information')
logger.warn('this is warning message')
logger.error('this is error message')
logger.fatal('this is fatal message, it is same as logger.critical')
logger.critical('this is critical message')

# 2016-10-08 21:59:19,493 INFO    : this is information
# 2016-10-08 21:59:19,493 WARNING : this is warning message
# 2016-10-08 21:59:19,493 ERROR   : this is error message
# 2016-10-08 21:59:19,493 CRITICAL: this is fatal message, it is same as logger.critical
# 2016-10-08 21:59:19,493 CRITICAL: this is critical message

# 移除一些日志处理器
logger.removeHandler(file_handler)

格式化输出

# 格式化输出

service_name = "Booking"
logger.error('%s service is down!' % service_name)  # 使用 python 自带的字符串格式化,不推荐
logger.error('%s service is down!', service_name)  # 使用 logger 的格式化,推荐
logger.error('%s service is %s!', service_name, 'down')  # 多参数格式化
logger.error('{} service is {}'.format(service_name, 'down')) # 使用 format 函数,推荐

# 2016-10-08 21:59:19,493 ERROR   : Booking service is down!

异常

当你使用 logging 模块记录异常信息时,不需要传入该异常对象,只要你直接调用 logger.error() 或者 logger.exception() 就可以将当前异常记录下来。

# 记录异常信息

try:
    1 / 0
except:
    # 等同于 error 级别,但是会额外记录当前抛出的异常堆栈信息
    logger.exception('this is an exception message')

# 2016-10-08 21:59:19,493 ERROR   : this is an exception message
# Traceback (most recent call last):
#   File "D:/Git/py_labs/demo/use_logging.py", line 45, in <module>
#     1 / 0
# ZeroDivisionError: integer division or modulo by zero

常见配置

通过日志名称来区分同一程序的不同模块

logger = logging.getLogger("App.UI")
logger = logging.getLogger("App.Service")

fmt 中允许使用的变量可以参考下表

  • %(name)s Logger 的名字
  • %(levelno)s 数字形式的日志级别
  • %(levelname)s 文本形式的日志级别
  • %(pathname)s 调用日志输出函数的模块的完整路径名,可能没有
  • %(filename)s 调用日志输出函数的模块的文件名
  • %(module)s 调用日志输出函数的模块名
  • %(funcName)s 调用日志输出函数的函数名
  • %(lineno)d 调用日志输出函数的语句所在的代码行
  • %(created)f 当前时间,用 UNIX 标准的表示时间的浮点数表示
  • %(relativeCreated)d 输出日志信息时的,自 Logger 创建以来的毫秒数
  • %(asctime)s 字符串形式的当前时间。默认格式是“2003-07-08 16:49:45,896”。逗号后面的是毫秒
  • %(thread)d 线程 ID。可能没有
  • %(threadName)s 线程名。可能没有
  • %(process)d 进程 ID。可能没有
  • %(message)s 用户输出的消息

Handler 日志处理器

最常用的是 StreamHandler 和 FileHandler, Handler 用于向不同的输出端打 log。

  • StreamHandler instances send error messages to streams (file-like objects).
  • FileHandler instances send error messages to disk files.
  • RotatingFileHandler instances send error messages to disk files, with support for maximum log file sizes and log file rotation.
  • TimedRotatingFileHandler instances send error messages to disk files, rotating the log file at certain timed intervals.
  • SocketHandler instances send error messages to TCP/IP sockets.
  • DatagramHandler instances send error messages to UDP sockets.
  • SMTPHandler instances send error messages to a designated email address.

日志格式配置的方式

logging 的配置大致有下面几种方式。

  • 通过代码进行完整配置,参考开头的例子,主要是通过 getLogger 方法实现。
  • 通过代码进行简单配置,下面有例子,主要是通过 basicConfig 方法实现。
  • 通过配置文件,下面有例子,主要是通过 logging.config.fileConfig(filepath)

通过 basicConfig 配置 logger : https://docs.python.org/2/library/logging.html#logging.basicConfig

日志重复输出的坑

可能的原因有很多,但总结下来无非就一个,日志中使用了重复的 handler

切记添加 handler 时不要重复。


2017-08-03 python , logging , logger

使用 setuptools 创建并发布 python 包

If you write something, and you want to share with the world. And let others use through pip install, you can upload your package to pypi.

Create project layout

Put your code in some fold like douban. Write your own setup.py to give basic info about this lib or package. And you can put a README.md file and LICENSE file

douban-dl/
	LICENSE.txt
	README.md
	setup.py
	douban/
		__init__.py
		__main__.py
		other_packages.py

Create setup.py to describe project

setup file often used to describe your project, you can have a template like this:

from setuptools import setup, find_packages

def readme():
	with open('README.md') as f:
		return f.read()

requirements = [
	"bs4",
	"requests"
]

setup(
	name='douban-dl',
	version='0.0.1',
	description='',
	long_description=readme(),
	packages=['douban',],
	url="https://github.com/einverne/douban-dl",
	author="einverne",
	author_email="einverne@gmail.com",
	entry_points={
		'console_scripts': [
			'douban-dl = douban.__main__:main',
		]},
	keywords="douban downloader",
	packages=find_packages(exclude=["tests"]),
	license='MIT',
    install_requires=requirements,
)

Create .pypirc file

Register your own account at https://pypi.python.org/pypi. And create following file under ~/.pypirc.

[distutils]
index-servers =
  pypi
  testpypi

[pypi]
username: einverne
password: password

[testpypi]
repository: https://test.pypi.org/legacy
username: einverne
password: password

Upload your first release

Use following command to create a compressed archive file which is under dist sub-directory. This file will wrap-up all project’s source code.

python setup.py sdist

And upload

python setup.py sdist [bdist_wininst] upload

this command upload archive file to pypi. bdist_wininst option alse create windows distribution.

All above had beed tested by myself -> https://pypi.python.org/pypi/douban-dl

reference


2017-08-02 python , linux , packages , module

Redis 读书笔记

这篇文章主要介绍 Redis 的持久化机制,主从复制等等

持久化机制

通常情况下 Redis 会将数据存储于内存中,但 Redis 也支持持久化。Redis 支持两种持久化方式,RDB 方式 和 AOF 方式。RDB 通过快照方式,将内存数据写入磁盘。而 AOF 方式则是类似 MySQL 日志方式,记录每次更新的日志。前者性能高,但是可能引起一定的数据丢失,后者相反。

RDB 方式

RDB 通过快照 snapshotting 完成,也是 Redis 默认的持久化方式,当符合一定条件时 Redis 会自动将内存中的所有数据以快照方式保存一份副本到硬盘上 (dump.rdb),这个过程称为”快照”。

Redis 根据以下情况执行快照:

  • 根据配置规则进行自动快照
  • 用户执行 SAVE 或者 BGSAVE 命令
  • 执行 FLUSHALL 命令
  • 执行复制 replication
  1. 配置规则
  • save 900 1 表示在 15min(900s) 时间内,有一个或者一个以上键被更改则进行快照。
  • save 300 10 表示 300 秒内超过 10 个 key 被修改,则发起快照
  1. SAVE 或 BGSAVE 命令

SAVE 命令时, Redis 同步地进行快照操作,会阻塞所有来自客户端的请求。尽量避免在生产环境使用这一命令。

BGSAVE 命令,后台异步进行快照。查看快照是否成功,通过 LASTSAVE 命令获取最近一次成功执行快照时间,返回结果 Unix 时间戳。

  1. FLUSHALL ,Redis 清除数据库所有数据。只要定义了自动快照条件,则会进行快照。如果没有定义自动快照,则不会进行快照。

  2. 复制操作时,即使没有定义自动快照条件,也会生成 RDB 快照

Redis 默认将快照文件存储在工作目录中 dump.rdb 文件中,可以通过配置 dir 和 dbfilename 两个参数分别来指定快照文件的存储路径和文件名。

快照的保存过程:

  1. redis 调用 fork, fork 一份子进程
  2. 父进程处理 client 请求,子进程负责将内存内容写入临时文件,父进程处理写请求时 os 为父进程创建副本,子进程地址空间内的数据是 fork 时刻整个数据库的快照
  3. 当子进程将快照写入临时文件完毕之后,用临时文件替换原来的快照文件,子进程退出。

需要注意的是,每次快照持久化都是将内存数据完整写入到磁盘一次,并不是增量的只同步变更数据。如果数据量大的话,而且写操作比较多,必然会引起大量的磁盘 io 操作,可能会严重影响性能。

AOF 方式

快照的方式是一定间隔备份一次,它的缺点就是如果 Redis 意外挂掉的话,就会丢失最后一次快照之后的所有修改。如果应用要求不能丢失任何修改可以采用 AOF 持久化方式。

AOF 将 Redis 执行的每一条写命令追加到硬盘文件中(默认为 appendonly.aof)。默认没有开启 AOF (append only file) ,可以通过 appendonly 参数启用:

appendonly yes

AOF 文件保存位置和 RDB 文件位置相同,通过 dir 参数设置,默认为 appendonly.aof ,通过 appendfilename 参数修改:

appendfilename appendonly.aof

Redis 在重启时会通过重新执行文件中保存的写命令来在内存中重建整个数据库。

操作系统会在内核中缓存写命令的修改,因此不是立即存盘,所以也可能会导致部分数据丢失。通过 fsync 函数强制系统写入磁盘的时机, 有三种方式如下(默认是:每秒 fsync 一次):

appendonly yes  # 启用 aof 持久化方式
# appendfsync always  # 收到写命令就立即写入磁盘,最慢,但是保证完全的持久化
appendfsync everysec  # 每秒钟写入磁盘一次,在性能和持久化方面做了很好的折中
# appendfsync no  # 完全依赖 os, 性能最好,持久化没保证

AOF 方式的缺点:

  • 持久化文件会越来越大,为了解决这个问题 Redis 提供了 bgrewriteaof 命令。 收到此命令 Redis 将使用与快照类似的方式将内存中的数据以命令方式保存到临时文件中,最后替换原来的文件

集群

结构上,容易发生单点故障,分配不同服务器

容量上,内存容易成为存储瓶颈,需要对数据进行分片

集群的特点在于拥有和单机实例同样的性能,同时在网络分区后能够提供一定的可访问性以及对主数据库故障恢复的支持。

主从复制

复制多副本部署不同服务器,防止一台故障丢失数据。通过主从复制可以允许多个 slave server 拥有和 master server 相同的数据库副本。

主从复制特点:

  • master 可以拥有多 slave
  • 多 slave 可以连接到同一个 master 外也能连接到其他 slave
  • 主从复制不会阻塞 master, master 在同步数据时也可以继续处理 client 请求

从数据库配置中:

slaveof 主数据库地址 主数据库端口

当配置好 slave 后,slave 与 master 建立连接,然后发送 sync 命令。无论是第一次连接还是重新连接,master 都会启动一个后台进程,将数据库快照保存到文件中,同时 master 主进程会开始收集新的写命令并缓存。后台进程完成写文件后,master 发送文件给 slave, slave 将文件保存到硬盘上,再加载到内存中,接着 master 就会把缓存的命令转发给 slave, 后续 master 将收到的写命令发送给 slave。如果 master 同时收到多个 slave 发来的同步连接命令,master 只会启动一个进程来写数据库镜像,然后发送给所有的 slave。

使用 info 命令来判断当前 redis 是主库还是从库, role 字段,还可以利用 master_link_status 来查看主从是否异步,up 为正常,down 为同步异步。

哨兵

监控 Redis 运行状况。

Redis 命令属性

Redis 不同命令拥有不同的属性,是否只读命令,是否是管理员命令,一个命令可以拥有多个属性。

  • REDIS_CMD_WRITE 属性,会修改 Redis 数据库数据
  • REDIS_CMD_DENYOOM 属性,可能增加 Redis 占用的存储空间,显然拥有该属性的命令都拥有 REDIS_CMD_WRITE 属性。
  • REDIS_CMD_NOSCRIPT 属性,无法在 Redis 脚本中执行
  • REDIS_CMD_RANDOM 脚本执行了该属性命令之后,不能执行拥有 REDIS_CMD_WRITE 属性命令
  • REDIS_CMD_SORT_FOR_SCRIPT 产生随机结果
  • REDIS_CMD_LOADING 当 Redis 启动时,只会执行拥有该属性的命令

Redis 事务

Redis 由单线程处理所有 client 请求,在接收到 client 发送的命令后会立即处理并返回结果,但是当 client 发出 multi 命令后,这个连接会进入事务上下文,连接后续命令并不会立即执行,而是先放到队列中,当收到 exec 命令后,redis 顺序执行队列中所有命令,并将结果一起返回。

get keydemo
multi
set keydemo 10
set keydemo 20
exec
get keydemo

在 multi 开始之后可以使用 discard 来取消事务。

Redis 的乐观锁

乐观锁,指的是每次拿数据时认为别人不会修改,不上锁,而在提交更新市判断期间是否有别人更新该数据。

乐观锁使用数据库版本记录实现,需要满足提交版本必须大于当前记录版本才能执行更新的乐观锁策略。

Redis 从 2.1 版本开始支持乐观锁,可以使用 watch 显式对 key 加锁。watch 命令会监视给定的 key,在 exec 结束时如果监视 key 从调用 watch 后发生过变化,则事务会失败。

其他

phpRedisAdmin 是一个 PHP 实现的 Redis 管理 Web 界面。地址:https://github.com/erikdubbelboer/phpRedisAdmin

reference


2017-08-01 redis , database , database , key-value , db

使用 openpyxl python lib 来读写 Excel

Openpyxl 是一个用来处理 Excel 格式文件的 Python 库,它能用来处理 Excel 2007 及以上版本的 excel 文件,也就是 .xlsx/.xlsm/.xltx/.xltm 格式的表格文件。

installation

使用 pip 安装

pip install openpyxl

usage

使用方法包括读和写,参考如下例子:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from openpyxl import Workbook, load_workbook


class ExcelWriter:
	def __init__(self, name):
		self.name = name
		self.wb = Workbook()

	def create_work_sheet(self, sheet_name, index):
		ws = self.wb.create_sheet(sheet_name, index)
		ws.sheet_properties.tabColor = "1072BA"

	def write_row(self):
		ws = self.wb.active
		for row in range(1, 10):  # 1 到 10 行
			ws.append(range(10))  # 10 列
		self.wb.save(self.name)

	def set_cell_value(self, row, col, value):
		ws = self.wb.active
		ws.cell(row=row, column=col).value = value

	def save(self):
		self.wb.save(self.name)


class ExcelReader:
	def __init__(self, path):
		self.path = path
		self.wb = load_workbook(path)

	def get_all_sheetnames(self):
		return self.wb.get_sheet_names

	def get_all_rows(self, sheet_name):
		"""
		按行进行迭代
		:param sheet_name:
		:return:
		"""
		ws = self.wb.get_sheet_by_name(sheet_name)  # 调用 get_sheet_by_name 如果 sheet name 不存在返回 None
		if ws is None:
			return
		rows = ws.rows
		for row in rows:
			if row is None:
				return
			yield [col.value for col in row]

	def get_cell_value(self, sheet_name, row, col):
		ws = self.wb.get_sheet_by_name(sheet_name)
		if ws is None:
			return
		return ws.cell(row=row, column=col).value


if __name__ == '__main__':
	writer = ExcelWriter("sample.xlsx")

	writer.write_row()

	reader = ExcelReader('sample.xlsx')
	sn = reader.get_all_sheetnames()

	for data in reader.get_all_rows('Sheet'):
		print data

图片图标

openpyxl 还有很多重量级的功能,比如绘图等等,具体可参考文档。

其他

Python 其他处理 Excel 的库

reference


2017-07-31 python , excel , openpyxl

发送邮件服务收集整理

能够稳定发送邮件的服务,能够满足个人使用需求,能够通过 API 调用直接发送邮件的服务。

mailgun

免费服务每个月 10000 封 / 单个域名 限制,个人其实完全用不完

sendgrid

免费服务每天可以发送 100 封邮件

Sendcloud

搜狐的服务,免费账号 50 封邮件 / 天 20 条短信 / 天

  • https://sendcloud.sohu.com/price.html

Amazon SES

如果是通过 Amazon EC2 托管的程序,每个月前 62000 封邮件免费。


2017-07-30 email , collection , email-service

电子书

最近文章

  • MySQL 中的日志配置和管理 MySQL 中默认是没有开启日志记录的,所以需要手动修改配置文件开启日志。而在 MySQL 中我们需要关心的有三类日志:
  • Java 查漏补缺之:ThreadLocal 使用 ThreadLocal 线程本地变量,每个线程保存变量的副本,对副本的改动,对其他的线程而言是透明的。
  • 为知笔记导出和备份 WizNote 已经用了好几年,虽然也一直在续费,但总感觉将死不死,基于整理这几年近 4000 条的笔记的目的,也一方面为迁移出 WizNote 的目的,研究一下 WizNote 笔记导出和备份的方法。
  • Nginx location 匹配规则 之前的关于 Nginx Config 的文章是当时看 Nginx 书记录下来的笔记,很大略,没有实际操作,等终究用到 location 的时候发现还是有很多需要注意的问题,比如匹配的优先顺序,比如 root 和 alias 的区别等等,所以单独拿一篇文章来记录一下目前遇到的问题,并来解决一下。
  • koajs 简单使用 Koa 是一个背靠 Express 的团队设计的全新的 Web 框架,旨在使之成为一个更轻量,更丰富,更加 robust 的基础框架。通过促进异步方法的使用,Koa 允许使用者抛弃 callback 调用,并且大大简化了错误处理。Koa 并没有将中间件绑定到核心代码,而是提供了一组优雅的方法让编写服务更加快速,通过很多第三方的扩展给 Koa 提供服务,从而实现更加丰富完整的 HTTP server。