单线程与隔离性

Redis是使用单线程的方式来执行事务的,事务以串行的方式运行,也就是说Redis中单个命令的执行和事务的执行都是线程安全的,不会相互影响,具有隔离性。

在多线程编程中,对于共享资源的访问要十分的小心:

import threading

num = 1
lock = threading.Lock() def change_num():
global num
for i in xrange(100000):
#lock.acquire()
num += 5
num -= 5
#lock.release() if __name__ == '__main__':
pool = [threading.Thread(target=change_num) for i in xrange(5)]
for t in pool:
t.start()
for t in pool:
t.join()
print num

在不加锁的情况下,num是不能保持为1的。

而在Redis中,并发执行单个命令具有很好的隔离性:

import redis

conn = redis.StrictRedis(host="localhost", port=6379, db=1)
conn.set('num', 1) def change_num(conn):
for i in xrange(100000):
┆ conn.incr('num', 5)
┆ conn.decr('num', 5) if __name__ == '__main__':
conn_pool = [redis.StrictRedis(host="localhost", port=6379, db=1)
for i in xrange(5)]
t_pool = []
for conn in conn_pool:
t = threading.Thread(target=change_num, args=(conn,))
t_pool.append(t)
for t in t_pool:
t.start()
for t in t_pool:
t.join()
print conn.get('num')

模拟的5个客户端同时对Redis中的num值进行操作,num最终结果会保持为1:

1
real 0m46.463s
user 0m28.748s
sys 0m6.276s

利用Redis中单个操作和事务的原子性可以做很多事情,最简单的就是做全局计数器了。

比如在短信验证码业务中,要限制一个用户在一分钟内只能发送一次,如果使用关系型数据库,需要为每个手机号记录上次发送短信的时间,当用户请求验证码时,取出与当前时间进行对比。

这一情况下,当用户短时间点击多次时,不仅增加了数据库压力,而且还会出现同时查询均符合条件但数据库更新短信发送时间较慢的问题,就会重复发送短信了。

在Redis中解决这一问题就很简单,只需要用手机号作为key创建一个生存期限为一分钟的数值即可。key不存在时能发送短信,存在时则不能发送短信:

def can_send(phone):
key = "message:" + str(phone)
if conn.set(key, 0, nx=True, ex=60):
┆ return True
else:
┆ return False

至于一些不可名的30分钟内限制访问或者下载5次的功能,将用户ip作为key,值设为次数上限,过期时间设为限制时间,每次用户访问时自减即可:

def can_download(ip):
key = "ip:" + str(ip)
conn.set(key, 5, nx=True, ex=600)
if conn.decr(key) >= 0:
┆ return True
else:
┆ return False

Redis基本事务与乐观锁

虽然Redis单个命令具有原子性,但当多个命令并行执行的时候,会有更多的问题。

比如举一个转账的例子,将用户A的钱转给用户B,那么用户A的账户减少需要与B账户的增多同时进行:

import threading
import time import redis conn = redis.StrictRedis(host="localhost", port=6379, db=1)
conn.mset(a_num=10, b_num=10) def a_to_b():
if int(conn.get('a_num')) >= 10:
conn.decr('a_num', 10)
time.sleep(.1)
conn.incr('b_num', 10)
print conn.mget('a_num', "b_num") def b_to_a():
if int(conn.get('b_num')) >= 10:
conn.decr('b_num', 10)
time.sleep(.1)
conn.incr('a_num', 10)
print conn.mget('a_num', "b_num") if __name__ == '__main__':
pool = [threading.Thread(target=a_to_b) for i in xrange(3)]
for t in pool:
t.start() pool = [threading.Thread(target=b_to_a) for i in xrange(3)]
for t in pool:
t.start()

运行结果:

['0', '10']
['0', '10']
['0', '0']
['0', '0']
['0', '10']
['10', '10']

出现了账户总额变少的情况。虽然是人为的为自增自减命令之间添加了100ms延迟,但在实际并发很高的情况中是很可能出现的,两个命令执行期间执行了其它的语句。

那么现在要保证的是两个增减命令执行期间不受其它命令的干扰,Redis的事务可以达到这一目的。

Redis中,被MULTI命令和EXEC命令包围的所有命令会一个接一个的执行,直到所有命令都执行完毕为止。一个事务完毕后,Redis才会去处理其它的命令。也就是说,Redis事务是具有原子性的。

