原子性
原子性是数据库的事务中的特性。在数据库事务的情景下,原子性指的是:一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。
对于Redis而言,命令的原子性指的是:一个操作的不可以再分,操作要么执行,要么不执行。
Redis操作原子性的原因
Redis的操作之所以是原子性的,是因为Redis是单线程的。
由于对操作系统相关的知识不是很熟悉,从上面这句话并不能真正理解Redis操作是原子性的原因,进一步查阅进程与线程的概念及其区别。
进程与线程
进程
计算机中已执行程序的实体。比如,一个启动了的php-fpm,就是一个进程。
线程
操作系统能够进行运算调度的最小单元。它被包含在进程之中,是进程的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。比如,mysql运行时,mysql启动后,该mysql服务就是一个进程,而mysql的连接、查询的操作,就是线程。
进程与线程的区别
资源(如打开文件):进程间的资源相互独立,同一进程的各线程间共享资源。某进程的线程在其他进程不可见。
通信:进程间通信:消息传递、同步、共享内存、远程过程调用、管道。线程间通信:直接读写进程数据段(需要进程同步和互斥手段的辅助,以保证数据的一致性)。
调度和切换:线程上下文切换比进程上下文切换要快得多。
线程,是操作系统最小的执行单元,在单线程程序中,任务一个一个地做,必须做完一个任务后,才会去做另一个任务。
Redis在并发中的表现
Redis的API是原子性的操作,那么多个命令在并发中也是原子性的吗?
看看下面这段代码:
$redis= newRedis();
$redis->connect('127.0.0.1',6379);
for($i= 0;$iget('val');
$num++;
$redis->set('val',$num);
usleep(10000);
}
用两个终端执行上面的程序,发现val的结果是小于2000的值,那么可以知道,在程序中执行多个Redis命令并非是原子性的,这也和普通数据库的表现是一样的。
如果想在上面的程序中实现原子性,可以将get和set改成单命令操作,比如incr,或者使用Redis的事务,或者使用Redis+Lua的方式实现。
原子性总结
综上所述,对Redis来说,执行get、set以及eval等API,都是一个一个的任务,这些任务都会由Redis的线程去负责执行,任务要么执行成功,要么执行失败,这就是Redis的命令是原子性的原因。
Redis本身提供的所有API都是原子操作,Redis中的事务其实是要保证批量操作的原子性。
事务
MULTI 、 EXEC 、 DISCARD 和 WATCH 是 Redis 事务相关的命令。事务可以一次执行多个命令, 并且带有以下两个重要的保证:
事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。
事务的关键字
EXEC 命令负责触发并执行事务中的所有命令:
如果客户端在使用 MULTI 开启了一个事务之后,却因为断线而没有成功执行 EXEC ,那么事务中的所有命令都不会被执行。
另一方面,如果客户端成功在开启事务之后执行 EXEC ,那么事务中的所有命令都会被执行。
当使用 AOF 方式做持久化的时候, Redis 会使用单个 write(2) 命令将事务写入到磁盘中。
然而,如果 Redis 服务器因为某些原因被管理员杀死,或者遇上某种硬件故障,那么可能只有部分事务命令会被成功写入到磁盘中。
如果 Redis 在重新启动时发现 AOF 文件出了这样的问题,那么它会退出,并汇报一个错误。
使用redis-check-aof程序可以修复这一问题:它会移除 AOF 文件中不完整事务的信息,确保服务器可以顺利启动。
从 2.2 版本开始,Redis 还可以通过乐观锁(optimistic lock)实现 CAS (check-and-set)操作,具体信息请参考文档的后半部分。
事务的语句
> MULTI
OK
> INCR foo
QUEUED
> INCR bar
QUEUED
> EXEC
1) (integer) 1
2) (integer) 1
为什么 Redis 不支持回滚(roll back)
如果你有使用关系式数据库的经验, 那么 “Redis 在事务失败时不进行回滚,而是继续执行余下的命令”这种做法可能会让你觉得有点奇怪。
以下是这种做法的优点:
Redis 命令只会因为错误的语法而失败(并且这些问题不能在入队时发现),或是命令用在了错误类型的键上面:这也就是说,从实用性的角度来说,失败的命令是由编程错误造成的,而这些错误应该在开发的过程中被发现,而不应该出现在生产环境中。
因为不需要对回滚进行支持,所以 Redis 的内部可以保持简单且快速。
有种观点认为 Redis 处理事务的做法会产生 bug , 然而需要注意的是, 在通常情况下, 回滚并不能解决编程错误带来的问题。 举个例子, 如果你本来想通过 INCR 命令将键的值加上 1 , 却不小心加上了 2 , 又或者对错误类型的键执行了 INCR , 回滚是没有办法处理这些情况的。