Spring Boot 实现Redis分布式锁原理

目录

分布式锁实现

引入jar包

封装工具类

模拟秒杀扣减库存

测试代码

方案优化

问题1:扣减库存逻辑无法保证原子性,

问题2:如果加锁失败,则会直接访问,无法重入锁

总结

分布式锁实现 引入jar包 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <exclusions> <exclusion> <groupId>io.lettuce</groupId> <artifactId>lettuce-core</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency>

说明:本文采用jedis来实现分布式锁。

封装工具类 @Component public class RedisLockUtil { private static final Logger logger = LoggerFactory.getLogger(RedisLockUtil.class); private static final Long RELEASE_SUCCESS = 1L; private static final String LOCK_SUCCESS = "OK"; private static final String RELEASE_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; @Resource private RedisTemplate<String, Object> redisTemplate; /** * 加锁方法仅针对单实例 Redis,哨兵、集群模式无法使用 * * @param lockKey 加锁键 * @param clientId 加锁客户端唯一标识(采用UUID) * @param seconds 锁过期时间 * @return true标识加锁成功、false代表加锁失败 */ public Boolean tryLock(String lockKey, String clientId, long seconds) { try { return redisTemplate .execute((RedisCallback<Boolean>) redisConnection -> { Jedis jedis = (Jedis) redisConnection.getNativeConnection(); SetParams params =new SetParams(); params.nx(); params.px(seconds); String result = jedis.set(lockKey, clientId, params); if (LOCK_SUCCESS.equals(result)) { return Boolean.TRUE; } return Boolean.FALSE; }); } catch (Exception e) { logger.error("tryLock error",e); } return false; } /** *释放锁,保持原子性操作,采用了lua脚本 * * @param lockKey * @param clientId * @return */ public Boolean unLock(String lockKey, String clientId) { try { return redisTemplate .execute((RedisCallback<Boolean>) redisConnection -> { Jedis jedis = (Jedis) redisConnection.getNativeConnection(); Object result = jedis.eval(RELEASE_LOCK_SCRIPT, Collections.singletonList(lockKey), Collections.singletonList(clientId)); if (RELEASE_SUCCESS.equals(result)) { return Boolean.TRUE; } return Boolean.FALSE; }); } catch (Exception e) { logger.error("unlock error",e); } return Boolean.FALSE; } }

说明:加锁的原理是基于Redis的NX、PX命令,而解锁采用的是lua脚本实现。

模拟秒杀扣减库存 public int lockStock() { String lockKey="lock:stock"; String clientId = UUID.randomUUID().toString(); long seconds =1000l; try { //加锁 boolean flag=redisLockUtil.tryLock(lockKey, clientId, seconds); //加锁成功 if(flag) { logger.info("加锁成功 clientId:{}",clientId); int stockNum= Integer.valueOf((String)redisUtil.get("seckill:goods:stock")); if(stockNum>0) { stockNum--; redisUtil.set("seckill:goods:stock",String.valueOf(stockNum)); logger.info("秒杀成功,剩余库存:{}",stockNum); } else { logger.error("秒杀失败,剩余库存:{}", stockNum); } //获取库存数量 return stockNum; } else { logger.error("加锁失败:clientId:{}",clientId); } } catch (Exception e) { logger.error("decry stock eror",e); } finally { redisLockUtil.unLock(lockKey, clientId); } return 0; } 测试代码 @RequestMapping("/redisLockTest") public void redisLockTest() { // 初始化秒杀库存数量 redisUtil.set("seckill:goods:stock", "10"); List<Future> futureList = new ArrayList<>(); //多线程异步执行 ExecutorService executors = Executors.newScheduledThreadPool(10); // for (int i = 0; i < 30; i++) { futureList.add(executors.submit(this::lockStock)); try { Thread.sleep(100); } catch (InterruptedException e) { logger.error("redisLockTest error",e); } } // 等待结果,防止主线程退出 futureList.forEach(t -> { try { int stockNum =(int) t.get(); logger.info("库存剩余数量:{}",stockNum); } catch (Exception e) { logger.error("get stock num error",e); } }); }

执行结果如下:

方案优化

上述分布式锁实现库存扣减是否存在相关问题呢?

问题1:扣减库存逻辑无法保证原子性,

具体的代码如下:

int stockNum= Integer.valueOf((String)redisUtil.get("seckill:goods:stock")); if(stockNum>0) { stockNum--; redisUtil.set("seckill:goods:stock",String.valueOf(stockNum)); }

这是典型的RMW模型,前面章节已经介绍了具体的实现方案,可以采用lua脚本和Redis的incry原子性命令实现,这里采用lua脚本来实现原子性的库存扣减。

具体实现如下:

public long surplusStock(String key ,int num) { StringBuilder lua_surplusStock = new StringBuilder(); lua_surplusStock.append(" local key = KEYS[1];"); lua_surplusStock.append(" local subNum = tonumber(ARGV[1]);"); lua_surplusStock.append(" local surplusStock=tonumber(redis.call('get',key));"); lua_surplusStock.append(" if (surplusStock- subNum>= -1) then"); lua_surplusStock.append(" return redis.call('incrby', KEYS[1], 0-subNum);"); lua_surplusStock.append(" else "); lua_surplusStock.append(" return -1;"); lua_surplusStock.append(" end"); List<String> keys = new ArrayList<>(); keys.add(key); // 脚本里的ARGV参数 List<String> args = new ArrayList<>(); args.add(Integer.toString(num)); long result = redisTemplate.execute(new RedisCallback<Long>() { @Override public Long doInRedis(RedisConnection connection) throws DataAccessException { Object nativeConnection = connection.getNativeConnection(); // 单机模式 if (nativeConnection instanceof Jedis) { return (Long) ((Jedis) nativeConnection).eval(lua_surplusStock.toString(), keys, args); } return -1l; } }); return result; } 问题2:如果加锁失败,则会直接访问,无法重入锁

因为单机版本的锁是无法重入锁,所以加锁失败就直接返回,此问题的解决方案,可以采用Redisson来实现,关于Redisson实现分布式锁,将在后续的文章中进行详细的讲解。

总结

本文主要讲解了Spring Boot集成Redis实现单机版本分布式锁,虽然单机版分布式锁存在锁的续期、锁的重入问题,但是我们还是需要掌握其原理和实现方法

到此这篇关于Spring Boot 实现Redis分布式锁原理的文章就介绍到这了,更多相关Spring Boot Redis分布式锁内容请搜索易知道(ezd.cc)以前的文章或继续浏览下面的相关文章希望大家以后多多支持易知道(ezd.cc)!

推荐阅读