python中可以用pipeline来创建事务:

def a_to_b():
if int(conn.get('a_num')) >= 10:
┆ pipeline = conn.pipeline()
┆ pipeline.decr('a_num', 10)
┆ time.sleep(.1)
┆ pipeline.incr('b_num', 10)
┆ pipeline.execute()
print conn.mget('a_num', "b_num") def b_to_a():
if int(conn.get('b_num')) >= 10:
┆ pipeline = conn.pipeline()
┆ pipeline.decr('b_num', 10)
┆ time.sleep(.1)
┆ pipeline.incr('a_num', 10)
┆ pipeline.execute()
print conn.mget('a_num', "b_num")

结果:

['0', '20']
['10', '10']
['-10', '30']
['-10', '30']
['0', '20']
['10', '10']

可以看到,两条语句确实一起执行了,账户总额不会变,但出现了负值的情况。这是因为事务在exec命令被调用之前是不会执行的,所以用读取的数据做判断与事务执行之间就有了时间差,期间实际数据发生了变化。

为了保持数据的一致性,我们还需要用到一个事务命令WATCH。WATCH可以对一个键进行监视,监视后到EXEC命令执行之前,如果被监视的键值发生了变化(替换,更新,删除等),EXEC命令会返回一个错误,而不会真正的执行:

>>> pipeline.watch('a_num')
True
>>> pipeline.multi()
>>> pipeline.incr('a_num',10)
StrictPipeline<ConnectionPool<Connection<host=localhost,port=6379,db=1>>>
>>> pipeline.execute()
[20]
>>> pipeline.watch('a_num')
True
>>> pipeline.incr('a_num',10) #监视期间改变被监视键的值
30
>>> pipeline.multi()
>>> pipeline.incr('a_num',10)
StrictPipeline<ConnectionPool<Connection<host=localhost,port=6379,db=1>>>
>>> pipeline.execute()
raise WatchError("Watched variable changed.")
redis.exceptions.WatchError: Watched variable changed.

现在为代码加上watch:

def a_to_b():
pipeline = conn.pipeline()
try:
┆ pipeline.watch('a_num')
┆ if int(pipeline.get('a_num')) < 10:
┆ ┆ pipeline.unwatch()
┆ ┆ return
┆ pipeline.multi()
┆ pipeline.decr('a_num', 10)
┆ pipeline.incr('b_num', 10)
┆ pipeline.execute()
except redis.exceptions.WatchError:
┆ pass
print conn.mget('a_num', "b_num") def b_to_a():
pipeline = conn.pipeline()
try:
┆ pipeline.watch('b_num')
┆ if int(pipeline.get('b_num')) < 10:
┆ ┆ pipeline.unwatch()
┆ ┆ return
┆ pipeline.multi()
┆ pipeline.decr('b_num', 10)
┆ pipeline.incr('a_num', 10)
┆ pipeline.execute()
except redis.exceptions.WatchError:
┆ pass
print conn.mget('a_num', "b_num")

结果:

['0', '20']
['10', '10']
['20', '0']

成功实现了账户转移,但是有三次尝试失败了,如果要尽可能的使每次交易都获得成功,可以加尝试次数或者尝试时间:

def a_to_b():
pipeline = conn.pipeline()
end = time.time() + 5
while time.time() < end:
┆ try:
┆ ┆ pipeline.watch('a_num')
┆ ┆ if int(pipeline.get('a_num')) < 10:
┆ ┆ ┆ pipeline.unwatch()
┆ ┆ ┆ return
┆ ┆ pipeline.multi()
┆ ┆ pipeline.decr('a_num', 10)
┆ ┆ pipeline.incr('b_num', 10)
┆ ┆ pipeline.execute()
┆ ┆ return True
┆ except redis.exceptions.WatchError:
┆ ┆ pass
return False

这样,Redis可以使用事务实现类似于锁的机制,但这个机制与关系型数据库的锁有所不同。关系型数据库对被访问的数据行进行加锁时,其它客户端尝试对被加锁数据行进行写入是会被阻塞的。

Redis执行WATCH时并不会对数据进行加锁,如果发现数据已经被其他客户端抢先修改,只会通知执行WATCH命令的客户端,并不会阻止修改,这称之为乐观锁。

