Redis-事务篇

同系列一:Redis 缓存数据库入门教程
同系列二:Redis-通用指令篇
同系列三:Redis-RDB-AOF持久化篇html

Redis-事务篇

Redis简介

Redis是C语言开发的一个高性能键值对(key -value) 内存数据库,能够用做数据库,缓存和消息中间件等。web

特色redis

  1. 做为内存数据库,它的性能很是优秀,数据存储在内存当中,读写速度很是快,支持并发10W QPS(每秒查询次数),单进程单线程,是线程安全的,采用IO多路复用机制。sql

  2. 丰富的数据类型,支持字符串,散列,列表,集合,有序集合等,支持数据持久化。能够将内存中数据保存在磁盘中,重启时加载。数据库

  3. 主从复制,哨兵,高可用,可用做分布式锁。能够做为消息中间件使用,支持发布订阅。缓存

什么是事务(Transaction)?

事务,通常是指要作的或者所作的事情,在计算机中是指访问并可能更新数据库中各类数据项的一个程序执行单元,它包含了一组数据操做指令,而且全部的指令都做为一个总体一块儿向系统提交或撤销操做(要么都执行,要么都不执行)事务是恢复和并发控制的基本单位。安全

begin transaction;
    update account set money = money-100 where name = '张三';
    update account set money = money+100 where name = '李四';
commit transaction;

特征

关系型数据库事务具备四大特性:原子性、一致性、隔离性、持久性服务器

简称:ACID属性网络

  • 原子性:事务是一个完整的操做。事务中的各部操做是不可分割的(原子的),要么都执行,要么都不执行数据结构

  • 一致性:事务必须是使数据库从一个一致性状态变到另外一个一致性状态,当事务完成时,数据必须是一致状态

  • 隔离性:对数据操做的全部并发事务都是相互隔离的,事务必须是独立,不依赖或影响其余事务

  • 永久性:事务一旦提交完成后,它对数据库的修改被永久保持,事务日志可以保持事务的永久性,其余操做不该该对其执行结果有任何影响


Redis中的事务?

Redis中的事务其实是一组命令的集合。它的事务支持一次执行多个命令,一个事务中全部命令都会被序列化。将一系列预约义命令打包成一个总体(队列),当执行时,一次性按照添加顺序依次执行,中间不会被打断或者干扰。在事务指向期间,服务端不会中断事务而改去执行其余客户端的命令请求,将事务中的全部命令执行完毕才会去处理其余客户端请求。

Redis事务的做用就是在一个队列中,一次性,顺序性,排他性的执行一系列命令

事务的基本操做

在 Redis 中使用事务会通过 3 个过程

  • 开启事务multi 执行该命令表示一个事务快的开始,在开启完事务的时候,每次操做的命令将会被插入到一个队列中并返回QUEUED,同时这个队列中的命令在事务没有被提交以前不会被实际执行 –>没有隔离级别的概念,可是总归是具备隔离性,毕竟不会受到别的命令打断
    例:
    在这里插入图片描述事务队列结构:
    在这里插入图片描述

  • 执行事务exec 执行该命令后,redis会执行事务块里面的全部命令,该命令须要和 multi 命令成对使用,事务不保证原子性且没有回滚,任意命令执行失败仍是会接着往下执行 –>不保证原子性
    例:
    在这里插入图片描述
    执行事务的流程:
    在这里插入图片描述

  • 取消事务discard 执行命令后,放弃执行该事务的全部命令,取消该事务,该命令须要和 multi 命令成对使用
    在这里插入图片描述
    整体执行流程:
    在这里插入图片描述


事务的工做流程

如图:
在这里插入图片描述
当客户端在给服务器发送一个 X 指令后,服务器大概会进行这么一个操做

判断咱们当前是否存在一个事务的状态,若是当前不是有事务状态,则去识别判断客户端发送过来的 X 是否是一个关于事务操做的指令,若是这个 X 是个普通的指令,那么服务端就直接执行了 X ,最后返回执行结果。

若是这个X是一个事务操做的则有三种状况:

  1. X 为 multi 指令,那么给它开启一个事务队列,返回一个OK给客户端
  2. X 为 discard 指令,因为咱们尚未开启事务状态,因此会直接报错:(error) ERR DISCARD without MULTI
  3. X为 exec 指令,因为咱们尚未开启事务状态,也会报错:(error) ERR EXEC without MULTI

若是当前有事务状态,则去识别判断客户端发送过来的X是否是一个关于事务操做的指令,若是这个X是个普通的指令,那么服务端就将 X 指令加到事务队列中去,最后返回 QUEUED问题:若是在中途输入了一个语法有误的命令,会有什么结果?

若是这个 X 是一个事务操做的则又有三种状况:

  1. X 为 multi 指令,那么它自己已经处于事务状态了,还来一个 multi 那么就会报错了,Redis事务禁止套娃:(error) ERR MULTI calls can not be nestedMULTI命令的发送不会形成整个事务失败,也不会修改事务队列中已有的数据
  2. X 为 discard 指令,那么会将当前事务队列销毁,队列中全部命令都不执行,而后返回 OK
  3. X 为 exec 指令,接下来会进行执行队列中的命令,返回结果。这里会产生一个问题,当咱们输入一条语法正确的命令,可是运行时实际会出现异常的命令,如:set cjcc value,接下来跟着用incr cjcc,会产生什么后果?

