赞
踩
在进入正题之前要搞清楚两个问题:一是为什么需要分布式锁,二是Redis为什么能够实现分布式锁。
假设现在有一个应用部署在了三台机器上,应用的某个资源需要进行加锁控制,如果用关键字synchronized加锁能控制住么?显然是不行的,因为synchronized是线程锁,只能作用在当前的JVM里,获取的锁是各自JVM主内存上的锁资源。就好比一个房间有三个门,不惯是打开哪个门上的锁都能进入这个房间。此时就需要一个统一的入口来提供锁资源,这就是分布式锁。能作为锁还必须满足一个条件,就是操作必须是原子性的,锁本身要能保证线程安全。能实现分布式锁的有Redis、ZooKeeper、数据库等,本文只介绍Redis。
setNX:SET if Not eXists,命令在指定的 key 不存在时,为 key 设置指定的值。设置成功,返回 1;设置失败,返回 0 。
get:获取指定key的值
getSet:设置指定 key 的值,并返回 key 的旧值。
很多人可能用了setNX和expire命令,这种操作不是原子操作,如果在调用expire设置过期时间之前机器挂了,这种情况会导致锁没法释放,即死锁。
public boolean lock(String key) throws InterruptedException { //自旋超时时间 int timeout = 3000; while(timeout-- >= 0) { Jedis resource = jedisPool.getResource(); //过期时间 long expires = System.currentTimeMillis() + 1000L; try { Long result = resource.setnx(key, String.valueOf(expires)); //通过setnx加锁成功 if (result.equals(1L)) { return true; } //通过setnx加锁失败,使用get和getSet组合命令加锁 else { String currentExpires = resource.get(key); //判断锁是否过期,如果过期重新加锁 if (StringUtils.isNotBlank(currentExpires) && Long.parseLong(currentExpires) < System.currentTimeMillis()) { String oldExpires = resource.getSet(key, String.valueOf(expires)); //判断值是否已被修改 if (StringUtils.isNotBlank(oldExpires) && oldExpires.equals(currentExpires)) { return true; } } } } finally { if (resource != null) resource.close(); } Thread.sleep(100L); } return false; }
锁 使 用 完 之 后 , 记 得 在 f i n a l l y 里 将 锁 释 放 。 \color{red}{锁使用完之后,记得在finally里将锁释放。} 锁使用完之后,记得在finally里将锁释放。
先了解几个参数
XX:只有key存在时才设置
NX:与setNX类似,只有key不存在时才设置
PX:表示过期时间的单位为毫秒
EX:表示过期时间的单位为秒
public boolean lock(String key) throws InterruptedException { //自旋超时时间 int timeout = 3000; while(timeout-- >= 0) { Jedis resource = jedisPool.getResource(); SetParams params = new SetParams(); //设置为setnx模式 params.nx(); //设置过期时间,单位为毫秒 params.px(1000); try { String result = resource.set(key, key, params); //返回OK表示设置成功 if ("OK".equals(result)) { return true; } } finally { if (resource != null) resource.close(); } Thread.sleep(100L); } return false; }
用set命令,设置key和过期时间是一个命令,所以不会出现应用层导致的死锁问题。
锁
使
用
完
之
后
,
记
得
在
f
i
n
a
l
l
y
里
将
锁
释
放
。
\color{red}{锁使用完之后,记得在finally里将锁释放。}
锁使用完之后,记得在finally里将锁释放。
lua脚本可以将一组Redis命令放在一次请求里完成,Redis会将脚本作为一个整体执行,保证了原子性
public boolean lock(String key) throws InterruptedException { //自旋超时时间 int timeout = 3000; while(timeout-- >= 0) { Jedis resource = jedisPool.getResource(); try { //lua脚本 String script = "if redis.call('set', " + key + "," + key + ",'nx','px',time)=='OK' then\n" + "return 'OK'\n" + "else\n" + " if redis.call('get'," + key + ") == " + key + " then\n" + " if redis.call('EXPIRE'," + key + ", 1000)==1 then\n" + " return 'OK'\n" + " end\n" + " end\n" + "end"; Object result = resource.eval(script); if ("OK".equals(result)) { return true; } } finally { if (resource != null) resource.close(); } Thread.sleep(100L); } return false; }
锁 使 用 完 之 后 , 记 得 在 f i n a l l y 里 将 锁 释 放 。 \color{red}{锁使用完之后,记得在finally里将锁释放。} 锁使用完之后,记得在finally里将锁释放。
推荐使用Redisson,该组件提供了多种分布式锁实现机制,如可重入锁(Reentrant Lock)、公平锁(Fair Lock)、读写锁(ReadWriteLock)、信号量(Semaphore) 等。Redisson的加锁和释放锁都是使用的lua脚本来实现的。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。