1. 事务描述
(1)什么是事务
事务, 就是把一堆事情绑在一起, 按顺序的执行, 都成功了才算完成, 否则恢复之前的样子
事务必须服从 ACID 原则, ACID 原则分别是原子性 (atomicity), 一致性(consistency), 隔离性(isolation) 和持久性(durability)
原子性: 操作这些指令时, 要么全部执行成功, 要么全部不执行. 只要其中一个指令执行失败, 所有的指令都执行失败, 数据进行回滚, 回到执行指令前的数据状态
一致性: 事务的执行使数据从一个状态转换为另一个状态, 但是对于整个数据的完整性保持稳定
隔离性: 在该事务执行的过程中, 无论发生的任何数据的改变都应该只存在于该事务之中, 对外界不存在影响, 只有在事务确认提交之后们才会显示该事务对数据的改变, 其他事务才能获取到这些改变后的数据
持久性: 当事务正确完成后, 它对于数据的改变是永久性的
(2)并发事务导致的常见错误
第一类丢失更新: 撤销一个事务时, 把其他事务已提交的更新数据覆盖
脏读: 一个事务读取到另一个事务未提交的更新数据
幻读也叫虚读: 一个事务执行两次查询, 第二次结果集包含第一次中没有或某些行已经被删除的数据, 造成两次结果不一致, 只是另一个事务在这两次查询中间插入或删除了数据造成的
不可重复读: 一个事务两次读取同一行的数据, 结果得到不同状态的结果, 中间正好另一个事务更新了该数据, 两次结果相异, 不可被信任
第二类丢失更新: 是不可重复读的特殊情况. 如果两个事物都读取同一行, 然后两个都进行写操作, 并提交, 第一个事物所做的改变就会丢失
2. Redis 事务处理
Redis 事务通过 MULTI,WATCH,UNWAYCH,EXEC,DISCARD 五个命令实现
MUTIL 命令: 用于开启一个事务, 客户端可以继续向服务器发送任意多条命令, 这些命令不会立即被执行, 而是被放到一个队列中, 当 EXEC 命令被调用时, 所有队列中的命令才会被执行, 它总是返回 OK
WATCH 命令: 对键进行监视, 直到用户执行 EXEC 命令的这段时间里面, 如果其他客户端抢先对任何被监视的键进行了替换, 更新或删除等操作, 那么当用户舱室执行 EXEC 命令的时候, 事务将失败并返回一个错误(之后用户可以选择重试或者放弃事务)
UNWATCH 命令: 在 WATCH 命令执行之后, EXEC 命令执行之前对链接进行重置
EXEC 命令: 执行事务命令
DISCARD 命令: 客户端可以取消 WATCH 命令名清空所有已入队命令
3. Redis 事务示例
下面将用商品交易示例说明事务处理过程
(1)将商品投放到市场
a. 使用散列 (users:) 来管理市场中的所有用户信息, 包括用户名, 用户拥有的钱
b. 使用集合 (inventory:) 来管理每个用户的所有商品信息, 包括商品名名
inventory:1 对应用户 User:1 的报告
c. 使用有序集合 (market:) 来管理投放到市场中的商品
流程: 检查 inventory:1 包裹中是否含有 ItemL 商品 ----->将 inventory:1 包裹中的 ItemL 商品添加到交易市场 ----->删除 inventory:1 包裹中的 ItemL 商品
- def list_item(conn,userid,goodsname,price):
- #使用有序集合 (market:) 来管理投放到市场中的商品
- #商品的 key 为 userid:goodsid
- inventory = 'inventory:%s' %(userid)
- user = 'User:%s' %(userid)
- goodsitem = '%s:%s' %(userid,goodsname)
- end = time.time() + 5
- pipe = conn.pipeline()
- while time.time() < end:
- try:
- pipe.watch(inventory) #监视包裹发生的变化
- if not pipe.sismember(inventory,goodsname): #检查用户是否仍然持有将要被放入市场的商品
- pipe.unwatch()
- return None
- pipe.multi()
- pipe.zadd('market:',{goodsitem:price}) #将商品投放到市场
- pipe.srem(inventory, goodsname) #从用户包裹中删除该商品
- pipe.execute()
- print('商场中的商品:',conn.zrange('market:', 0, -1,withscores=True))
- print('商品投放市场后用户 {} 的商品:{}'.format(userid, conn.smembers(inventory)))
- return True
- except Redis.exceptions.WatchError as e:
- print(e)
- return False
测试:
(2)交易: User:2 购买交易市场中的中的 ItemL 商品
思想: 使用 watch 对市场以及买家的个人信息进行监视, 然后获取买家拥有的钱数以及商品市场的售价, 并检查买家是否有足够的钱来购买商品, 如果买家没有足够的钱, 那么程序会取消事务, 如果买家钱足够, 那么程序首先会将买家支付的钱转移给卖家, 然后将售出的商品移除商品交易市场.
- def purchase_item(conn, buyerid, goodsname, sellerid):
- buyer = 'User:{}'.format(buyerid) #买家 key
- seller = 'User:{}'.format(sellerid) #卖家 key
- buy_inventory = 'inventory:{}'.format(buyerid) #买家包裹 key
- sell_inverntory = 'inventory:{}'.format(sellerid) #卖家包裹 key
- goodsitem = '{}:{}'.format(sellerid,goodsname) #市场中的商品 key
- end = time.time() + 5
- pipe = conn.pipeline()
- while time.time() < end:
- try:
- pipe.watch('market:',buyer) #对商品买卖市场以及买家的个人信息进行监视
- funds = int(pipe.hget(buyer,'funds')) #得到买家拥有的钱
- print('买家手上的钱:', funds)
- price = pipe.zscore('market:', goodsitem) #得到商品的价格
- print('price:',price)
- if funds < price:
- pipe.unwatch()
- return None
- pipe.multi()
- pipe.hincrby(buyer, 'funds', int(-price)) #买家的钱减少
- pipe.hincrby(seller, 'funds', int(price)) #卖家的钱增加
- pipe.sadd(buy_inventory,goodsname)
- pipe.zrem('market:', goodsitem)
- pipe.execute()
- print('交易市场中的商品:', conn.zrange('market:', 0, -1, withscores=True))
- print('买家现在手中的钱:',conn.hget(buyer, 'funds'))
- print('买家包裹内容:', conn.smembers(buy_inventory))
- print('卖家现在手中的钱:',conn.hget(seller, 'funds'))
- return True
- except Redis.exceptions.WatchError as e:
- print(e)
- return False
测试:
- if __name__ == '__main__':
- conn = Redis.Redis()
- #create_users(conn)
- #create_user_inventory(conn)
- list_item(conn,1,'ItemL',28)
- purchase_item(conn, 2, 'ItemL', 1)
4. Redis 持久化
Redis 提供了两种不同的持久化方式来将数据从内存存储到硬盘里面, 一种是 RDB 持久化, 原理是将 Redis 内存中的数据库记录定时 dump 到磁盘上的 RDB 持久化, 另外一种是 AOF(append only file)持久化, 原理是被执行的写命令复制到磁盘里.
(1)RDB 持久化配置
- # Save the DB on disk:
- # 设置 sedis 进行数据库镜像的频率.
- # 900 秒 (15 分钟) 内至少 1 个 key 值改变(则进行数据库保存 -- 持久化).
- # 300 秒 (5 分钟) 内至少 10 个 key 值改变(则进行数据库保存 -- 持久化).
- # 60 秒 (1 分钟) 内至少 10000 个 key 值改变(则进行数据库保存 -- 持久化).
- save 900 1
- save 300 10
- save 60 10000
- stop-writes-on-bgsave-error yes
- # 在进行镜像备份时, 是否进行压缩. yes: 压缩, 但是需要一些 CPU 的消耗. no: 不压缩, 需要更多的磁盘空间.
- rdbcompression yes
- # 一个 CRC64 的校验就被放在了文件末尾, 当存储或者加载 rbd 文件的时候会有一个 10% 左右的性能下降, 为了达到性能的最大化, 你可以关掉这个配置项.
- rdbchecksum yes
- # 快照的文件名
- dbfilename dump.rdb
- # 存放快照的目录
- dir ./
(2)创建快照的方法
a. 客户端可以通过想 Redis 发送 besave 命令来创建一个快照, 对于支持 bgsave 命令的平台来说(基本上所有平台都支持, 除了 Windows 平台),Redis 会调用 fork 来创建一个子进程, 然后子进程负责将快照写入磁盘, 而父进程继续处理命令请求
b. 客户端还可以向 Redis 发送 save 命令来创建一个快照, 接到 save 命令的 Redis 服务器在快照创建完毕之前不再响应任何其他命令, save 命令不常用, 我们通常只会在没有足够内存去支持 bgsave 的情况下, 又或者即使等待持久化操作执行完毕也无所谓的情况下, 才会使用这个命令
c. 当 Redis 通过 shutdown 命令接收到关闭服务器请求时, 或者接收到标准 term 命令时, 会执行一个 save 命令, 阻塞所有客户端, 不再执行客户端发送的任何命令, 并在 save 命令执行完毕之后关闭服务器
d. 当一个 Redis 服务器链接另一个 Redis 服务器, 并向对方发送 sync 同步命令来开始一次复制操作时, 如果主服务器目前没有执行 bgsave 操作, 或者主服务器并非刚刚执行完 bgsave 操作, 那么主服务器就会执行 bgsave
(3)AOF 持久化配置
- # 是否开启 AOF, 默认关闭(no)
- appendonly yes
- # 指定 AOF 文件名
- appendfilename appendonly.aof
- # Redis 支持三种不同的刷写模式:
- # appendfsync always #每次收到写命令就立即强制写入磁盘, 是最有保证的完全的持久化, 但速度也是最慢的, 一般不推荐使用.
- appendfsync everysec #每秒钟强制写入磁盘一次, 在性能和持久化方面做了很好的折中, 是受推荐的方式.
- # appendfsync no #完全依赖 OS 的写入, 一般为 30 秒左右一次, 性能最好但是持久化最没有保证, 不被推荐.
- # 在日志重写时, 不进行命令追加操作, 而只是将其放在缓冲区里, 避免与命令的追加造成 DISK IO 上的冲突.
- # 设置为 yes 表示 rewrite 期间对新写操作不 fsync, 暂时存在内存中, 等 rewrite 完成后再写入, 默认为 no, 建议 yes
- no-appendfsync-on-rewrite yes
- # 当前 AOF 文件大小是上次日志重写得到 AOF 文件大小的二倍时, 自动启动新的日志重写过程.
- auto-aof-rewrite-percentage 100
- # 当前 AOF 文件启动新的日志重写过程的最小值, 避免刚刚启动 Reids 时由于文件尺寸较小导致频繁的重写.
- auto-aof-rewrite-min-size 64mb
(4)AOF 重写原理
AOF 重写和 RDB 创建快照一样, 都巧妙地利用了写时复制机制:
a. Redis 执行 fork() , 现在同时拥有父进程和子进程
b. 子进程开始将新 AOF 文件的内容写入到临时文件
c . 对于所有新执行的写入命令, 父进程一边将它们累积到一个内存缓存中, 一边将这些改动追加到现有 AOF 文件的末尾, 这样样即使在重写的中途发生停机, 现有的 AOF 文件也还是安全的
d. 当子进程完成重写工作时, 它给父进程发送一个信号, 父进程在接收到信号之后, 将内存缓存中的所有数据追加到新 AOF 文件的末尾
注意: 在将内存缓存中的数据追加到新 AOF 文件末尾和 rename 时, 主进程是阻塞的
来源: https://www.cnblogs.com/xiaobingqianrui/p/10094845.html