springBoot实现redis分布式锁
于博客 人气:0
参考:https://blog.csdn.net/weixin_44634197/article/details/108308395
、、
使用redis的set命令带NX(not exist)参数实现分布式锁
NX:只有当不存在时,才可以set;成功set会返回OK,不成功返回null
//分布式锁 public Map<String, List<Catelog2Vo>> getCatalogJsonFromDBWithRedisLock() { //1、占分布式锁。去redis占坑 Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent("lock", "lock"); if(aBoolean){ //加锁成功 执行业务 Map<String, List<Catelog2Vo>> dataFromDB = this.getDataFromDB(); //删除锁 stringRedisTemplate.delete("lock"); return dataFromDB; }else { //加锁失败 重试 自旋 return getCatalogJsonFromDBWithRedisLock(); } }
阶段二 独立加上分布式锁的过期时间
//分布式锁 public Map<String, List<Catelog2Vo>> getCatalogJsonFromDBWithRedisLock() { //1、占分布式锁。去redis占坑 Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent("lock", "lock"); if(aBoolean){ //加锁成功 执行业务 //2、设置过期时间 stringRedisTemplate.expire("lock",30, TimeUnit.SECONDS); Map<String, List<Catelog2Vo>> dataFromDB = this.getDataFromDB(); //删除锁 stringRedisTemplate.delete("lock"); return dataFromDB; }else { //加锁失败 重试 自旋 return getCatalogJsonFromDBWithRedisLock(); } }
阶段三 原子占锁和设置过期时间
//分布式锁 public Map<String, List<Catelog2Vo>> getCatalogJsonFromDBWithRedisLock() { //1、占分布式锁。去redis占坑 并设置过期时间 必须是同步的 原子的 Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent("lock", "lock",30,TimeUnit.SECONDS); if(aBoolean){ //加锁成功 执行业务 Map<String, List<Catelog2Vo>> dataFromDB = this.getDataFromDB(); //删除锁 stringRedisTemplate.delete("lock"); return dataFromDB; }else { //加锁失败 重试 自旋 return getCatalogJsonFromDBWithRedisLock(); } }
阶段四 删锁进行权限uuid匹配
//分布式锁 public Map<String, List<Catelog2Vo>> getCatalogJsonFromDBWithRedisLock() { //1、占分布式锁。去redis占坑 并设置过期时间 必须是同步的 原子的 String uuid = UUID.randomUUID().toString(); Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent("lock",uuid,30,TimeUnit.SECONDS); if(aBoolean){ //加锁成功 执行业务 Map<String, List<Catelog2Vo>> dataFromDB = this.getDataFromDB(); String lock = stringRedisTemplate.opsForValue().get("lock"); if(uuid.equals(lock)){ //删除自己的锁 stringRedisTemplate.delete("lock"); } return dataFromDB; }else { //加锁失败 重试 自旋 return getCatalogJsonFromDBWithRedisLock(); } }
阶段五 lua脚本 删锁原子操作
//分布式锁 public Map<String, List<Catelog2Vo>> getCatalogJsonFromDBWithRedisLock() { //1、占分布式锁。去redis占坑 并设置过期时间 必须是同步的 原子的 String uuid = UUID.randomUUID().toString(); Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent("lock",uuid,30,TimeUnit.SECONDS); if(aBoolean){ //加锁成功 执行业务 Map<String, List<Catelog2Vo>> dataFromDB = this.getDataFromDB(); //获取值 + 对比 + 删除 必须是原子操作 lua脚本解锁 String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] " + "then " + " return redis.call('del', KEYS[1])" + "else " + " return 0 " + "end"; Long result = stringRedisTemplate.execute(new DefaultRedisScript<Long>(luaScript, Long.class), Arrays.asList("lock"), uuid); return dataFromDB; }else { //加锁失败 重试 自旋 return getCatalogJsonFromDBWithRedisLock(); } }
阶段六 最终结果
不论业务是否正确完成都删除自己建立的锁
//分布式锁 public Map<String, List<Catelog2Vo>> getCatalogJsonFromDBWithRedisLock() { //1、占分布式锁。去redis占坑 并设置过期时间 必须是同步的 原子的 String uuid = UUID.randomUUID().toString(); Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent("lock",uuid,300,TimeUnit.SECONDS); if(aBoolean){ //加锁成功 执行业务 Map<String, List<Catelog2Vo>> dataFromDB = null; try { dataFromDB = this.getDataFromDB(); }finally { //获取值 + 对比 + 删除 必须是原子操作 lua脚本解锁 String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] " + "then " + " return redis.call('del', KEYS[1])" + "else " + " return 0 " + "end"; Long result = stringRedisTemplate.execute(new DefaultRedisScript<Long>(luaScript, Long.class), Arrays.asList("lock"), uuid); } return dataFromDB; }else { //加锁失败 重试 自旋 //睡眠 try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } return getCatalogJsonFromDBWithRedisLock(); } }
- 本博文通过进阶的形式 不断提出问题以及解决思路,一步一步完善代码,实现具有高可靠性的分布式锁功能.
自己实现redis分布式锁
通过redis实现分布式锁
public class RedisLockImpl { private static final long EXPIRETIME = 3000l; public Map<String,Object> getRedisLock() { //分布式锁实现 if (redisLock.lock("redisKey", EXPIRETIME, 0, 0)) { try { //续命 Thread thread = new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(EXPIRETIME/2);//这里休眠设置的超时时间的一般 }catch (InterruptedException e){ e.printStackTrace(); } //判断key是否存在 如果存在就重新设置超时时间 if (redisLock.hasExists(couponLock)){ //续命 boolean b = redisLock.setExpireTime(couponLock, EXPIRETIME); System.out.printf("续命"+ b); } } }); //执行业务 businessWork(); } finally { //无论成功失败都取解锁 boolean b = redisLock.releaseLock(couponLock); System.out.printf("解锁"+ b); } } else { //加锁失败 重试 自旋 try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } return getRedisLock(); } } public void businessWork(){ System.out.printf("这里执行业务代码!"); } }
redis工具类
import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisCluster; import redis.clients.jedis.JedisCommands; import java.util.ArrayList; import java.util.List; import java.util.UUID; import java.util.concurrent.TimeUnit; /** * @author yuerli * @Date 2020/7/7 * 通过给set NX(一个有锁,其他线程不能再获取锁),PX(设置锁的自动过期时间) 进行保证redis的值以及过期时间的原子性 * 通过给锁设置一个拥有者的标识,即每次在获取锁的时候,生成一个随机不唯一的串放入当前线程,释放锁的时候先去判断对应的值是否和线程中的值相同(使用lua脚本) * 避免删除了其他锁 */ @Component public class RedisLock { private RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>(); private String UNLOCK_LUA; private ThreadLocal<String> lockFlag = new ThreadLocal<String>(); @Autowired public RedisLock(RedisTemplate<Object, Object> redisTemplate) { // 通过Lua脚本来达到释放锁的原子性 if("".equals(this.UNLOCK_LUA) || this.UNLOCK_LUA==null ) { 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 "); this.UNLOCK_LUA = sb.toString(); } this.redisTemplate=redisTemplate; } public boolean lock(String key, long expire, int retryTimes, long sleepMillis) { boolean result = setRedis(key, expire); // 如果获取锁失败,按照传入的重试次数进行重试 while((!result) && retryTimes--> 0){ try { Thread.sleep(sleepMillis); } catch (InterruptedException e) { return false; } result = setRedis(key, expire); } return result; } private boolean setRedis(String key, long expire) { //为了保证设置锁和过期时间的两个操作原子性 spring data 的 RedisTemplate当中没有这样的方法,但是jedis当中有这样的原子操作的方法 //需要通过RedisTemplate的execute方法获取jedis里操作命令对象 // NX:表示只有当锁定资源不存在的时候才能set成功。利用Redis的原子性,保证了只有第一个请求的线程才能获得锁,而后其他线程在锁定资源释放前都不能获取锁 // PX:expire表示锁定的资源的自动过期时间,单位是毫秒。具体过期时间根据实际场景而定。 //通过set NX,PX的命令设置保证了Redis值和自动过期时间的原子性,避免在调用setIfAbsent方法的时候线程挂掉,没有设置过期时间而导致死锁,使得锁不能释放 try { String result = redisTemplate.execute(new RedisCallback<String>() { @Override public String doInRedis(RedisConnection connection) throws DataAccessException { JedisCommands commands = (JedisCommands) connection.getNativeConnection(); String uuid = UUID.randomUUID().toString(); lockFlag.set(uuid); // 锁定的资源 return commands.set(key, uuid, "NX", "PX", expire); } }); return !StringUtils.isEmpty(result); } catch (Exception e) { System.out.println(e.getMessage()); } return false; } /*上面的方法通过设置set的NX,PX命令保证了Redis值和自动过期时间的原子性,但是还有一个问题是如果线程T1获取锁,但是在处理T1的业务时候, 由于某些原因阻塞了较长时间,这个时候设定的过期时间到了,线程T2获取了锁,线程T1操作完后释放了锁(释放了T2的锁) 所以也就是说T2的线程上面没有提供锁的保护机制。因此需要给锁定一个拥有者的标识,即每次在获取锁的时候,生成一个随机不唯一的串放入当前线程, 释放锁的时候先去判断对应的值是否和线程中的值相同。*/ public boolean releaseLock(String key) { // 释放锁的时候,有可能因为持锁之后方法执行时间大于锁的有效期,此时有可能已经被另外一个线程持有锁,所以不能直接删除 try { List<String> keys = new ArrayList<String>(); keys.add(key); List<String> args = new ArrayList<String>(); args.add(lockFlag.get()); // 使用lua脚本删除redis中匹配value的key,可以避免由于方法执行时间过长而redis锁自动过期失效的时候误删其他线程的锁 // spring自带的执行脚本方法中,集群模式直接抛出不支持执行脚本的异常,所以只能拿到原redis的connection来执行脚本 Long result = redisTemplate.execute(new RedisCallback<Long>() { public Long doInRedis(RedisConnection connection) throws DataAccessException { Object nativeConnection = connection.getNativeConnection(); // 集群模式和单机模式虽然执行脚本的方法一样,但是没有共同的接口,所以只能分开执行 // 集群模式 if (nativeConnection instanceof JedisCluster) { return (Long) ((JedisCluster) nativeConnection).eval(UNLOCK_LUA, keys, args); } // 单机模式 else if (nativeConnection instanceof Jedis) { return (Long) ((Jedis) nativeConnection).eval(UNLOCK_LUA, keys, args); } return 0L; } }); return result != null && result > 0; } catch (Exception e) { e.printStackTrace(); } return false; } /** * 获取过期时间 * @param key * @return */ public Long getExpireTime(String key) { Long expire1 = redisTemplate.getExpire(key); return expire1; } /** * 重新设置过期时间 * @param key * @return */ public boolean setExpireTime(String key, long expire) { Boolean expire1 = redisTemplate.expire(key, expire, TimeUnit.MILLISECONDS); return expire1; } /** * 判断是否存在 * @param key * @return */ public boolean hasExists(String key) { boolean exists = redisTemplate.hasKey(key); return exists; } }
redis做幂等
//利用redis做幂等 //同一个key2秒之内只能触发一次 if(!redisLock.lock("redisKey",2000,0,0) ) { System.out.printf("请勿重复提交"); }
写的有错的地方,请大家指正,多多包涵!
加载全部内容