一、什么是分布式锁?
要介绍分布式锁,首先要提到与分布式锁相对应的是线程锁、进程锁。
线程锁:主要用来给方法、代码块加锁。当某个方法或代码使用锁,在同一时刻仅有一个线程执行该方法或该代码段。线程锁只在同一JVM中有效果,因为线程锁的实现在根本上是依靠线程之间共享内存实现的,比如synchronized是共享对象头,显示锁Lock是共享某个变量(state)。
进程锁:为了控制同一操作系统中多个进程访问某个共享资源,因为进程具有独立性,各个进程无法访问其他进程的资源,因此无法通过synchronized等线程锁实现进程锁。
分布式锁:当多个进程不在同一个系统中,用分布式锁控制多个进程对资源的访问。
二、分布式锁的使用场景。
线程间并发问题和进程间并发问题都是可以通过分布式锁解决的,但是强烈不建议这样做!因为采用分布式锁解决这些小问题是非常消耗资源的!分布式锁应该用来解决分布式情况下的多进程并发问题才是最合适的。
有这样一个情境,线程A和线程B都共享某个变量X。
如果是单机情况下(单JVM),线程之间共享内存,只要使用线程锁就可以解决并发问题。
如果是分布式情况下(多JVM),线程A和线程B很可能不是在同一JVM中,这样线程锁就无法起到作用了,这时候就要用到分布式锁来解决。
三、分布式锁的实现(Redis)
分布式锁实现的关键是在分布式的应用服务器外,搭建一个存储服务器,存储锁信息,这时候我们很容易就想到了Redis。首先我们要搭建一个Redis服务器,用Redis服务器来存储锁信息。
在实现的时候要注意的几个关键点:
1、锁信息必须是会过期超时的,不能让一个线程长期占有一个锁而导致死锁;
2、同一时刻只能有一个线程获取到锁。
几个要用到的redis命令:
setnx(key, value):“set if not exits”,若该key-value不存在,则成功加入缓存并且返回1,否则返回0。
get(key):获得key对应的value值,若不存在则返回nil。
getset(key, value):先获取key对应的value值,若不存在则返回nil,然后将旧的value更新为新的value。
expire(key, seconds):设置key-value的有效期为seconds秒。
看一下流程图:
在这个流程下,不会导致死锁。
我采用Jedis作为Redis客户端的api,下面来看一下具体实现的代码。
(1)首先要创建一个Redis连接池。
- public class RedisPool {
- private static JedisPool pool;//jedis连接池
- private static int maxTotal = 20;//最大连接数
- private static int maxIdle = 10;//最大空闲连接数
- private static int minIdle = 5;//最小空闲连接数
- private static boolean testOnBorrow = true;//在取连接时测试连接的可用性
- private static boolean testOnReturn = false;//再还连接时不测试连接的可用性
- static {
- initPool();//初始化连接池
- }
- public static Jedis getJedis(){
- return pool.getResource();
- }
- public static void close(Jedis jedis){
- jedis.close();
- }
- private static void initPool(){
- JedisPoolConfig config = new JedisPoolConfig();
- config.setMaxTotal(maxTotal);
- config.setMaxIdle(maxIdle);
- config.setMinIdle(minIdle);
- config.setTestOnBorrow(testOnBorrow);
- config.setTestOnReturn(testOnReturn);
- config.setBlockWhenExhausted(true);
- pool = new JedisPool(config, "127.0.0.1", 6379, 5000, "liqiyao");
- }
- }
(2)对Jedis的api进行封装,封装一些实现分布式锁需要用到的操作。
- public class RedisPoolUtil {
- private RedisPoolUtil(){}
- private static RedisPool redisPool;
- public static String get(String key){
- Jedis jedis = null;
- String result = null;
- try {
- jedis = RedisPool.getJedis();
- result = jedis.get(key);
- } catch (Exception e){
- e.printStackTrace();
- } finally {
- if (jedis != null) {
- jedis.close();
- }
- return result;
- }
- }
- public static Long setnx(String key, String value){
- Jedis jedis = null;
- Long result = null;
- try {
- jedis = RedisPool.getJedis();
- result = jedis.setnx(key, value);
- } catch (Exception e){
- e.printStackTrace();
- } finally {
- if (jedis != null) {
- jedis.close();
- }
- return result;
- }
- }
- public static String getSet(String key, String value){
- Jedis jedis = null;
- String result = null;
- try {
- jedis = RedisPool.getJedis();
- result = jedis.getSet(key, value);
- } catch (Exception e){
- e.printStackTrace();
- } finally {
- if (jedis != null) {
- jedis.close();
- }
- return result;
- }
- }
- public static Long expire(String key, int seconds){
- Jedis jedis = null;
- Long result = null;
- try {
- jedis = RedisPool.getJedis();
- result = jedis.expire(key, seconds);
- } catch (Exception e){
- e.printStackTrace();
- } finally {
- if (jedis != null) {
- jedis.close();
- }
- return result;
- }
- }
- public static Long del(String key){
- Jedis jedis = null;
- Long result = null;
- try {
- jedis = RedisPool.getJedis();
- result = jedis.del(key);
- } catch (Exception e){
- e.printStackTrace();
- } finally {
- if (jedis != null) {
- jedis.close();
- }
- return result;
- }
- }
- }
(3)分布式锁工具类
- public class DistributedLockUtil {
- private DistributedLockUtil(){
- }
- public static boolean lock(String lockName){//lockName可以为共享变量名,也可以为方法名,主要是用于模拟锁信息
- System.out.println(Thread.currentThread() + "开始尝试加锁!");
- Long result = RedisPoolUtil.setnx(lockName, String.valueOf(System.currentTimeMillis() + 5000));
- if (result != null && result.intValue() == 1){
- System.out.println(Thread.currentThread() + "加锁成功!");
- RedisPoolUtil.expire(lockName, 5);
- System.out.println(Thread.currentThread() + "执行业务逻辑!");
- RedisPoolUtil.del(lockName);
- return true;
- } else {
- String lockValueA = RedisPoolUtil.get(lockName);
- if (lockValueA != null && Long.parseLong(lockValueA) >= System.currentTimeMillis()){
- String lockValueB = RedisPoolUtil.getSet(lockName, String.valueOf(System.currentTimeMillis() + 5000));
- if (lockValueB == null || lockValueB.equals(lockValueA)){
- System.out.println(Thread.currentThread() + "加锁成功!");
- RedisPoolUtil.expire(lockName, 5);
- System.out.println(Thread.currentThread() + "执行业务逻辑!");
- RedisPoolUtil.del(lockName);
- return true;
- } else {
- return false;
- }
- } else {
- return false;
- }
- }
- }
- }
@Component @Slf4j public class DistributedLockService { /** * 获取redis分布式锁 ,catchKey有效期默认为{expireMs} * @param cacheKey 分布式锁key * @return 如果锁成功返回 返回key对应的value,否则返回null */ public String tryLock(String cacheKey) { return this.tryLock(cacheKey, expireMs); } /** * 获取cacheKey的redis分布式锁 ,catchKey有效期设置为expireMilliS * @param cacheKey 分布式锁key * @param expireMs 分布式锁 有效期 * @return 如果锁成功返回 返回key对应的value,否则返回null */ public String tryLock(String cacheKey,long expireMs) { String lockKey = lockKeyPrefix + cacheKey; String lockValue = UUID.randomUUID().toString(); // 生成随机value boolean locked = false; try { // 1、尝试获取并发锁 locked = this.setIfNotExist(lockKey, lockValue, expireMs); // 2、 若当前不能获取锁资源,阻塞,定时重试 if (!locked) { int times = 0; while (times++ < lockRetryTimes && !locked && lockRetrySpan > 0) { log.warn("获取key[{}]更新锁失败,第[{}]次重试加锁", cacheKey, times); Thread.sleep(lockRetrySpan); locked = this.setIfNotExist(lockKey, lockValue, expireMs); } } } catch (JedisException e) { log.error("--获取key[{}]更新锁异常,redis降级处理--", cacheKey, e); } catch (Exception e) { log.warn("获取key[{}]更新锁异常", cacheKey, e); } return locked ? lockValue : null; } /** * 解除key为value的锁 * * @param key * @param value * @return */ public Boolean unlock(String key,String value) { String lockKey = lockKeyPrefix + key; return (Boolean)redisTemplate.execute((RedisCallback<Boolean>) connection -> { Object nativeConnection = connection.getNativeConnection(); Long result = 0L; List<String> keys = new ArrayList<>(); keys.add(lockKey); List<String> values = new ArrayList<>(); values.add(value); // 集群模式 if (nativeConnection instanceof JedisCluster) { result = (Long) ((JedisCluster) nativeConnection).eval(UNLOCK_LUA, keys, values); } // 单机模式 if (nativeConnection instanceof Jedis) { result = (Long) ((Jedis) nativeConnection).eval(UNLOCK_LUA, keys, values); } boolean locked = (result == 1); if (!locked) { log.warn("Redis分布式锁,解锁{}失败!", lockKey); } return locked; }); } /** * <p> * 命令 SET resource-name anystring NX PX max-lock-time 是一种在 Redis 中实现锁的简单方法。 * <p> * 客户端执行以上的命令: * <p> * 如果服务器返回 OK ,那么这个客户端获得锁。 * 如果服务器返回 NIL ,那么客户端获取锁失败,可以在稍后再重试。 *h * @param key 锁的Key * @param value 锁的值 * @param expireMs 失效时间(毫秒) * @return */ private Boolean setIfNotExist(final String key, final String value, final long expireMs) { long now = System.currentTimeMillis(); return (Boolean) redisTemplate.execute((RedisCallback<Boolean>) connection -> { Object nativeConnection = connection.getNativeConnection(); String result = null; if (nativeConnection instanceof JedisCommands) { result = ((JedisCommands) nativeConnection).set(key, value, NX, PX, expireMs); } log.debug("获取锁 key:{},result:{},耗时:{}", key, result, System.currentTimeMillis() - now); return OK.equalsIgnoreCase(result); }); } /** * 解锁的lua脚本 */ public static final String UNLOCK_LUA; static { StringBuilder sb = new StringBuilder(); sb.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] "); sb.append("then "); sb.append(" return redis.call(\"del\",KEYS[1]) "); sb.append("else "); sb.append(" return 0 "); sb.append("end "); UNLOCK_LUA = sb.toString(); } private final static String lockKeyPrefix = "ship_distLock:";//锁前缀 /** * 将key 的值设为value ,当且仅当key不存在,等效于 SETNX */ public static final String NX = "NX"; /** * 以毫秒为单位设置 key 的过期时间 * PX = milliseconds */ public static final String PX = "PX"; /** * 分布式锁超时时间,单位:毫秒 */ @Value("${distributedLock.expire:1000}") private long expireMs; /** * 分布式锁重试次数 */ @Value("${distributedLock.retry.times:3}") private int lockRetryTimes; /** * 分布式锁重试时间间隔 毫秒 */ @Value("${distributedLock.retry.span:100}") private int lockRetrySpan; /** * 调用set后的返回值 */ public static final String OK = "OK"; @Resource private RedisTemplate redisTemplate; }