前言
Redis分布式锁的原理掌握了相关理论知识后,我们还需要掌握其具体实现的方法,本文将Spring Boot集成Redis如何实现单机Redis分布式锁进行详细讲解。
分布式锁实现
引入jar包
org.springframework.boot
spring-boot-starter-data-redis
io.lettuce
lettuce-core
redis.clients
jedis
说明:本文采用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 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) 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) 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 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 keys = new ArrayList<>();
keys.add(key);
// 脚本里的ARGV参数
List args = new ArrayList<>();
args.add(Integer.toString(num));
long result = redisTemplate.execute(new RedisCallback() {
@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实现单机版本分布式锁,虽然单机版分布式锁存在锁的续期、锁的重入问题,但是我们还是需要掌握其原理和实现方法,如有疑问,请随时反馈,大家共同学习,共同进步。