关注公众号:CoderBuff,回复“redis”获取《Redis5.x入门教程》完整版PDF。html
咱们在学习MySQL的存储殷勤时知道,MySQL中innodb支持事务而myisam不支持事务。而事务具备四个特性:java
在redis尽管提供了事务相关的命令,但实际上它是一个“假事务”,由于它并不支持回滚,也就是说在redis中一个事务有多个命令执行,并不能保证原子性。因此要使用redis的事务,必定要慎重。mysql
在redis中事务相关的命令一共有如下几个:git
watch [key1] [key2]
:监视一个或多个key,在事务开始以前若是被监视的key有改动,则事务被打断。程序员
multi
:标记一个事务的开始。github
exec
:执行事务。面试
discard
:取消事务的执行。redis
unwatch
:取消监视的key。sql
127.0.0.1:6379> multi OK 127.0.0.1:6379> set name kevin QUEUED 127.0.0.1:6379> set age 25 QUEUED 127.0.0.1:6379> get name QUEUED 127.0.0.1:6379> set sex male QUEUED 127.0.0.1:6379> exec 1) OK 2) OK 3) "kevin" 4) OK
取消事务执行,命令将不会被执行。数据库
127.0.0.1:6379> multi OK 127.0.0.1:6379> set name yulinfeng QUEUED 127.0.0.1:6379> set age 26 QUEUED 127.0.0.1:6379> discard OK 127.0.0.1:6379> get name "kevin"
127.0.0.1:6379> multi OK 127.0.0.1:6379> set name yulinfeng QUEUED 127.0.0.1:6379> setget age 26 (error) ERR unknown command `setget`, with args beginning with: `age`, `26`, 127.0.0.1:6379> exec (error) EXECABORT Transaction discarded because of previous errors. 127.0.0.1:6379> get name "kevin"
127.0.0.1:6379> multi OK 127.0.0.1:6379> incr name QUEUED 127.0.0.1:6379> set age 26 QUEUED 127.0.0.1:6379> exec 1) (error) ERR value is not an integer or out of range 2) OK 127.0.0.1:6379> get age "26"
watch
监视key在事务以前被改动,正常未被改动时的状况,全部命令正常执行。127.0.0.1:6379> watch name OK 127.0.0.1:6379> multi OK 127.0.0.1:6379> set name yulinfeng QUEUED 127.0.0.1:6379> set age 18 QUEUED 127.0.0.1:6379> exec 1) OK 2) OK 127.0.0.1:6379> get age "18"
watch
监视key,此时在事务执行前key被改动,事务将取消不会执行全部命令。咱们如今一个redis客户端中执行watch命令。
127.0.0.1:6379> watch name OK
此时咱们打开另外一个redis客户端,修改key=name的值。
127.0.0.1:6379> set name kevin OK
咱们再次回到第一个客户端,开始输入事务的命令块。
127.0.0.1:6379> multi OK 127.0.0.1:6379> set name abc QUEUED 127.0.0.1:6379> set age 1 QUEUED 127.0.0.1:6379> exec (nil)
可看到经过exec
执行事务时,事务并无执行成功,而是返回“nil”。
Java中Jedis使用redis事务,则经过调用如下方法实现,具体命令可参照文档:
@Test public void testTransaction() { Jedis jedis = RedisClient.getJedis(); jedis.watch("a", "c"); Transaction transaction = jedis.multi(); transaction.set("a", "b"); transaction.set("c", "d"); transaction.exec(); }
redis中自带的事务命令,最致命的前面已经屡次提到,那就是不保证原子性,因此在使用redis的事务时,必定要谨慎。
但若是咱们必定要在redis中实现真正的事务应该怎么办呢?redis为咱们提供了另一种更为“灵活”的方式——Lua脚本。
在这里固然并不会详细讲解Lua的语法规则,咱们一步步来看在redis中如何执行Lua脚本,以及Lua是如何运用在redis保证事务的。
咱们先用Lua脚本在redis中实现调用字符串的set
命令,咱们先看示例:
127.0.0.1:6379> eval "return redis.call('set', KEYS[1], ARGV[1])" 1 company bat OK 127.0.0.1:6379> get company "bat"
eval
是执行Lua脚本的命令,第二个参数是Lua脚本,第三个参数是一个数字表示一共有多少个key,第四个参数表示key值,第五个参数表示value值,eval [lua scripts] [numskey] [key1] [key2] [value1] [value2] ……
。
接下来,咱们来一个Lua脚本,脚本中包含写入name的值和age的值。
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 (nil) 127.0.0.1:6379> get name "kevin" 127.0.0.1:6379> get age "25"
对于简单的Lua脚本经过命令行的方式直接编辑问题不大,但若是是比较复杂得Lua脚本,一般咱们会单独写一个Lua脚本文件,而后载入它,例如如下示例:
local exist = redis.call('exists', KEYS[1]) if exist then return redis.call('incr', KEYS[1]) else return nil end
咱们将它保存为Lua脚本文件,执行如下命令:
okevindeMacBook-Air:redis-5.0.7 okevin$ redis-cli --eval ~/Desktop/lua_test.lua view (nil)
能够看到key=view并不存在,因此返回nil,若是此时咱们在redis中定义了一个key=view的值,此时将返回如下信息:
okevindeMacBook-Air:redis-5.0.7 okevin$ redis-cli --eval ~/Desktop/lua_test.lua view (integer) 2
有关本节的源码:https://github.com/yu-linfeng/redis5.x_tutorial/tree/master/code/jedis
在Jedis能够直接调用Jedis
类的eval
方法,第一个参数是Lua脚本,第二个参数是key值,第三个参数是value值。
public void testLua() { Jedis jedis = RedisClient.getJedis(); List<String> keys = new ArrayList<>(); keys.add("name"); keys.add("age"); List<String> values = new ArrayList<>(); values.add("kevin"); values.add("25"); jedis.eval("redis.call('set', KEYS[1], ARGV[1]) redis.call('set', KEYS[2], ARGV[2])", keys, values); jedis.close(); }
redis在咱们平常开发中,除了用来作缓存提升应用程序的性能,下降数据库压力以外。可能用途最普遍地当属用redis来作分布式锁了。
在单机中,咱们要解决并发时线程安全的问题会使用JDK的synchronized
或者Lock
类,或者直接使用线程安全的类,例如JUC(java.util.concurrent并发包)。而在大型的应用程序中,单机部署显然不能知足咱们的需求,这个时候要在分布式集群环境中对互斥资源进行控制访问,就须要使用到分布式锁。
在本章中,咱们着重介绍基于redis的分布式锁,同时将简单介绍其余分布式锁的解决方案。
开始以前先总结不管什么方式的分布式锁,其核心都是若有不存在某个key则写入,存在则返回写入失败。
redis中主要经过setnx
命令实现,全称是“SET if Not eXists”,意为若是存在则写入。若是不存在key则返回1,已经存在了这个key,则会返回0。释放锁时直接调用del
命令删除便可。
127.0.0.1:6379> setnx redis_lock a (integer) 1 127.0.0.1:6379> setnx redis_lock a (integer) 0
可是请注意,使用setnx
有必定的风险,咱们知道加锁就有存在“死锁”的可能性,而打破死锁的方法之一就是主动释放资源(设置锁过时时间),然而setnx
并无提供过时时间的设置,redis提供了另一个命令——expire
来设置key值得过时时间,因此改造上面的例子为如下所示:
127.0.0.1:6379> setnx redis_lock a #设置一个分布式锁的key为redis_lock (integer) 1 127.0.0.1:6379> expire redis_lock 5 #设置redis_lock的过时时间为5秒,到期自动删除 (integer) 1 127.0.0.1:6379> setnx redis_lock a #此时再设置分布式锁的key为redis_lock,返回0失败 (integer) 0 127.0.0.1:6379> setnx redis_lock a #过5秒再设置分布式锁的key为redis_lock,返回1成功 (integer) 1
能够看到经过组合setnx
和expire
命令,能达到咱们想要的结果。可是请注意,它仍然存在一个问题,那就是这两个命令并非原子性的,若是在执行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秒后自动删除:
127.0.0.1:6379> set lock a ex 5 nx #设置一个key值名为“lock”的锁,5秒后自动删除 OK 127.0.0.1:6379> set lock a ex 5 nx #5秒内设置一个key值名为“lock”的锁,5秒后自动删除。返回nil失败 (nil) 127.0.0.1:6379> set lock a ex 5 nx #5秒后设置一个key值名为“lock”的锁,5秒后自动删除。返OK成功 OK
使用redis做为分布式锁,最好要设置过时时间,也就是最好使用set命令。
ZooKeeper是一个分布式协调服务中间件,它能够用做注册中心、动态配置中心等等。
咱们利用ZooKeeper的临时有序节点也能够实现分布式锁。
ZooKeeper的数据结构相似Linux中的文件结构,整体来说它时“一棵树”,节点中记录相关信息。节点分为“永久节点”和“临时节点”。当咱们要获取一个锁时,须要在ZooKeeper的结构中建立一个临时有序节点,释放锁一样时删除节点。获取分布式锁,即获取一个ZooKeeper的临时有序节点,若是获取到的有序节点存在比序号比本身更小的兄弟节点,即获取锁失败。
基于ZooKeeper实现分布式锁能够利用ZooKeeper监听的特性,一旦有节点发生变化能够进行通知。这点是Redis不具有的。但因为它的实现方式是建立和删除节点,因此在性能上不如redis。
经过MySQL实现分布式锁是我之前遇到的一个面试问题,思考如下实现方式:
在MySQL建立一个有关锁的表“tb_lock”,一共有两列,一列叫“key”并设置为惟一索引,另外一列设置为“value”。
获取锁时,经过
insert
插入一条记录,若是插入成功则获取锁成功;插入失败则获取锁失败。
一听,是否是以为有点意思,好像确实能经过MySQL来实现分布式锁,这样咱们就没必要引入redis或ZooKeeper。那为何咱们平常开发中几乎没有人这样用过呢?实际上,MySQL实现分布式锁,它仅仅知足了控制互斥资源这一点,尽管它是最核心的,但分布式锁不只是控制互斥资源,它还须要具有如下特性:
因此若是要使用MySQL来实现分布式锁,你须要去解决以上的问题,对于成熟的redis和ZooKeeper分布式锁方案,咱们大可没必要再造一个不可靠的轮子。
关注公众号:CoderBuff,回复“redis”获取《Redis5.x入门教程》完整版PDF。