关注公众号:CoderBuff,回复“redis”获取《Redis5.x入门教程》完整版PDF。

第六章 · 事务

我们在学习MySQL的存储殷勤时知道,MySQL中innodb支持事务而myisam不支持事务。而事务具有四个特性:

  • 一致性
  • 原子性
  • 隔离性
  • 持久性

在redis尽管提供了事务相关的命令,但实际上它是一个“假事务”,因为它并不支持回滚,也就是说在redis中一个事务有多个命令执行,并不能保证原子性。所以要使用redis的事务,一定要慎重

Redis中的“假事务”(不保证原子性)

在redis中事务相关的命令一共有以下几个:

watch [key1] [key2]:监视一个或多个key,在事务开始之前如果被监视的key有改动,则事务被打断。

multi:标记一个事务的开始。

exec:执行事务。

discard:取消事务的执行。

unwatch:取消监视的key。

  • 正常执行事务
  1. 127.0.0.1:6379> multi
  2. OK
  3. 127.0.0.1:6379> set name kevin
  4. QUEUED
  5. 127.0.0.1:6379> set age 25
  6. QUEUED
  7. 127.0.0.1:6379> get name
  8. QUEUED
  9. 127.0.0.1:6379> set sex male
  10. QUEUED
  11. 127.0.0.1:6379> exec
  12. 1) OK
  13. 2) OK
  14. 3) "kevin"
  15. 4) OK
  • 取消事务执行

取消事务执行,命令将不会被执行。

  1. 127.0.0.1:6379> multi
  2. OK
  3. 127.0.0.1:6379> set name yulinfeng
  4. QUEUED
  5. 127.0.0.1:6379> set age 26
  6. QUEUED
  7. 127.0.0.1:6379> discard
  8. OK
  9. 127.0.0.1:6379> get name
  10. "kevin"
  • 事务中的命令出现命令性错误,类似Java的编译错误,执行事务时,所有的命令都不会被执行。
  1. 127.0.0.1:6379> multi
  2. OK
  3. 127.0.0.1:6379> set name yulinfeng
  4. QUEUED
  5. 127.0.0.1:6379> setget age 26
  6. (error) ERR unknown command `setget`, with args beginning with: `age`, `26`,
  7. 127.0.0.1:6379> exec
  8. (error) EXECABORT Transaction discarded because of previous errors.
  9. 127.0.0.1:6379> get name
  10. "kevin"
  • 事务中出现执行时错误,类似Java的运行时异常,执行事务时,部分命令会被执行成功,也即是不保证原子性
  1. 127.0.0.1:6379> multi
  2. OK
  3. 127.0.0.1:6379> incr name
  4. QUEUED
  5. 127.0.0.1:6379> set age 26
  6. QUEUED
  7. 127.0.0.1:6379> exec
  8. 1) (error) ERR value is not an integer or out of range
  9. 2) OK
  10. 127.0.0.1:6379> get age
  11. "26"
  • 使用watch监视key在事务之前被改动,正常未被改动时的情况,所有命令正常执行。
  1. 127.0.0.1:6379> watch name
  2. OK
  3. 127.0.0.1:6379> multi
  4. OK
  5. 127.0.0.1:6379> set name yulinfeng
  6. QUEUED
  7. 127.0.0.1:6379> set age 18
  8. QUEUED
  9. 127.0.0.1:6379> exec
  10. 1) OK
  11. 2) OK
  12. 127.0.0.1:6379> get age
  13. "18"
  • 使用watch监视key,此时在事务执行前key被改动,事务将取消不会执行所有命令。

我们现在一个redis客户端中执行watch命令。

  1. 127.0.0.1:6379> watch name
  2. OK

此时我们打开另一个redis客户端,修改key=name的值。

  1. 127.0.0.1:6379> set name kevin
  2. OK

我们再次回到第一个客户端,开始输入事务的命令块。

  1. 127.0.0.1:6379> multi
  2. OK
  3. 127.0.0.1:6379> set name abc
  4. QUEUED
  5. 127.0.0.1:6379> set age 1
  6. QUEUED
  7. 127.0.0.1:6379> exec
  8. (nil)

可看到通过exec执行事务时,事务并没有执行成功,而是返回“nil”。

Java中Jedis使用redis事务,则通过调用以下方法实现,具体命令可参照文档:

  1. @Test
  2. public void testTransaction() {
  3. Jedis jedis = RedisClient.getJedis();
  4. jedis.watch("a", "c");
  5. Transaction transaction = jedis.multi();
  6. transaction.set("a", "b");
  7. transaction.set("c", "d");
  8. transaction.exec();
  9. }

通过Lua脚本保证Redis的真事务

