Redis Transactions

基础知识

事务

所谓事务(Transaction) ,是指作为单个逻辑工作单元执行的一系列操作。事务必须满足ACID原则(原子性、一致性、隔离性和持久性)。简单来说事务其实就是打包一组操作(或者命令)作为一个整体,在事务处理时将顺序执行这些操作,并返回结果,如果其中任何一个环节出错,所有的操作将被回滚。

事务的四大特性(ACID):

  • 原子性 事务是数据库的逻辑工作单位,事务中包含的各操作要么都做,要么都不做
  • 一致性 事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。因此当数据库只包含成功事务提交的结果时,就说数据库处于一致性状态。如果数据库系统 运行中发生故障,有些事务尚未完成就被迫中断,这些未完成事务对数据库所做的修改有一部分已写入物理数据库,这时数据库就处于一种不正确的状态,或者说是 不一致的状态。
  • 隔离性 一个事务的执行不能其它事务干扰。即一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相干扰。
  • 持续性 也称永久性,指一个事务一旦提交,它对数据库中的数据的改变就应该是永久性的。接下来的其它操作或故障不应该对其执行结果有任何影响。

当程序中可能出现并发的情况时,就需要通过一定的手段来保证在并发情况下数据的准确性,通过这种手段保证了当前用户和其他用户一起操作时,所得到的结果和他单独操作时的结果是一样的。这种手段就叫做并发控制。并发控制的目的是保证一个用户的工作不会对另一个用户的工作产生不合理的影响。

常说的并发控制,一般都和数据库管理系统(DBMS)有关。在DBMS中的并发控制的任务,是确保在多个事务同时存取数据库中同一数据时,不破坏事务的隔离性和统一性以及数据库的统一性。

锁(LOCKING)便是最常用的并发控制机构,是防止其他事务访问指定的资源控制、实现并发控制的一种主要手段。

实现并发控制的主要手段大致可以分为乐观并发控制和悲观并发控制两种。

悲观锁(Pessimistic Lock)

当要对数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发。这种借助数据库锁机制,在修改数据之前先锁定,再修改的方式被称之为悲观并发控制【又名“悲观锁”,Pessimistic Concurrency Control,缩写“PCC”】。悲观锁有两种模式:

  • 共享锁【Shared lock】又称为读锁,简称S锁。顾名思义,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。

  • 排他锁【Exclusive lock】又称为写锁,简称X锁。顾名思义,排他锁就是不能与其他锁并存,如果一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据行读取和修改。

悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。

悲观锁

但是在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁的机会。另外还会降低并行性,一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完才可以处理那行数据。

举例(MySql-Innodb):

1
2
3
4
5
6
7
8
9
10
//0.开启事务
begin
//1.查询商品库存信息
select quantity from items where id=1 for update;
//2.修改库存
update item set quantity=2 where id=1;
//3.提交事务
commit;

// 以上,在对id = 1的记录修改前,先通过for update的方式进行加锁,然后再进行修改

上面提到,使用select…for update会把数据给锁住,不过需要注意一些锁的级别,MySQL InnoDB默认行级锁。行级锁都是基于索引的,如果一条SQL语句用不到索引是不会使用行级锁的,会使用表级锁把整张表锁住,这点需要注意

乐观锁(Optimistic Locking)

乐观锁是相对悲观锁而言的,乐观锁假设数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回给用户错误的信息,让用户决定如何去做。

相对于悲观锁,在对数据库进行处理的时候,乐观锁并不会使用数据库提供的锁机制。一般的实现乐观锁的方式就是记录数据版本。

乐观锁

乐观并发控制相信事务之间的数据竞争(data race)的概率是比较小的,因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁。

示例(MySql-Innodb):

1
2
3
4
5
6
//查询商品库存信息,quantity = 3
select quantity from items where id=1;
// 修改商品库存为2
update items set quantity=2 where id=1 and quantity=3;

// 以上,在更新之前,先查询一下库存表中当前库存数(quantity),然后在做update的时候,以库存数作为一个修改条件。当提交更新的时候,判断数据库表对应记录的当前库存数与第一次取出来的库存数进行比对,如果数据库表当前库存数与第一次取出来的库存数相等,则予以更新,否则认为是过期数据。

Redis事务

在Redis中实现事务主要依靠一下5个命令来实现:

  • MULTI 标记一个事务块的开始。
  • EXEC 执行所有事务块内的命令。
  • DISCARD 取消事务,放弃执行事务块内的所有命令。
  • WATCH key [key …] 监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
  • UNWATCH 取消 WATCH 命令对所有 key 的监视。

Redis事务从开始到结束通常会通过三个阶段: 事务开始 -> 命令入队 -> 事务执行

开启事务

放弃事务

编译时错误(入队前就能检测出来)

从 Redis 2.6.5 开始,服务器会对命令入队失败的情况进行记录,并在客户端调用 EXEC 命令时,拒绝执行并自动放弃这个事务

运行时错误(入队前不能检测出来)

即使事务中有某个/某些命令在执行时产生了错误, 事务中的其他命令仍然会继续执行

WATCH监控

WATCH指令,类似乐观锁,事务提交时,如果Key的值已被别的客户端改变,整个事务队列都不会被执行

应用

服务器访问并发比较大,无效访问频繁,比如说频繁请求接口,爬虫频繁访问服务器,抢购瞬时请求过大,我们需要限流(对访问来源计数,超过设定次数,设置过期时间,提醒访问频繁,稍后再试)处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
limits=500   #设置1秒内限制次数50
if EXISTS userid
return '访问频繁,锁定时间剩余(ttl userid)秒'
if userid_count_time > limits
exprice userid,3600
return '访问频繁,稍后再试'
else
MUlTI
incr userid_count_time # 对用户每秒的请求进行原子递增计数
exprice userid_count_time , 60
EXEC

//使用事务的目的是避免执行错误中断,userid_count_time持久化到磁盘,高并发下这个很有必要

Redis事务和Mysql事务区别

mysql redis
开启事务 start transaction multi
回滚事务 rollback 不能回滚,使用discard命令可以放弃事务队列
提交事务 commit, 即使遇到语法错误也会提交 exec, 如果遇到语法错误会放弃事务中的sql
悲观锁 使用select … for update实现悲观锁
乐观锁 通常使用version或时间戳来实现乐观锁 使用watch监控对象变化来实现乐观锁
原子性 具备 具备
一致性 具备 具备
隔离性 具备 具备
持久性 具备 当redis服务器使用AOF持久化模式并appendfsync设置为always时具备

了解更多:https://redis.io/topics/transactions

关注作者公众号,获取更多资源!
赏作者一杯咖啡~