1. 什么是锁
在一个进程中,当存在多个线程同时操作某个共享资源时,就需要对共享资源做同步,使其在修改这个共享资源时能够线性地执行操作。而实现同步的手段就是锁,当线程准备对共享资源做修改前,先获取锁,如果当前共享资源已经被锁,则进行等待;若没有被锁,则成功获取锁,并允许执行修改。在修改完毕后,释放锁资源,那么下一个线程也是如此,只有获取到锁,才能对共享资源修改。
2. 什么是分布式锁
分布式锁是为了防止分布式系统中多个进程之间相互干扰,从而需要一种分布式协调技术来对这些进程进行调度。
3. Redis实现分布式锁
实现分布式锁的手段有很多,比如基于数据库的悲观锁和乐观锁、基于Redis做分布式锁、基于Zookeeper做分布式锁等等。之前,我们把Redis当做缓存系统使用,今天我们再来看看如何使用Redis实现分布式锁。
Redis实现分布式锁需要用到其中的两个方法:setnx()
和expire()
。
- setnx()
setnx
的含义就是SET if Not Exists
,该方法是原子的,如果key不存在,则设置当前key成功,返回1;如果当前key已经存在,则设置当前key失败,返回0。
- expire()
expire
设置过期时间,要注意的是setnx
命令不能设置key的超时时间,只能通过expire
来对key设置。
3.1 实现步骤
setnx(key, value)
如果返回0,则说明当前资源已经被锁,需要等待;如果返回1,则说明成功获取锁;expire()
命令对key设置超时时间,避免死锁;- 执行业务代码后,通过
delete
命令删除key。
需要注意的是:从解决日常工作中的需求来说,该方案已经够用。但从技术角度来说,该方案还不够严谨。比如在第一步设置
setnx
执行成功后,expire
命令执行前出现了异常,那么就还是会出现死锁问题。但是我们这个项目中暂时不考虑这种异常情况了,在后面可能会有一个专门讨论分布式锁
的专题博客,可以持续关注下。
3.2 业务背景
以商品微服务下的商品入库单业务为背景,商品入库单存在未生效
和已生效
两种业务状态,当录入一个商品入库单保存成功之后,该商品入库单为未生效
状态,此时商品还没有入库。只有在执行生效这个业务动作时,商品入库单从未生效
变成已生效
状态,然后再对商品的库存进行增加。从业务角度上来说,一个商品入库单是不允许重复生效的,所以在这里就需要对生效的商品入库单进行加锁,避免分布式环境下重复生效。
3.3 代码实现
在com.autumn.mall.commons.utils
包结构中,有一个RedisUtils
工具类,用来对Redis常规操作做了简单的封装,其中我们也对获取分布式锁的操作进行了封装:
public boolean tryLock(String key) {
return tryLock(key, null);
}
public boolean tryLock(String key, String value) {
return tryLock(key, value, 3, TimeUnit.MINUTES);
}
public boolean tryLock(String key, String value, int timeout, TimeUnit timeUnit) {
try {
if (StringUtils.isBlank(value)) {
long currTime = System.currentTimeMillis();
// 加锁成功
return redisTemplate.opsForValue().setIfAbsent(key, currTime);
}
return redisTemplate.opsForValue().setIfAbsent(key, value);
} finally {
redisTemplate.expire(key, timeout, timeUnit);
}
}
商品入库单(GoodsInboundServiceImpl
)生效方法:
public void doEffect(String uuid) {
// 获取分布式锁
try {
while (redisUtils.tryLock(getLockKeyPrefix() + uuid) == false) {
TimeUnit.SECONDS.sleep(3);
}
} catch (Exception e) {
MallExceptionCast.cast(CommonsResultCode.TRY_LOCKED_ERROR);
}
Optional<GoodsInbound> optional = goodsInboundRepository.findById(uuid);
if (optional.isPresent() == false) {
MallExceptionCast.cast(CommonsResultCode.ENTITY_IS_NOT_EXIST);
}
if (optional.get().getState().equals(BizState.effect)) {
MallExceptionCast.cast(ProductResultCode.ENTITY_IS_EQUALS_TARGET_STATE);
}
// 生效入库单
GoodsInbound entity = optional.get();
entity.setState(BizState.effect);
getRepository().save(entity);
// 商品入库
List<GoodsInboundDetail> details = goodsInboundDetailRepository.findAllByGoodsInboundUuidOrderByLineNumber(entity.getUuid());
List<Stock> stocks = new ArrayList<>();
details.stream().forEach(detail -> {
Stock stock = new Stock();
stock.setEntityKey(MallModuleKeyPrefixes.PRODUCT_KEY_PREFIX_OF_GOODS + detail.getGoodsUuid());
stock.setWarehouse(entity.getWarehouse());
stock.setQuantity(detail.getQuantity());
stocks.add(stock);
});
ResponseResult responseResult = stockClient.inbound(stocks);
// 删除分布式锁
redisUtils.remove(getLockKeyPrefix() + uuid);
}