分布式锁一般有三种实现方式:
本篇博客将介绍第二种方式,基于Redis实现分布式锁。
虽然网上已经有各种介绍Redis分布式锁实现的博客,然而他们的实现却有着各种各样的问题,为了避免误人子弟,本篇博客将详细介绍如何正确地实现Redis分布式锁。
首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:
互斥性:在任意时刻,只有一个客户端能持有锁。
不会发生死锁:即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
具有容错性:只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
解铃还须系铃人:加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.9.0</version> </dependency> 备注:根据版本不同,jedis 的set 方法也有所不同
# redis.properties 配置文件: # region Redis jedis # redis配置开始 # Redis数据库索引(默认为0) spring.redis.database=0 # Redis服务器地址 spring.redis.host=localhost # Redis服务器连接端口 spring.redis.port=6379 # Redis服务器连接密码(默认为空) spring.redis.password=redis123456 # 连接池最大连接数(使用负值表示没有限制) spring.redis.jedis.pool.max-active=1024 # 连接池最大阻塞等待时间(使用负值表示没有限制) spring.redis.jedis.pool.max-wait=10000 # 连接池中的最大空闲连接 spring.redis.jedis.pool.max-idle=100 # 连接池中的最小空闲连接 spring.redis.jedis.pool.min-idle=5 # 连接超时时间(毫秒) spring.redis.timeout=10000 # 连接耗尽时是否阻塞, false报异常,ture阻塞直到超时 spring.redis.block-when-exhausted=true # endregion
@Configuration @PropertySource("classpath:application.properties") @Slf4j public class RedisConfig { @Value("${spring.redis.host}") private String host; @Value("${spring.redis.port}") private int port; @Value("${spring.redis.timeout}") private int timeout; @Value("${spring.redis.jedis.pool.max-idle}") private int maxIdle; @Value("${spring.redis.jedis.pool.max-wait}") private long maxWaitMillis; @Value("${spring.redis.password}") private String password; @Value("${spring.redis.block-when-exhausted}") private boolean blockWhenExhausted; @Bean public JedisPool redisPoolFactory() throws Exception { log.info("JedisPool注入开始!!"); log.info("redis地址:" + host + ":" + port); JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); jedisPoolConfig.setMaxIdle(maxIdle); jedisPoolConfig.setMaxWaitMillis(maxWaitMillis); // 连接耗尽时是否阻塞, false报异常,ture阻塞直到超时, 默认true jedisPoolConfig.setBlockWhenExhausted(blockWhenExhausted); // 是否启用pool的jmx管理功能, 默认true jedisPoolConfig.setJmxEnabled(true); JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout, password); log.info("JedisPool注入成功!!"); return jedisPool; } }
@Service @Slf4j public class RedisService { //Redis 成功返回结果标识 private static final String LOCK_SUCCESS = "OK"; //Reis 操作返回成功 private static final Long RELEASE_SUCCESS = 1L; //Redis锁不存在时才设置成功 private static final String SET_IF_NOT_EXIST = "NX"; //Redis锁超时时间单位 private static final String SET_WITH_EXPIRE_TIME = "PX"; @Autowired private JedisPool jedisPool; /** * 尝试获取分布式锁 * * @param lockKey 锁 * @param requestId 请求唯一标识(可以通过uuid等方式获取唯一ID) * @param expireTime 超期时间(毫秒) * @return 是否获取成功 */ public boolean tryGetDistributedLock(String lockKey, String requestId, int expireTime) { Jedis jedis = jedisPool.getResource(); //Jedis 3.0.0 以前的版本 //String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime); //Jedis 3.0.0 及以后的版本 SetParams setParams = SetParams.setParams().nx().px(expireTime); String result = jedis.set(lockKey, requestId, setParams); return LOCK_SUCCESS.equals(result); } /** * 释放分布式锁 * * @param lockKey 锁 * @param requestId 请求唯一标识(加锁时用的唯一标识) * @return 是否释放成功 */ public boolean releaseDistributedLock(String lockKey, String requestId) { Jedis jedis = jedisPool.getResource(); String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId)); return RELEASE_SUCCESS.equals(result); } }
1. 利用redis 的set 命令的 5个参数保证操作的原子性 2. 利用Lua 脚本保证在释放锁时的原子性 3. 利用requestId 唯一标识保证不会释放别人的锁