redis中自带的事务命令,最致命的前面已经多次提到,那就是不保证原子性,所以在使用redis的事务时,一定要谨慎。

但如果我们一定要在redis中实现真正的事务应该怎么办呢?redis为我们提供了另外一种更为“灵活”的方式——Lua脚本

在这里当然并不会详细讲解Lua的语法规则,我们一步步来看在redis中如何执行Lua脚本,以及Lua是如何运用在redis保证事务的。

我们先用Lua脚本在redis中实现调用字符串的set命令,我们先看示例:

  1. 127.0.0.1:6379> eval "return redis.call('set', KEYS[1], ARGV[1])" 1 company bat
  2. OK
  3. 127.0.0.1:6379> get company
  4. "bat"

eval是执行Lua脚本的命令,第二个参数是Lua脚本,第三个参数是一个数字表示一共有多少个key,第四个参数表示key值,第五个参数表示value值,eval [lua scripts] [numskey] [key1] [key2] [value1] [value2] ……

接下来,我们来一个Lua脚本,脚本中包含写入name的值和age的值。

  1. 127.0.0.1:6379> eval "redis.call('set', KEYS[1], ARGV[1]) redis.call('set', KEYS[2], ARGV[2])" 2 name age kevin 25
  2. (nil)
  3. 127.0.0.1:6379> get name
  4. "kevin"
  5. 127.0.0.1:6379> get age
  6. "25"

对于简单的Lua脚本通过命令行的方式直接编辑问题不大,但如果是比较复杂得Lua脚本,通常我们会单独写一个Lua脚本文件,然后载入它,例如以下示例:

  1. local exist = redis.call('exists', KEYS[1])
  2. if exist then
  3. return redis.call('incr', KEYS[1])
  4. else
  5. return nil
  6. end

我们将它保存为Lua脚本文件,执行以下命令:

  1. okevindeMacBook-Air:redis-5.0.7 okevin$ redis-cli --eval ~/Desktop/lua_test.lua view
  2. (nil)

可以看到key=view并不存在,所以返回nil,如果此时我们在redis中定义了一个key=view的值,此时将返回以下信息:

  1. okevindeMacBook-Air:redis-5.0.7 okevin$ redis-cli --eval ~/Desktop/lua_test.lua view
  2. (integer) 2

Jedis中如何载入Lua脚本

有关本节的源码:https://github.com/yu-linfeng/redis5.x_tutorial/tree/master/code/jedis

在Jedis可以直接调用Jedis类的eval方法,第一个参数是Lua脚本,第二个参数是key值,第三个参数是value值。

  1. public void testLua() {
  2. Jedis jedis = RedisClient.getJedis();
  3. List<String> keys = new ArrayList<>();
  4. keys.add("name");
  5. keys.add("age");
  6. List<String> values = new ArrayList<>();
  7. values.add("kevin");
  8. values.add("25");
  9. jedis.eval("redis.call('set', KEYS[1], ARGV[1]) redis.call('set', KEYS[2], ARGV[2])", keys, values);
  10. jedis.close();
  11. }

第七章 · 分布式锁

redis在我们日常开发中,除了用来做缓存提高应用程序的性能,降低数据库压力之外。可能用途最广泛地当属用redis来做分布式锁了。

在单机中,我们要解决并发时线程安全的问题会使用JDK的synchronized或者Lock类,或者直接使用线程安全的类,例如JUC(java.util.concurrent并发包)。而在大型的应用程序中,单机部署显然不能满足我们的需求,这个时候要在分布式集群环境中对互斥资源进行控制访问,就需要使用到分布式锁。

在本章中,我们着重介绍基于redis的分布式锁,同时将简单介绍其他分布式锁的解决方案。

开始之前先总结无论什么方式的分布式锁,其核心都是如有不存在某个key则写入,存在则返回写入失败

通过redis实现分布式锁

redis中主要通过setnx命令实现,全称是“SET if Not eXists”,意为如果存在则写入。如果不存在key则返回1,已经存在了这个key,则会返回0。释放锁时直接调用del命令删除即可。

  1. 127.0.0.1:6379> setnx redis_lock a
  2. (integer) 1
  3. 127.0.0.1:6379> setnx redis_lock a
  4. (integer) 0

但是请注意,使用setnx有一定的风险,我们知道加锁就有存在“死锁”的可能性,而打破死锁的方法之一就是主动释放资源(设置锁过期时间),然而setnx并没有提供过期时间的设置,redis提供了另外一个命令——expire来设置key值得过期时间,所以改造上面的例子为以下所示:

  1. 127.0.0.1:6379> setnx redis_lock a #设置一个分布式锁的key为redis_lock
  2. (integer) 1
  3. 127.0.0.1:6379> expire redis_lock 5 #设置redis_lock的过期时间为5秒,到期自动删除
  4. (integer) 1
  5. 127.0.0.1:6379> setnx redis_lock a #此时再设置分布式锁的key为redis_lock,返回0失败
  6. (integer) 0
  7. 127.0.0.1:6379> setnx redis_lock a #过5秒再设置分布式锁的key为redis_lock,返回1成功
  8. (integer) 1