用SET()构建锁

用WACTH实现的乐观锁一般情况下是适用的,但存在一个问题,程序会为完成一个执行失败的事务而不断地进行重试。当负载增加的时候,重试次数会上升到一个不可接受的地步。

如果要自己正确的实现锁的话,要避免下面几个情况:

  • 多个进程同时获得了锁
  • 持有锁的进程在释放锁之前崩溃了,而其他进程却不知道
  • 持有锁的进行运行时间过长,锁被自动释放了,进程本身不知道,还会尝试去释放锁

Redis中要实现锁,需要用到一个命令,SET()或者说是SETNX()。SETNX只会在键不存在的情况下为键设置值,现在SET命令在加了NX选项的情况下也能实现这个功能,而且还能设置过期时间,简直就是天生用来构建锁的。

只要以需要加锁的资源名为key设置一个值,要获取锁时,检查这个key存不存在即可。若存在,则资源已被其它进程获取,需要阻塞到其它进程释放,若不存在,则建立key并获取锁:

import time
import uuid class RedisLock(object): def __init__(self, conn, lockname, retry_count=3, timeout=10,):
self.conn = conn
self.lockname = 'lock:' + lockname
self.retry_count = int(retry_count)
self.timeout = int(timeout)
self.unique_id = str(uuid.uuid4()) def acquire(self):
retry = 0
while retry < self.retry_count:
if self.conn.set(lockname, self.unique_id, nx=True, ex=self.timeout):
return self.unique_id
retry += 1
time.sleep(.001)
return False def release(self):
if self.conn.get(self.lockname) == self.unique_id:
self.conn.delete(self.lockname)
return True
else:
return False

获取锁的默认尝试次数限制3次,3次获取失败则返回。锁的生存期限默认设为了10s,若不主动释放锁,10s后锁会自动消除。

还保存了获取锁时锁设置的值,当释放锁的时候,会先判断保存的值和当前锁的值是否一样,如果不一样,说明是锁过期被自动释放然后被其它进程获取了。所以锁的值必须保持唯一,以免释放了其它程序获取的锁。

使用锁:

def a_to_b():
lock = Redlock(conn, 'a_num')
if not lock.acquire():
┆ return False pipeline = conn.pipeline()
try:
┆ pipeline.get('a_num')
┆ (a_num,) = pipeline.execute()
┆ if int(a_num) < 10:
┆ ┆ return False
┆ pipeline.decr('a_num', 10)
┆ pipeline.incr('b_num', 10)
┆ pipeline.execute()
┆ return True
finally:
┆ lock.release()

释放锁时也可以用Lua脚本来告诉Redis:删除这个key当且仅当这个key存在而且值是我期望的那个值:

    unlock_script = """
if redis.call("get",KEYS[1]) == ARGV[1] then
┆ return redis.call("del",KEYS[1])
else
┆ return 0
end"""

可以用conn.eval来运行Lua脚本:

    def release(self):
┆ self.conn.eval(unlock_script, 1, self.lockname, self.unique_id)

这样,一个Redis单机锁就实现了。我们可以用这个锁来代替WATCH,或者与WACTH同时使用。

实际使用中还要根据业务来决定锁的粒度的问题,是锁住整个结构还是锁住结构中的一小部分。

粒度越大,性能越差,粒度越小,发生死锁的几率越大。

