本文主要介绍下Redis实现分布式锁的过程,
redis版本:redis 4.0,单实例,暂不考虑redis高可用
客户端:Spring-data-redis
分布式锁满足的条件
1.互斥性。在任意时刻,只有一个客户端能持有锁。 2.不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。 3.解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
1. 获取锁
唯一性:利用Redis中
SETNX key value
将key的值设为 value ,当且仅当 key 不存在; 若给定的 key 已经存在,则 SETNX 不做任何动作;
自动过期性:利用Redis中
SETEX key seconds value
将值 value 关联到 key ,并将 key 的生存时间设为 seconds (以秒为单位)。
以上两个命令必须进行原子操作,以防止设置完key以后没设置自动过期,从而可能导致锁无法被释放。
redisTemplate
没有直接提供同时操作者两个命令的接口,但通过RedisCallback
可以实现
具体代码如下:
/** * 获取锁 * * @param lockedKey * @param expire * @return */ public boolean getLock(String lockedKey, long expire) { lockedValue = UUID.randomUUID().toString() ; //获取锁 String exeResult = stringRedisTemplate.execute((RedisCallback<String>) connection -> { JedisCommands commands = (JedisCommands) connection.getNativeConnection(); /** * NX: 表示只有当锁定资源不存在的时候才能 SET 成功。利用 Redis 的原子性, * 保证了只有第一个请求的线程才能获得锁,而之后的所有线程在锁定资源被释放之前都不能获得锁。 * * PX: expire 表示锁定的资源的自动过期时间,单位是毫秒。具体过期时间根据实际场景而定 */ return commands.set(lockedKey, lockedValue, "NX", "PX", expire); }); //是否获取到锁 boolean result = LOCK_SUCCESS.equals(exeResult); return result; }
同时还提供带自动重试功能的方法来获取锁,当获取不到锁时,在一定时间内继续重试。
/** * 获取锁 * 如果获取不到,自动尝试多次,直到花费的时间超过tryTimeOut时间 * @param lockedKey * @param expire * @param tryTimeOut * @return */ public boolean getLock(String lockedKey, long expire, long tryTimeOut) { //单位都是毫秒 long startTime = System.currentTimeMillis() ; Random random = new Random(); while ((System.currentTimeMillis() - startTime) <= tryTimeOut){ if( getLock(lockedKey,expire) ){ return true ; } try { Thread.sleep(50, random.nextInt(100)); } catch (InterruptedException e) { e.printStackTrace(); } } return false ; }
2. 释放锁
注意以下情况:
1.线程A获取到锁以后,锁超时时间为1s,A在执行其他操作花费的时间超过1s时,锁因超时会被自动删除。 2.此时B线程尝试获取锁时,得到锁,并设置超时时间为1s;如果此时线程的A的操作完成,要进行释放锁 3.但是锁的持有者已经变成B了,所有要区分锁的持有者,否则就会导致线程A把线程B的锁释放掉了
为了解决以上问题,在释放锁的时候,一定要区分锁的持有者是否等于释放者。
为了保证多线程环境下的正确性,要保证 判断锁持有者操作和删除锁的操作是一个院子操作。
这里通过Lua脚本实现:
/** * 释放锁 * * @param lockedKey * @return */ public boolean releaseLock(String lockedKey) { if (lockedValue == null || lockedValue.length() == 0){ return false ; } // 使用Lua脚本删除Redis中匹配value的key,可以避免由于方法执行时间过长而redis锁自动过期失效的时候误删其他线程的锁 // 删除前要通过value来判断是否为自己的锁 String script = new StringBuffer() .append("if redis.call('get', KEYS[1]) == ARGV[1] then ") .append(" return redis.call('del', KEYS[1]) ") .append("else ") .append(" return 0 ") .append("end ") .toString(); DefaultRedisScript<Long> redisScript = new DefaultRedisScript<Long>(); redisScript.setResultType(Long.class); redisScript.setScriptText(script); //执行脚本 Long exeResult = stringRedisTemplate.execute(redisScript, Arrays.asList(lockedKey), lockedValue); boolean result = (RELEASE_SUCCESS == exeResult); return result; }
3. 完整源码
import org.springframework.data.redis.core.RedisCallback;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.data.redis.core.script.DefaultRedisScript;import redis.clients.jedis.JedisCommands;import java.util.Arrays;import java.util.Random;import java.util.UUID;/** * 通过redis实现分布式锁 * */public class RedisLock { private static final String LOCK_SUCCESS = "OK"; private static final Long RELEASE_SUCCESS = 1L; private String lockedValue ; /** * redis操作类 */ StringRedisTemplate stringRedisTemplate; public RedisLock(StringRedisTemplate stringRedisTemplate){ this.stringRedisTemplate = stringRedisTemplate ; } /** * 获取锁 * * @param lockedKey * @param expire * @return */ public boolean getLock(String lockedKey, long expire) { lockedValue = UUID.randomUUID().toString() ; //获取锁 String exeResult = stringRedisTemplate.execute((RedisCallback<String>) connection -> { JedisCommands commands = (JedisCommands) connection.getNativeConnection(); /** * NX: 表示只有当锁定资源不存在的时候才能 SET 成功。利用 Redis 的原子性, * 保证了只有第一个请求的线程才能获得锁,而之后的所有线程在锁定资源被释放之前都不能获得锁。 * * PX: expire 表示锁定的资源的自动过期时间,单位是毫秒。具体过期时间根据实际场景而定 */ return commands.set(lockedKey, lockedValue, "NX", "PX", expire); }); //是否获取到锁 boolean result = LOCK_SUCCESS.equals(exeResult); return result; } /** * 获取锁 * 如果获取不到,自动尝试多次,直到花费的时间超过tryTimeOut时间 * @param lockedKey * @param expire * @param tryTimeOut * @return */ public boolean getLock(String lockedKey, long expire, long tryTimeOut) { //单位都是毫秒 long startTime = System.currentTimeMillis() ; Random random = new Random(); while ((System.currentTimeMillis() - startTime) <= tryTimeOut){ if( getLock(lockedKey,expire) ){ return true ; } try { Thread.sleep(50, random.nextInt(100)); } catch (InterruptedException e) { e.printStackTrace(); } } return false ; } /** * 释放锁 * * @param lockedKey * @return */ public boolean releaseLock(String lockedKey) { if (lockedValue == null || lockedValue.length() == 0){ return false ; } // 使用Lua脚本删除Redis中匹配value的key,可以避免由于方法执行时间过长而redis锁自动过期失效的时候误删其他线程的锁 // 删除前要通过value来判断是否为自己的锁 String script = new StringBuffer() .append("if redis.call('get', KEYS[1]) == ARGV[1] then ") .append(" return redis.call('del', KEYS[1]) ") .append("else ") .append(" return 0 ") .append("end ") .toString(); DefaultRedisScript<Long> redisScript = new DefaultRedisScript<Long>(); redisScript.setResultType(Long.class); redisScript.setScriptText(script); //执行脚本 Long exeResult = stringRedisTemplate.execute(redisScript, Arrays.asList(lockedKey), lockedValue); boolean result = (RELEASE_SUCCESS == exeResult); return result; } }
作者:heichong
链接:https://www.jianshu.com/p/1cdc1da59436
点击查看更多内容
1人点赞
评论
共同学习,写下你的评论
评论加载中...
作者其他优质文章
正在加载中
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