目前几种分布式锁的实现方式:
- 数据库实现(不适合数据量比较大的互联网公司)、
- 基于ZK的实现(1、Zk的节点改变时候的watcher事件通知。2、节点类型中的有序节点可实现先到先得公平策略)
- 基于Redis的实现(setNX + 有效期,实现相对ZK简单一些)
工作中经常用到Redis,所以决定采用redis实现分布式锁, 首先先要明确目标,目标明确了才有技术方案
分布式锁实现的目标:
- 高性能(加锁和解锁性能高)
- 互斥访问(一个线程持有锁,另一个线程不能持有)
- 不能产生死锁(例如redis客户端挂了,或者设置过期时间时候挂了导致key永久有效)
- 解锁(只能解除自己的锁)
具体实现:
1 package com.brightcns.wuxi.citizencard.common.feature.util; 2 3 import javafx.beans.binding.ObjectExpression; 4 import lombok.extern.slf4j.Slf4j; 5 import org.springframework.data.redis.connection.RedisConnection; 6 import org.springframework.data.redis.connection.ReturnType; 7 import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; 8 import redis.clients.jedis.JedisPoolConfig; 9 import redis.clients.util.SafeEncoder; 10 11 12 /** 13 * 分布式锁的常用类 14 * @author maxianming 15 * @date 2018/8/13 15:50 16 */ 17 @Slf4j 18 public class LockUtils { 19 20 private JedisConnectionFactory jedisConnectionFactory; 21 private static final Long RELEASE_SUCCESS = 1L; 22 private static final String LOCK_SUCCESS = "OK"; 23 private static final String SET_IF_NOT_EXIST = "NX"; 24 private static final String SET_WITH_EXPIRE_TIME = "PX"; 25 26 public LockUtils(JedisConnectionFactory jedisConnectionFactory) { 27 if (jedisConnectionFactory == null) { 28 throw new IllegalArgumentException("jedisConnectionFactory not be allowed null"); 29 } 30 this.jedisConnectionFactory = jedisConnectionFactory; 31 } 32 /** 33 * 尝试获取分布式锁 34 * @param lockKey 锁 35 * @param requestId 请求标识 36 * @param expireTime 超期时间 ms 37 * @return 是否获取成功 38 */ 39 public void lock(String lockKey, String requestId, int expireTime) throws InterruptedException { 40 RedisConnection redisConnection = getRedisConnection(); 41 try { 42 while (true) { 43 Object result = redisConnection.execute("SET", new byte[][]{ 44 SafeEncoder.encode(lockKey), SafeEncoder.encode(requestId), SafeEncoder.encode(SET_IF_NOT_EXIST), 45 SafeEncoder.encode(SET_WITH_EXPIRE_TIME), SafeEncoder.encode(String.valueOf(expireTime))}); 46 if (result != null) { 47 if (LOCK_SUCCESS.equals(new String((byte[])(result)))) { 48 return; 49 } 50 } 51 try { 52 Thread.sleep(expireTime); 53 } catch (InterruptedException e) { 54 log.error("中断异常", e); 55 throw e; 56 } 57 } 58 } finally { 59 redisConnection.close(); 60 } 61 } 62 63 64 /** 65 * 释放分布式锁 66 * @param lockKey 锁 67 * @param requestId 请求标识 68 * @return 是否释放成功 69 */ 70 public boolean unLock(String lockKey, String requestId) { 71 RedisConnection redisConnection = getRedisConnection(); 72 try { 73 String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; 74 Object result = redisConnection.eval(SafeEncoder.encode(script), ReturnType.INTEGER, 1, getByteParams(new String[]{lockKey, requestId})); 75 if (RELEASE_SUCCESS.equals(result)) { 76 return true; 77 } 78 return false; 79 } finally { 80 redisConnection.close(); 81 } 82 83 } 84 85 private byte[][] getByteParams(String... params) { 86 byte[][] p = new byte[params.length][]; 87 for (int i = 0; i < params.length; i++) 88 p[i] = SafeEncoder.encode(params[i]); 89 90 return p; 91 } 92 93 private RedisConnection getRedisConnection() { 94 RedisConnection redisConnection = jedisConnectionFactory.getConnection(); 95 return redisConnection; 96 } 97 98 public static void main(String[] args) { 99 JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory();100 jedisConnectionFactory.setPort(6379);101 jedisConnectionFactory.setHostName("*");102 jedisConnectionFactory.setPassword("*");103 104 JedisPoolConfig poolConfig = new JedisPoolConfig();105 poolConfig.setMaxTotal(40);106 poolConfig.setMaxIdle(10);107 poolConfig.setMinIdle(5);108 jedisConnectionFactory.setPoolConfig(poolConfig);109 jedisConnectionFactory.afterPropertiesSet();110 LockUtils lockUtils =new LockUtils(jedisConnectionFactory);111 Thread thread = new Thread(() -> {112 try {113 lockUtils.lock("lock", "123", 20000);114 } catch (InterruptedException e) {115 e.printStackTrace();116 }117 System.out.println("加锁结果:" + "成功");118 119 boolean result = lockUtils.unLock("lock", "123");120 121 System.out.println("解锁结果:" + result);122 },"jedis-thread");123 thread.start();128 }129 130 }
(1)加锁
jedis提供了set方法将setNX和expire整合在一起的原子方法,这就避免了expire之前redis挂了导致的key永久有效,从而死锁。
实际项目中使用的Spring boot整合的redis,所以直接获取jedis客户端不太方便,查看源码发现 存在execute支持redis命令。
requestId的作用,解锁的时候传入加锁时候的相同值,避免错误解除别的线程的锁
expireTime的作用,redis key的有效期,根据实际时间设置
(2)解锁
采用lua脚本,主要保证一、解锁操作的原子性 二、解的是自己的锁