可以看到通过组合setnxexpire命令,能达到我们想要的结果。但是请注意,它仍然存在一个问题,那就是这两个命令并不是原子性的,如果在执行expire redis_lock 5时,redis服务恰好宕机,此时这个key将会一直存在。

好在redis为我们提供了set命令的分布式用法并且可以设置为过期时间,关键是原子性的。官方的命令参数为set key value [expiration EX seconds|PX milliseconds] [NX|XX]

[expiration EX seconds|PX milliseconds]参数EX表示过期时间单位为“秒”,PX表示过期时间单位为“毫秒”。

[NX|XX]参数NX表示“SET if Not eXists”不存在则写入,XX表示“SET if eXists”存在则写入,分布式锁的场景中使用“NX”参数。

所以我们设置一个key值名为“lock”的锁,5秒后自动删除:

  1. 127.0.0.1:6379> set lock a ex 5 nx #设置一个key值名为“lock”的锁,5秒后自动删除
  2. OK
  3. 127.0.0.1:6379> set lock a ex 5 nx #5秒内设置一个key值名为“lock”的锁,5秒后自动删除。返回nil失败
  4. (nil)
  5. 127.0.0.1:6379> set lock a ex 5 nx #5秒后设置一个key值名为“lock”的锁,5秒后自动删除。返OK成功
  6. OK

使用redis作为分布式锁,最好要设置过期时间,也就是最好使用set命令。

其他分布式锁

通过ZooKeeper实现分布式锁

ZooKeeper是一个分布式协调服务中间件,它可以用作注册中心动态配置中心等等。

我们利用ZooKeeper的临时有序节点也可以实现分布式锁。

ZooKeeper的数据结构类似Linux中的文件结构,总体来讲它时“一棵树”,节点中记录相关信息。节点分为“永久节点”和“临时节点”。当我们要获取一个锁时,需要在ZooKeeper的结构中创建一个临时有序节点,释放锁同样时删除节点。获取分布式锁,即获取一个ZooKeeper的临时有序节点,如果获取到的有序节点存在比序号比自己更小的兄弟节点,即获取锁失败。

基于ZooKeeper实现分布式锁可以利用ZooKeeper监听的特性,一旦有节点发生变化可以进行通知。这点是Redis不具备的。但由于它的实现方式是创建和删除节点,所以在性能上不如redis。

通过MySQL实现分布式锁

通过MySQL实现分布式锁是我以前遇到的一个面试问题,思考以下实现方式:

在MySQL创建一个有关锁的表“tb_lock”,一共有两列,一列叫“key”并设置为唯一索引,另一列设置为“value”。

获取锁时,通过insert插入一条记录,如果插入成功则获取锁成功;插入失败则获取锁失败。

一听,是不是觉得有点意思,好像确实能通过MySQL来实现分布式锁,这样我们就不必引入redis或ZooKeeper。那为什么我们日常开发中几乎没有人这样用过呢?实际上,MySQL实现分布式锁,它仅仅满足了控制互斥资源这一点,尽管它是最核心的,但分布式锁不仅是控制互斥资源,它还需要具备以下特性:

  • 可设置过期时间,防止死锁
  • 需要具备阻塞获取锁的特性
  • 较高的性能和可靠性
  • 锁还需要可重入
  • ……

所以如果要使用MySQL来实现分布式锁,你需要去解决以上的问题,对于成熟的redis和ZooKeeper分布式锁方案,我们大可不必再造一个不可靠的轮子。

关注公众号:CoderBuff,回复“redis”获取《Redis5.x入门教程》完整版PDF。

这是一个能给程序员加buff的公众号 (CoderBuff)

