<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); } }); }
执行结果如下:
上述分布式锁实现库存扣减是否存在相关问题呢?
具体的代码如下:
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; }
因为单机版本的锁是无法重入锁,所以加锁失败就直接返回,此问题的解决方案,可以采用Redisson来实现,关于Redisson实现分布式锁,将在后续的文章中进行详细的讲解。
本文主要讲解了Spring Boot集成Redis实现单机版本分布式锁,虽然单机版分布式锁存在锁的续期、锁的重入问题,但是我们还是需要掌握其原理和实现方法