场景
在抢购平台上,如何避免超买超卖的场景?
如果你是单体服务,可以使用synchronized ,但是如果是部署两台或多台服务的话,synchronized 就不起作用了,因为的作用域只是单个的JVM。这个时候就要引入分布式锁的概念。
概念
同一时间只能有一个客户端对共享资源进行操作。
实现
1.利用setnx
@Autowired
private RedisTemplate redisTemplate;
@RequestMapping("/getProduct")
@RequestMapping("/getProduct")
public String getProduct() {
String lockKey = "product_01";
//每个线程给一个给一个标识
String cid = UUID.randomUUID().toString();
//如果这个值存在返回false 如果这个值不存在插入成功返回true setnx
//这里设置超时时间是为了解决,如果执行是突然出现宕机情况,这个锁一直无法释放。
boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, cid, 10, TimeUnit.SECONDS);
if (!result) {
return "ERROR";
}
try {
/*
一些业务代码操作,成功返回成功
return "Success"
*/
} finally {
//关闭自己开启的锁 但是任然存在问题 可能在判断的那0.01s这个锁被释放掉 这样就会导致删除的任然不是自己的锁
if (cid.equals(redisTemplate.opsForValue().get(lockKey))) {
redisTemplate.delete(lockKey);
}
}
return "ERROR";
}
但是以上代码仍然存在问题,如果在业务操作中,出现一些类似慢sql、卡死等问题,导致代码无法在超时时间内完成,于是又可能出现超买超卖的现象。其实主要是对超时时间得把控,设置太短了容易,导致代码锁失效,太长了系统宕机,之后锁一直无法释放。怎么来解决这个问题呢?
锁续命,就是当系统业务逻辑代码没有执行完对代码超时时间进行续命。
Redisson(redis的儿子)很好的封装了这一机制。
大概逻辑:当一个线程如果加锁成功,那么它会启动一个后台线程,每隔一段时间检查是否还持有,如果持有那么就会延长时间。其他线程过来的时候就会一直判断能都加锁成功。
pom.xml
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.6.5</version>
</dependency>
配置bean
@Bean
public Redisson redisson() {
Config config = new Config();
config
.useSingleServer()//单机模式
.setAddress("redis://127.0.0.1:6379")
.setDatabase(0);
return (Redisson) Redisson.create(config);
}
@Autowired
private Redisson redisson;
@RequestMapping("/getProduct2")
public String getProduct2() {
String lockKey = "product_01";
//拿到锁对象
RLock lock = redisson.getLock(lockKey);
try {
//加锁
lock.lock();
/*
一些业务代码操作,成功返回成功
return "Success"
*/
} finally {
//释放锁
lock.unlock();
}
return "ERROR";
}
//加锁逻辑 lua脚本在redis执行 会保证原子性
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
internalLockLeaseTime = unit.toMillis(leaseTime);
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
"if (redis.call('exists', KEYS[1]) == 0) then " +//判断我们的lockkey是否存在
"redis.call('hset', KEYS[1], ARGV[2], 1); " +//如果不存在 就会设置一个key
"redis.call('pexpire', KEYS[1], ARGV[1]); " +//设置一个超时时间 超时时间默认30s
"return nil; " +
"end; " +
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"return redis.call('pttl', KEYS[1]);",
Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}
//续命的逻辑
private void scheduleExpirationRenewal(final long threadId) {
if (expirationRenewalMap.containsKey(getEntryName())) {
return;
}
Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
//lua脚本主要判断key是否存在(lockkey),
//然后判断锁是否还存在 存在就续命
//如果线程结束了,就不会加锁了( ARGV[2])== 1)
RFuture<Boolean> future = commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return 1; " +
"end; " +
"return 0;",
Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
future.addListener(new FutureListener<Boolean>() {
@Override
public void operationComplete(Future<Boolean> future) throws Exception {
expirationRenewalMap.remove(getEntryName());
if (!future.isSuccess()) {
log.error("Can't update lock " + getName() + " expiration", future.cause());
return;
}
if (future.getNow()) {
// reschedule itself 延迟10秒调用
scheduleExpirationRenewal(threadId);
}
}
});
}
//锁加完以后,会延迟internalLockLeaseTime / 3,(一般为10秒)
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
if (expirationRenewalMap.putIfAbsent(getEntryName(), task) != null) {
task.cancel();
}
}
其实性能还是一个问题,并发变成串行。
参考
分布式锁的代码实现 https://blog.csdn.net/qq_37781649/article/details/108814474
有问题及时指正啊各位!