Redis的“假事务”与分布式锁的更多相关文章

  1. 第四节:Geo类型介绍以及Redis批量操作、事务、分布式锁

    一. Geo类型 1. 类型说明 Geo 是 Redis 3.2 版本后新增的数据类型,用来保存兴趣点(POI,point of interest)的坐标信息.可以实现计算两 POI 之间的距离.获取 ...

  2. 基于redis集群实现的分布式锁,可用于秒杀商品的库存数量管理,有測试代码(何志雄)

    转载请标明出处. 在分布式系统中,常常会出现须要竞争同一资源的情况,本代码基于redis3.0.1+jedis2.7.1实现了分布式锁. redis集群的搭建,请见我的另外一篇文章:<>& ...

  3. python使用redis实现协同控制的分布式锁

    python使用redis实现协同控制的分布式锁 上午的时候,有个腾讯的朋友问我,关于用zookeeper分布式锁的设计,他的需求其实很简单,就是节点之间的协同合作. 我以前用redis写过一个网络锁 ...

  4. 使用数据库、Redis、ZK分别实现分布式锁!

    分布式锁三种实现方式: 基于数据库实现分布式锁: 基于缓存(Redis等)实现分布式锁: 基于Zookeeper实现分布式锁: 基于数据库实现分布式锁 悲观锁 利用select - where - f ...

  5. Redis中是如何实现分布式锁的?

    分布式锁常见的三种实现方式: 数据库乐观锁: 基于Redis的分布式锁: 基于ZooKeeper的分布式锁. 本地面试考点是,你对Redis使用熟悉吗?Redis中是如何实现分布式锁的. 要点 Red ...

  6. 【spring boot】【redis】spring boot基于redis的LUA脚本 实现分布式锁

    spring boot基于redis的LUA脚本 实现分布式锁[都是基于redis单点下] 一.spring boot 1.5.X 基于redis 的 lua脚本实现分布式锁 1.pom.xml &l ...

  7. Redis事务和分布式锁

    Redis事务 Redis中的事务(transaction)是一组命令的集合.事务同命令一样都是Redis最小的执行单位,一个事务中的命令要么都执行,要么都不执行.Redis事务的实现需要用到 MUL ...

  8. 分布式缓存技术redis学习系列(五)——redis实战(redis与spring整合,分布式锁实现)

    本文是redis学习系列的第五篇,点击下面链接可回看系列文章 <redis简介以及linux上的安装> <详细讲解redis数据结构(内存模型)以及常用命令> <redi ...

  9. 使用redis设计一个简单的分布式锁

    最近看了有关redis的一些东西,了解了redis的一下命令,就记录一下: redis中的setnx命令: 关于redis的操作命令,我们一般会使用set,get等一系列操作,数据结构也有很多,这里我 ...

随机推荐

  1. Java入门 - 高级教程 - 05.网络编程

    原文地址:http://www.work100.net/training/java-networking.html 更多教程:光束云 - 免费课程 网络编程 序号 文内章节 视频 1 概述 2 Soc ...

  2. 一行代码去掉Devexpress弹窗

    使用的是.net hook方法: 使用代码: using System; using System.Windows.Forms; namespace AlexDevexpressToolTest { ...

  3. 关于Hive中case when不准使用子查询的解决方法

    在公司用Hive实现个规则的时候,遇到了要查询某个字段是否在另一张表中,大概情况就是 A表: id value1 value2 1 100 0 2 101 1 3 102 1 B表: value1 1 ...

  4. Linux程序守护脚本

    不废话,直接上脚本,[]注释的下发语句需要按需替换: #!/usr/bin/env bash PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/us ...

  5. [计算几何+图论]doge

    题意 在平面直角坐标系上,你有一只doge在原点处.doge被绳子拴住了,绳子不会打结,没有弹性(但很柔软),并且长度为L.平面上有一些目标,因此你的doge会按照顺序去捡起它们,但是doge只能走直 ...

  6. JMeter入门 | 第一个并发测试

    JMeter入门 | 第一个并发测试 背景 近期我们组新来了一些新同事,之前从来没有用过JMeter做个并发测试,于是准备了一系列小教程去指引新同事,本章主要是新人入门体验教程,快速实现第一个接口并发 ...

  7. linux系统CentOS7中find命令使用

    一.作用 查找文件或目录 二.参数(常用) -atime 查找在指定时间曾被存取过的目录或文件,单位以24小时计算.(访问时间,执行文件等) -ctime 查找指定时间曾被更改的目录或文件,单位以24 ...

  8. Docker(二):理解容器编排工具Kubernetes内部工作原理

    一.Kubernetes是什么 要说到Docker就不得不说说Kubernetes.当Docker容器在微服务的环境下数量一多,那么统一的,自动化的管理自然少不了.而Kubernetes就是一个这样的 ...

  9. 学过 C++ 的你,不得不知的这 10 条细节!

    每日一句英语学习,每天进步一点点: “Action may not always bring happiness; but there is no happiness without action.” ...

  10. 批处理版MPlayer播放器(甲兵时代原创批处理)(下)

    注意,由于空间不支持显示退格键,需要自己手动补上,方法如上图: 接上篇: 批处理版音视频播放器上(甲兵时代原创批处理) :Bc cls COLOR 2F echo. call :colour &quo ...