Redis锁构造的更多相关文章

  1. (实例篇)php 使用redis锁限制并发访问类示例

    1.并发访问限制问题 对于一些需要限制同一个用户并发访问的场景,如果用户并发请求多次,而服务器处理没有加锁限制,用户则可以多次请求成功. 例如换领优惠券,如果用户同一时间并发提交换领码,在没有加锁限制 ...

  2. php 使用redis锁限制并发访问类

    1.并发访问限制问题 对于一些需要限制同一个用户并发访问的场景,如果用户并发请求多次,而服务器处理没有加锁限制,用户则可以多次请求成功. 例如换领优惠券,如果用户同一时间并发提交换领码,在没有加锁限制 ...

  3. 解锁redis锁的正确姿势

    解锁redis锁的正确姿势 redis是php的好朋友,在php写业务过程中,有时候会使用到锁的概念,同时只能有一个人可以操作某个行为.这个时候我们就要用到锁.锁的方式有好几种,php不能在内存中用锁 ...

  4. C#多线程编程(6)--线程安全2 互锁构造Interlocked

    在线程安全1中,我介绍了线程同步的意义和一种实现线程同步的方法:volatile.volatile关键字属于原子操作的一种,若对一个关键字使用volatile,很多时候会显得很"浪费&quo ...

  5. redis锁处理并发问题

    redis锁处理并发问题 redis锁处理高并发问题十分常见,使用的时候常见有几种错误,和对应的解决办法. set方式 setnx方式 setnx+getset方式 set方式 加锁:redis中se ...

  6. redis 初步认识四(redis锁,防并发)

    using System; namespace ConsoleAppRedis { class Program { static void Main(string[] args) { //第一种,无登 ...

  7. redis锁机制介绍与实例

    转自:https://m.jb51.net/article/154421.htm 今天小编就为大家分享一篇关于redis锁机制介绍与实例,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要 ...

  8. 定时任务redis锁+自定义lambda优化提取冗余代码

    功能介绍: 我系统中需要跑三个定时任务,由于是多节点部署,为了防止多个节点的定时任务重复执行.所以在定时任务执行时加个锁,抢到锁的节点才能执行定时任务,没有抢到锁的节点就不执行.从而避免了定时任务重复 ...

  9. 多线程并发问题解决之redis锁

    一 问题背景 我们做的是医疗信息化系统,在系统中一条患者信息对医院中当前科室中的所有诊断医生是可见的,当有一个诊断医生点击按钮处理该数据时,数据的状态发生了变化,其他的医生就不可以再处理此患者的数据了 ...

随机推荐

  1. js作用域与执行环境(前端基础系列)

    一.作用域(what?) 官方解释是:"一段程序代码中所用到的名字并不总是有效/可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域." 单从文字理解比较难懂,举个栗子: ...

  2. Activiti源代码分析

    ExecutionEntity内部含有parent,是一个运行树或运行路径.应该是一个流程实例的运行过程,一个实例相应一个ExecutionEntity,通过getActivity得到的是当前正在运行 ...

  3. 《3》CentOS7.0+OpenStack+kvm云平台部署—配置Glance

    感谢朋友支持本博客,欢迎共同探讨交流.因为能力和时间有限,错误之处在所难免,欢迎指正. 假设转载.请保留作者信息. 博客地址:http://blog.csdn.net/qq_21398167 原博文地 ...

  4. 什么是PMI

    项目管理协会 PMI PMI是世界领先的非盈利会员协会的项目管理专业机构 ,在全球185个国家有70多万会员和证书持有人.此外,PMI还是多个英文短语的缩写,较为著名的是采购经理指数PMI. 项目管理 ...

  5. path和classpath细节

    从学习java的最初我们就被要求先设置path变量和classpath变量.但是这两个环境变量到底有什么作用呢? 1.path环境变量 path环境变量的主要作用是告诉操作系统到哪里去寻找某个程序,如 ...

  6. 搭建angular1 gulp项目(上传到gitup)

    (安装好相关的前端环境) 1.新建一个文件夹,名字为angular-gulp,dos命令切换到该目录,输入npm init,继续添上你需要的信息,ok之后目录中多了package.json(管理项目所 ...

  7. vue从入门到女装:从零开始搭建后台管理系统(一)安装框架

    安装及运行都是基于node的,不会node的可以自行百度,网上教程很多,也不难 项目效果预览: demo1 demo2 源码下载 开始安装框架: vue ==>vue-cli安装   eleme ...

  8. 【JavaScript】 JS面向对象的模式与实践 (重点整治原型这个熊孩子 (/= _ =)/~┴┴ )

    参考书籍 <JavaScript高级语言程序设计>—— Nicholas C.Zakas <你不知道的JavaScript>  —— KYLE SIMPSON   在JS的面向 ...

  9. javascript中的异步 macrotask 和 microtask 简介

    javascript中的异步 macrotask 和 microtask 简介 什么是macrotask?什么是microtask?在理解什么是macrotask?什么是microtask之前,我们先 ...

  10. 前端构建之gulp与常用插件(转载)

    原博主:幻天芒 原文地址:http://www.cnblogs.com/humin/p/4337442.html gulp是什么? http://gulpjs.com/ 相信你会明白的! 与著名的构建 ...