正文
一、Redis分布式锁原理
分布式锁需要满足以下几点要求
在分布式系统环境下,一个方法在同一时间只能被一个机器的的一个线程执行 即独享(相互排斥);
高可用的获取锁与释放锁 ;
高性能的获取锁与释放锁;
具备可重入特性;
具备锁失效机制,防止死锁;
具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。
目前市面上普遍使用的分布式锁实现的方式主要有三种一种是基于数据库,一种是基于Zookeeper(点击链接可查看基于Zk实现的分布式锁),还有一种就是现在说的基于Redis实现的分布式锁。本文只说单机状态下Redis的分布式锁。如果在主从复制或者集群模式下(如果你能允许下面这些小概率的事件发生,同样也可以用在集群或者主从复制的情况下),如果主节点数据还没有同步到从节点的时候,突然宕机了,然后从节点变为主节点,获取到了锁,就违背了分布式锁的相互排斥原则。
那么Redis实现分布式锁的原理是什么?
Redis实现分布式锁基于SetNx命令,因为在Redis中key是保证是唯一的。所以当多个线程同时的创建setNx时,只要谁能够创建成功谁就能够获取到锁。SetNx命令 每次SetNx检查该 key是否已经存在,如果已经存在的话不会执行任何操作,返回为0。 如果不存在的话直接新增该key,新增成功返回1。
二、Redis实现分布式锁
基于Jedis实现
工具类
package com.xiaojie.utils; import org.springframework.stereotype.Component; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; /** * @ClassRedisPoolUtil * @Description jedispool连接工具 * @AuthorAdministrator * @Date {Date}{Time} * @Version 1.0 **/ @Component public class RedisPoolUtil { private static String IP = "192.168.139.154"; //Redis的端口号 private static int PORT = 6379; //可用连接实例的最大数目,默认值为8; //如果赋值为-1,则表示不限制;如果pool已经分配了maxActive个jedis实例,则此时pool的状态为exhausted(耗尽)。 private static int MAX_ACTIVE = 100; //控制一个pool最多有多少个状态为idle(空闲的)的jedis实例,默认值也是8。 private static int MAX_IDLE = 20; //等待可用连接的最大时间,单位毫秒,默认值为-1,表示永不超时。如果超过等待时间,则直接抛出JedisConnectionException; private static int MAX_WAIT = 3000; private static int TIMEOUT = 3000; //在borrow一个jedis实例时,是否提前进行validate操作;如果为true,则得到的jedis实例均是可用的; private static boolean TEST_ON_BORROW = true; //在return给pool时,是否提前进行validate操作; private static boolean TEST_ON_RETURN = true; private static JedisPool jedisPool = null; /** * redis过期时间,以秒为单位 */ public final static int EXRP_HOUR = 60 * 60; //一小时 public final static int EXRP_DAY = 60 * 60 * 24; //一天 public final static int EXRP_MONTH = 60 * 60 * 24 * 30; //一个月 /** * 初始化Redis连接池 */ private static void initialPool() { try { JedisPoolConfig config = new JedisPoolConfig(); config.setMaxTotal(MAX_ACTIVE); config.setMaxIdle(MAX_IDLE); config.setMaxWaitMillis(MAX_WAIT); config.setTestOnBorrow(TEST_ON_BORROW); jedisPool = new JedisPool(config, IP, PORT, TIMEOUT, "xiaojie"); } catch (Exception e) { //logger.error("First create JedisPool error : "+e); e.getMessage(); } } /** * 在多线程环境同步初始化 */ private static synchronized void poolInit() { if (jedisPool == null) { initialPool(); } } /** * 同步获取Jedis实例 * * @return Jedis */ public synchronized static Jedis getJedis() { if (jedisPool == null) { poolInit(); } Jedis jedis = null; try { if (jedisPool != null) { jedis = jedisPool.getResource(); } } catch (Exception e) { e.getMessage(); // logger.error("Get jedis error : "+e); } return jedis; } /** * 释放jedis资源 * * @param jedis */ public static void returnResource(final Jedis jedis) { if (jedis != null && jedisPool != null) { jedisPool.returnResource(jedis); } } public static Long sadd(String key, String... members) { Jedis jedis = null; Long res = null; try { jedis = getJedis(); res = jedis.sadd(key, members); } catch (Exception e) { //logger.error("sadd error : "+e); e.getMessage(); } return res; } }
获取锁
package com.xiaojie.lock; import com.xiaojie.utils.RedisPoolUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import redis.clients.jedis.Jedis; import java.util.UUID; /** * @author xiaojie * @version 1.0 * @description: 基于redis实现分布式锁 * @date 2021/9/19 21:17 */ @Component public class RedisDistributeLock { @Autowired private RedisPoolUtil redisPoolUtil; /** * @description: * @param: key 键值 * @param: notLockTimeOut 为获取锁的等待时间 * @param: timeOut 键值过期时间 * @return: java.lang.String * @author xiaojie * @date: 2021/9/19 21:15 */ public String getLock(String key, Integer notLockTimeOut, Long timeOut) { Jedis jedis = redisPoolUtil.getJedis(); //计算锁的超时时间 Long endTime = System.currentTimeMillis() + notLockTimeOut; //当前时间比锁的超时时间小,证明获取锁时间没有超时,继续获取 while (System.currentTimeMillis() < endTime) { String lockValue = UUID.randomUUID().toString(); //如果设置成功返回1 获取到锁,如果返回0 获取锁失败,继续获取 if (1 == jedis.setnx(key, lockValue)) { //设置键值过期时间,防止死锁 jedis.expire(key, timeOut); return lockValue; } } //关闭连接 try { if (jedis != null) { jedis.close(); } } catch (Exception e) { e.printStackTrace(); } return null; } /** * @description: 释放锁 * @param: key * @return: boolean * @author xiaojie * @date: 2021/9/19 21:25 */ public boolean unLock(String key, String lockValue) { //获取Redis连接 Jedis jedis = redisPoolUtil.getJedis(); try { // 判断获取锁的时候保证自己删除自己 if (lockValue.equals(jedis.get(key))) { return jedis.del(key) > 0 ? true : false; } } catch (Exception e) { e.printStackTrace(); } finally { if (jedis != null) { jedis.close(); } } return false; } }
基于RedisTemplate实现
package com.xiaojie.lock; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.data.redis.core.script.RedisScript; import org.springframework.stereotype.Component; import java.util.Collections; /** * @author xiaojie * @version 1.0 * @description: 基于RedisTemplate实现分布式锁 * @date 2021/9/19 21:57 */ @Component public class RedisTemplateDistributeLock { private static final Long SUCCESS = 1L; @Autowired private RedisTemplate redisTemplate; /** * @description: 获取锁 * @param: lockKey 键值 * @param: value 唯一的值 区别那个获取到的锁 * @param: expireTime 过期时间 * @return: boolean * @author xiaojie * @date: 2021/9/19 22:15 */ public boolean getLock(String lockKey, String value, Long expireTime) { try { //lua脚本 String script = "if redis.call('setNx',KEYS[1],ARGV[1]) == 1 then if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('expire',KEYS[1],ARGV[2]) else return 0 end else return 0 end"; Long result = (Long) redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Collections.singletonList(lockKey), value, expireTime); return SUCCESS.equals(result); } catch (Exception e) { e.printStackTrace(); } return false; } /** * @description: 释放锁 * @param: * @param: lockKey * @param: value * @return: boolean * @author xiaojie * @date: 2021/9/19 22:18 */ public boolean releaseLock(String lockKey, String value) { try { //lua脚本 String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class); Long result = (Long) redisTemplate.execute(redisScript, Collections.singletonList(lockKey), value); return SUCCESS.equals(result); } catch (Exception e) { e.printStackTrace(); } return false; } }
三、测试
分布式锁在分布式定时任务的测试
package com.xiaojie.schedule; import com.xiaojie.entity.User; import com.xiaojie.lock.RedisDistributeLock; import com.xiaojie.lock.RedisTemplateDistributeLock; import com.xiaojie.mapper.UserMapper; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.util.List; import java.util.UUID; /** * @ClassMyTask * @Description 模拟定时任务 * @AuthorAdministrator * @Date {Date}{Time} * @Version 1.0 **/ @Component public class MyTask { @Autowired private UserMapper userMapper; @Autowired private RedisDistributeLock redisDistributeLock; private static final String REDISKEY = "xiaojie_redis_lock"; private static final Integer NOLOCK_TIMEOUT=5; private static final Long TIME_OUT=5L; private static final String value= UUID.randomUUID().toString(); int i; @Scheduled(cron = "0/5 * * * * ? ") public void scheduledTask(){ String lockValue = redisDistributeLock.getLock(REDISKEY, NOLOCK_TIMEOUT, TIME_OUT); if(StringUtils.isBlank(lockValue)){ System.out.println("获取锁失败"); return; } List<User> users = userMapper.selectAll(); for (User user:users){ user.setNum(user.getNum()+1); int updateNum = userMapper.updateNum(user.getNum(),user.getId()); System.out.println(updateNum); } i++; System.out.println("执行了"+i+"次"); //释放锁 redisDistributeLock.unLock(REDISKEY,lockValue); } }
在不加锁的情况下,开启两台机器
可见一个机器上运行了5次一次运行7次理论上应该是7+5=12,但是数据库结果却是8。
加上锁之后
一个执行了7次一个执行了2次 7+2=9,正好符合我们测试需要的结果。
完整代码:spring-boot: Springboot整合redis、消息中间件等相关代码
参考:springboot的RedisTemplate实现分布式锁_long2010110的专栏-CSDN博客_redistemplate实现分布式锁