一、使用场景
分布式服务中有些场景(比如定时任务和库存更新)需要考虑并发执行所可能带来的问题。
如果不使用专业的分布式定时工具(如quartz),而是简单地依赖定时器来做定时任务,那么会有并发执行的情况,可能造成某些问题。
对于库存更新这样的场景,我们需要考虑对同一条记录进行更新并记录所可能出现的问题,比如读未提交造成的记录错乱。为了解决这些问题,我们可以引入分布式事务锁。这种锁可以确保在分布式系统中操作的正确性和一致性。
二、分布式锁的实现过程中可能会遇到的问题
1.程序出现异常导致锁一直不释放,造成死锁
2.锁的过期时间到了,但是业务没处理完,导致其他线程拿到锁,也就是分布式锁无法自动续期
为解决以上问题,推荐使用比较完整优秀的分布式锁:
- 基于Redisson实现分布式锁原理(Redission是一个独立的redis客户端,是与Jedis、Lettuce同级别的存在)
Redisson的加锁机制如下图所示:
锁资源被多个线程获取,成功则执行lua脚本,保存数据到redis数据库。
如果获取失败: 一直通过while循环尝试获取锁(可自定义等待时间,超时后返回失败)。
Redisson提供的分布式锁是支持锁自动续期的,也就是说,锁的过期时间到了,但是业务没处理完,那么redisson会自动给redis中的目标key延长超时时间,这在Redisson中称之为 Watch Dog 看门狗机制。
三、代码实现
1.依赖和配置文件
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.15.0</version>
</dependency>
在配置文件中加
spring:
redis:
redisson:
file: classpath:redisson.yaml
然后新建一个redisson.yaml文件,也放在resouce目录下
singleServerConfig:
idleConnectionTimeout: 10000
connectTimeout: 10000
timeout: 3000
retryAttempts: 3
retryInterval: 1500
password: 123456
subscriptionsPerConnection: 5
clientName: null
address: "redis://localhost:6379"
subscriptionConnectionMinimumIdleSize: 1
subscriptionConnectionPoolSize: 50
connectionMinimumIdleSize: 32
connectionPoolSize: 64
database: 1
dnsMonitoringInterval: 5000
threads: 0
nettyThreads: 0
codec: !<org.redisson.codec.JsonJacksonCodec> {}
transportMode: "NIO"
2.Redis工具类
(1)redisson操作类,本案例只用到tryLock()和unLock()两个方法,其他的可以根据业务灵活选用
package com.sync.task.core.redislock;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* redisson操作类
*/
@Component
public class RedisDistributedLocker {
@Autowired
private RedissonClient redissonClient;
public RLock lock(String lockKey) {
RLock lock = redissonClient.getLock(lockKey);
lock.lock();
return lock;
}
public RLock lock(String lockKey, int leaseTime) {
RLock lock = redissonClient.getLock(lockKey);
lock.lock(leaseTime, TimeUnit.SECONDS);
return lock;
}
public RLock lock(String lockKey, TimeUnit unit ,int timeout) {
RLock lock = redissonClient.getLock(lockKey);
lock.lock(timeout, unit);
return lock;
}
public boolean tryLock(String lockKey, TimeUnit unit, int waitTime, int leaseTime) {
RLock lock = redissonClient.getLock(lockKey);
try {
return lock.tryLock(waitTime, leaseTime, unit);
} catch (InterruptedException e) {
return false;
}
}
public boolean tryLock(String lockKey) {
RLock lock = redissonClient.getLock(lockKey);
return lock.tryLock();
}
public void unlock(String lockKey) {
RLock lock = redissonClient.getLock(lockKey);
//查看是自己加的锁吗,是自己的锁再释放
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
public void unlock(RLock lock) {
lock.unlock();
}
}
(2)分布式锁工具类
package com.sync.task.core.redislock;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.concurrent.TimeUnit;
/**
* redis分布式锁工具类
*/
@Component
public class RedisLockUtils {
@Autowired
private RedisDistributedLocker locker;
private static RedisDistributedLocker distributedLocker;
@PostConstruct
private void init() {
distributedLocker = locker;
}
/**
* 加锁
* @param lockKey
* @return
*/
public static RLock lock(String lockKey) {
return distributedLocker.lock(lockKey);
}
/**
* 释放锁
* @param lockKey
*/
public static void unlock(String lockKey) {
distributedLocker.unlock(lockKey);
}
/**
* 释放锁
* @param lock
*/
public static void unlock(RLock lock) {
distributedLocker.unlock(lock);
}
/**
* 带超时的锁
* @param lockKey
* @param timeout 超时时间 单位:秒
*/
public static RLock lock(String lockKey, int timeout) {
return distributedLocker.lock(lockKey, timeout);
}
/**
* 带超时的锁
* @param lockKey
* @param unit 时间单位
* @param timeout 超时时间
*/
public static RLock lock(String lockKey, int timeout, TimeUnit unit ) {
return distributedLocker.lock(lockKey, unit, timeout);
}
/**
* 尝试获取锁
* @param lockKey
* @param waitTime 最多等待时间
* @param leaseTime 上锁后自动释放锁时间
* @return
*/
public static boolean tryLock(String lockKey, int waitTime, int leaseTime) {
return distributedLocker.tryLock(lockKey, TimeUnit.SECONDS, waitTime, leaseTime);
}
/**
* 尝试获取锁
* @param lockKey
* @param unit 时间单位
* @param waitTime 最多等待时间
* @param leaseTime 上锁后自动释放锁时间
* @return
*/
public static boolean tryLock(String lockKey, TimeUnit unit, int waitTime, int leaseTime) {
return distributedLocker.tryLock(lockKey, unit, waitTime, leaseTime);
}
/**
* 尝试获取锁,不设置等待和过期时间,看门狗才能真正生效,自动续期
* @param lockKey
* @return
*/
public static boolean tryLock(String lockKey) {
return distributedLocker.tryLock(lockKey);
}
}
3.业务实现类
保存商品信息到Redis在这里就不做赘述了,需要了解的看我另外一篇文章:
https://mp.csdn.net/mp_blog/creation/editor/130848593
以下是保存到Redis的商品信息:
提交商品订单:
/**
* 提交订单
* @param productId 商品id
* @param count 购买数量
* @return
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Result<String> submitOrder(String productId, long count) {
String productKey = PRODUCT + productId; // PRODUCT_N001
String stockKey = PRODUCT_STOCK + productId; // PRODUCT_STOCK_N001
String stockLockKey = PRODUCT_STOCK_LOCK +productId; // PRODUCT_STOCK_LOCK_N001
// 获取库存锁,不会自动续期,最多等待3秒,超过则放弃获取锁,20秒后锁自动释放
// boolean res = RedisLockUtils.tryLock(stockLockKey, TimeUnit.SECONDS, 3, 20);
// 获取库存锁,自动续期
boolean res = RedisLockUtils.tryLock(stockLockKey);
if (!res) {
// 超过等待时间,放弃获取锁
log.info("线程{} 获取锁失败", stockLockKey);
return ResponseData.error("系统正忙,请稍后再试");
}
log.info("线程{} 获取锁成功", stockLockKey);
try {
// 查询库存
Integer stock = (Integer) redisTemplate.opsForValue().get(stockKey);
// 缓存没有商品库存,查询数据库
if (stock == null) {
Product product = productMapper.getById(productId);
if (Objects.isNull(product)) {
return ResponseData.error("订单提交失败,商品已下架");
}
redisTemplate.opsForValue().set(productKey, JSON.toJSONString(product));
// 数据库的库存信息要根据实际业务来获取,如果商品有规格信息,库存应该根据规格来获取
stock = product.getStack();
redisTemplate.opsForValue().set(stockKey, stock);
return ResponseData.error("订单提交失败,库存不足");
}
// 检查库存是否足够
if (stock < count) {
return ResponseData.error("订单提交失败,库存不足");
}
// 更新数据库中的库存数据
int row = productMapper.updateStock(productId, stock - count);
if (row > 0) {
// 更新Redis中缓存的商品库存数据
redisTemplate.opsForValue().decrement(stockKey, count);
}
// 库存扣减完,创建订单
Order order = Order.builder()
.id(IdUtil.simpleUUID()).count(count).productId(productId).status(1)
.build();
String orderId = orderService.createOrder(order);
return ResponseData.success(orderId, "订单创建成功");
} catch (OptimisticLockingFailureException e) {
// 如果出现乐观锁异常,则释放 Redis 锁并返回错误信息
log.error("更新库存失败,商品 {} 可能已被更新", productId);
} catch (Exception e) {
log.error("系统开小差了~");
} finally {
RedisLockUtils.unlock(stockLockKey);
log.info("线程{} 释放锁", stockLockKey);
}
return ResponseData.error("系统正忙,请稍后再试");
}