事务操做注意事项

定义事务过程当中,命令格式输入错误

在Redis中,若是所定义的事务包含命令出现了语法错误(后面命令照常能够入队),那么这个事务中的全部命令都不会执行,包括正确的命令

127.0.0.1:6379> multi   开启了一个事务
OK
127.0.0.1:6379> set key 1  添加正确的命令到事务队列
QUEUED
127.0.0.1:6379> sett s     添加错误的命令到事务队列
(error) ERR unknown command 'sett'
127.0.0.1:6379> set k k    添加正确的命令到事务队列,发现还能加到队列
QUEUED
127.0.0.1:6379> exec       尝试执行事务,发现报错了,由于上面出现了一条编译错误的命令,而放弃了当前事务
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379>

例:
在这里插入图片描述结果:
在这里插入图片描述


定义事务过程当中,命令执行错误

在Redis中,若是所定义的事务队列命令语法正确,可是没法正确的执行(运行时异常,如Java的除0异常之类。。。。,在这Redis里就是至关于对一个字符串进行自增)那么运行错误的命令不会被执行,其余正确的命令都会被执行

127.0.0.1:6379> multi
OK
127.0.0.1:6379> set cjcc value    添加正确的命令到事务队列
QUEUED
127.0.0.1:6379> incr cjcc         添加正确的命令到事务队列,自增字符串运行会出错
QUEUED
127.0.0.1:6379> set cjcc1 value1  添加正确的命令到事务队列
QUEUED
127.0.0.1:6379> exec
1) OK       执行成功
2) (error) ERR value is not an integer or out of range
3) OK       执行成功
127.0.0.1:6379>

在这里插入图片描述

注意:

单个 Redis 命令的执行是原子性的,但 Redis 没有在事务上增长任何维持原子性的机制,因此 Redis 事务的执行并非原子性的,因此也不具备一致性

redis的事务能够理解为一个打包的批量执行脚本,但批量指令并不是原子化的操做,中间某条指令的失败不会致使前面已作指令的回滚,也不会形成后续的指令不作(前面有图)。redis提供了一个简单的事务,这也是和传统关系型数据库的区别,但凡报错,整个事务必回滚


事务回滚

手动进行事务回滚

记录操做过程当中被影响的数据以前的状态,把他存储起来,对于单个的对于string这种,存储前先获取值存起来,对于hash,list,set,zset 先拷贝一份副本,万一出问题了用将整个副本恢复回去。

对于一些重要的操做,咱们必须经过程序去检测数据的正确性,以保证 Redis 事务的正确执行,避免出现数据不一致的状况。Redis 之因此保持这样简易的事务,彻底是为了保证移动互联网的核心问题——性能。

Redis——锁

watch 命令

watch key1 [key2……]

WATCH 是一个乐观锁(CAS(Compare And Swap)CAS会出现ABA问题)。命令用于监视一个(或多个) key ,若是在事务执行以前这个(或这些) key 被其余命令所改动,那么事务将被拒绝执行,并向客户端返回表明事务执行失败的空回复,其实就是你们都监控同一个或几个点,我想操做什么东西的时候,只要你们都没有动它,那么我就会进行操做,若是发现有人动了即会被监控发现触发了这个条件,那我就取消此次事务操做

在这里插入图片描述

每一个redis数据库都保存着一个 watched_keys 字典,这个字典的键是某个被 watch 命令监视的数据库键,而字典的值则是一个链表,链表中记录了全部监视相应数据库键的客户端,数据结构以下图:

c1…c2表明有c1和c2两个客户端在监控name这个key
在这里插入图片描述
全部对数据库进行修改命令,在执行以后都会对 watched_keys 字典进行检查,查看是否有客户端正在监视刚刚被命令修改过的数据库键,若是有,则会将监视被修改键的 reids_dirty_cas 标识打开,
表示该客户端的事务安全性已经被破坏.若是在事务提交时,检测到该标识被打开,则会拒绝执行它们提交的事务,以此来保证事务的安全性.
在这里插入图片描述
WATCH 只能在客户端进入事务状态以前执行,在事务状态下发送 WATCH 命令会引起一个错误,但它不会形成整个事务失败,也不会修改事务队列中已有的数据(和前面处理 MULTI 的状况同样

那么 watch 会不会出现ABA问题?

ABA问题:一个线程把数据A变为了B,而后又从新变成了A。此时另一个线程读取的时候,发现A没有变化,就误觉得是原来的那个A

代码位置:multi.c >> void touchWatchedKey(redisDb *db, robj *key)

/* "Touch" a key, so that if this key is being WATCHed by some client the * next EXEC will fail. */
void touchWatchedKey(redisDb *db, robj *key) {
    list *clients;
    listIter li;
    listNode *ln;

    if (dictSize(db->watched_keys) == 0) return;
    clients = dictFetchValue(db->watched_keys, key);
    if (!clients) return;

    /* Mark all the clients watching this key as CLIENT_DIRTY_CAS */
    /* Check if we are already watching for this key */
    listRewind(clients,&li);
    while((ln = listNext(&li))) {
        client *c = listNodeValue(ln);

        c->flags |= CLIENT_DIRTY_CAS;
    }
}

每当key的内容被修改时,则遍历全部watch了该 key 的客户端,设置相应的状态为CLIENT_DIRTY_CAS,因此不会出现 ABA 问题

看图:
在这里插入图片描述


unwatch 命令

unwatch  //取消对全部key的监控

若是在执行 WATCH 命令以后, EXEC 命令或 DISCARD 命令先被执行了的话,那么就不须要再执行 UNWATCH 。由于 EXEC 命令会执行事务,所以 WATCH 命令的效果已经产生了;而 DISCARD 命令在取消事务的同时也会取消全部对 key 的监视,所以这两个命令执行以后,就没有必要执行 UNWATCH

分布式锁

setnx(set if not exists)实现

命令介绍

set if not exists 若是不存在,则 SET

  • 使用setnx 设置一个公共锁
    • 有值则返回设置失败: 0
    • 无值则返回设置成功: 1
setnx lock-key value

在这里插入图片描述
利用分布式锁控制商城超卖场景的问题: 电商618活动热卖商品 X ,很是多客户抢购,3S内将全部商品购买完毕,如何防止最后一件商品被多人同时购买。

这时候,使用上面的watch监控一个库存的key,还能不能解决问题?

不能,这个key会一直变化,如从100一点点的减到了0 ,这样的话,作了一次,剩下的事务所有被放弃了,一我的订购了一个个X商品,其余人订购的就所有被取消掉,这是不现实的。毕竟库存都还没必定到最后一件,由于watch 主要是监控某一个 key 值,有没有被其余客户端改变过,而不是控制其余客户端能不能修改这个值,因此这个时候用 watch 就不合适了,因此须要引入新的方法 setnx

watch setnx
key 有没被别人改过 key 能不能被别人改

用法:利用setnx 命令的返回值特征

  • 对于返回设置成功的,拥有控制权,进行下一步的具体业务操做,如:incr cjcc
  • 对于返回设置失败的,没有控制权,进行排队或者等待操做完毕
  • 操做完成后,经过del命令操做释放锁

在这里插入图片描述


改良1 setnx + expire

须要注意的是: 当咱们用了上述方案设计了简单的分布式锁,已经能够实现控制客户端能不能具体控制对应的某个业务了,问题来了,当咱们设置锁了以后,客户端挂了/停电了(),可是它恰恰已经得到锁了,没来得及打开,这样就容易产生了死锁的风险。因此咱们要有一个保底机制,当用户控制加锁后,不能仅仅由用户进行解锁,咱们在系统层面也须要作到能控制锁的释放,因此改进方案以下:使用 expire 命令为锁定的key添加一个时间限定,到时释放锁
在这里插入图片描述
命令

expire key second
pexpire key milliseconds

因而如今命令变成了:
setnx cjcc 1
expire cjcc 10 //设置cjcc值为1,cjcc过时时间为10秒

在这里插入图片描述


改良2 set扩展参数

可是,上面改良的方案还有一种可能,这种解决方案终归仍是2条命令组成的,万一在设置完成值后,还没来得及设置expire过时时间,系统就已经挂掉了。锁未释放,也会引起死锁问题,新的解决方案:
set key value NX [EX seconds] [PX milliseconds]
在这里插入图片描述

set参考: SET 命令的行为能够经过一系列参数来修改by http://redisdoc.com/string/set.html

EX seconds : 将键的过时时间设置为 seconds 秒。 执行 SET key value EX seconds 的效果等同于执行 SETEX key seconds value 。

PX milliseconds : 将键的过时时间设置为 milliseconds 毫秒。 执行 SET key value PX milliseconds 的效果等同于执行 PSETEX key milliseconds value 。

NX : 只在键不存在时, 才对键进行设置操做。 执行 SET key value NX 的效果等同于执行 SETNX key value 。

XX : 只在键已经存在时, 才对键进行设置操做。

由于 SET 命令能够经过参数来实现 SETNXSETEX 以及 PSETEX 命令的效果, 因此 Redis 未来的版本可能会移除并废弃 SETNXSETEXPSETEX 这三个命令。

锁过时时间设置

上面设置的锁的时间都是为了方便测试,截图,要是生产上真设置几秒一次,那就真的玩蛇了。
通常操做一般都是微秒或者毫秒级,因此锁定时间不合适设置太大了,至于具体时间,看本身业务测试来肯定区间【持有锁的最短执行时间,最长执行时间】以及网络请求耗时之类的来定。

其余文章

同系列一:Redis 缓存数据库入门教程
同系列二:Redis-通用指令篇
同系列三:Redis-RDB-AOF持久化篇