Redis分布式锁
在订单扣除-1的时候,如果服务使用负载均衡,Nginx配置upstream两个端口,在高并发下会产生超卖或者其他问题,具体的问题代码:
String lockKey = "lock:product_101";
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "zhuge");
stringRedisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);
String clientId = UUID.randomUUID().toString();
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS); //jedis.setnx(k,v)
if (!result) {
return "error_code";
}
try {
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
} finally {
if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
stringRedisTemplate.delete(lockKey);
}
}
以上代码在高并发下,不能保证原子性,为了保证原子性,可以使用redisson来实现
上代码
1、导包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.6.5</version>
</dependency>
2、交给springIOC容器实例化
@Component
public class XXX{
@Bean
public Redisson redisson() {
// 此为单机模式
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379").setDatabase(0);
return (Redisson) Redisson.create(config);
}
}
3、代码实现
//首先需要在redis里面set("stock","100")来造测试数据
@GetMapping("/testRedisson")
public String redisson(){
String lockKey = "lock:product_101";
//获取锁
RLock lock = redisson.getLock(lockKey);
//加分布式锁
lock.lock();
try {
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if (stock>0){
int realStock = stock -1;
stringRedisTemplate.opsForValue().set("stock",realStock+"");
System.out.println("扣减成功,剩余库存:"+realStock);
}else {
System.out.println("扣减失败,库存不足");
}
}finally {
lock.unlock();
}
return "end";
}
在application.yml修改port来部署两个相同的服务,端口不同。
然后修改Nginx的配置文件
upstream redislock{
server 自己的ip:端口1 weight=1;
server 自己的ip:端口2 weight=1;
}
server {
listen 80;
server_name localhost;
location /{
root html;
index index.html index.htm;
proxy_pass http://redislock;
}
}
可以使用jemeter压测查看日志是否多扣
redisson加锁
多个线程去对key进行加锁,只会有一个加锁成功,其他线程如果加锁失败,会自旋,一直重复加锁的步骤,循环的过程中会有停顿,这个是根据获取锁的时候,已经获取锁的剩余过期时间,停顿时间就是这个剩余时间,防止一直持续加锁,影响性能
源码解析
1、获取锁
//获取锁
RLock lock = redisson.getLock(lockKey);
最底层是name的赋值,即代码中我们需要锁定的key值
2、上锁
//加分布式锁
lock.lock();
@Override
public void lock() {
try {
lockInterruptibly();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
-----------------------------------------------------------------------------------------------------------------------------------
@Override
public void lockInterruptibly() throws InterruptedException {
lockInterruptibly(-1, null);
}
----------------------------------------------------------------------------------------------------------------------------------- @Override
public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
long threadId = Thread.currentThread().getId();
Long ttl = tryAcquire(leaseTime, unit, threadId); // @1
// lock acquired
if (ttl == null) {
return;
}
// 发布订阅,线程阻塞等待时候如何提前结束,需要触发,onMessage方法里面有具体的代码逻辑
RFuture<RedissonLockEntry> future = subscribe(threadId);
commandExecutor.syncSubscription(future);
try {
while (true) {
//获取剩余过期时间,并且尝试继续加锁
ttl = tryAcquire(leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
break;
}
// waiting for message
if (ttl >= 0) {
//阻塞ttl时间再去执行加锁
getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
getEntry(threadId).getLatch().acquire();
}
}
} finally {
unsubscribe(future, threadId);
}
// get(lockAsync(leaseTime, unit));
}
------------------------------------------------------------@1--------------------------------------------------------------------- private Long tryAcquire(long leaseTime, TimeUnit unit, long threadId) {
return get(tryAcquireAsync(leaseTime, unit, threadId));
}
-----------------------------------------------------------------------------------------------------------------------------------
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {
//根据传值,我们可以看到leaseTime=-1,unit=null,threadId是当前的线程id
if (leaseTime != -1) {
return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
}
//@2
RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
ttlRemainingFuture.addListener(new FutureListener<Long>() {
@Override
public void operationComplete(Future<Long> future) throws Exception {
if (!future.isSuccess()) {
return;
}
Long ttlRemaining = future.getNow(); //加锁成功会获取lua脚本的nil,即Java 的null
// lock acquired
if (ttlRemaining == null) {
scheduleExpirationRenewal(threadId); // @3
}
}
});
return ttlRemainingFuture;
}
--------------------------------@2-----------------------------------------------------------------------------------------------
<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不存在则hset(lockkey,线程id,1),设置lockkey的过期时间30
"redis.call('hset', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"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]);", //这个是锁已经存在,无法加上锁,它会获取剩余时间pttl,然后再去@1处继续往下处理
Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
//解析上面的lua脚本,后面的三个参数分别是:
// Collections.<Object>singletonList(getName()) = 我们加锁的数据lockKey
// internalLockLeaseTime = 30s
// getLockName(threadId) = id:线程id
}
-------------------------------------------------------------------------------@3-------------------------------------------------
private void scheduleExpirationRenewal(final long threadId) {
if (expirationRenewalMap.containsKey(getEntryName())) {
return;
}
// new TimerTask定时任务,在后面有30/3秒的定时执行
Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
//lua脚本是判断localkey,线程id,即存储的数据是否还存在,如果存在,则再加30s的过期时间,返回1,1即Java中的true,在operationComplete方法中会用到这个值,即future.getnow()是true,循环调用自己scheduleExpirationRenewal
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
scheduleExpirationRenewal(threadId);
}
}
});
}
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
if (expirationRenewalMap.putIfAbsent(getEntryName(), task) != null) {
task.cancel();
}
}
3、解锁
lock.unlock();
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"if (redis.call('exists', KEYS[1]) == 0) then " + //判断lockKey是否还在,不存在则发布消息,返回1
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; " +
"end;" +
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " + //判断当前线程的数据是否存在
"return nil;" +
"end; " +
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " + //当前线程id减1,加锁的时候是1,正常情况是0,删除localkey,并且发布消息。
"if (counter > 0) then " +
"redis.call('pexpire', KEYS[1], ARGV[2]); " +
"return 0; " +
"else " +
"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; "+
"end; " +
"return nil;",
Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId));
}
1、总结:线程1,第一次加锁,首先set值,然后设置过期时间。如果期间有其他线,2抢占锁,首先获取线程1的数据过期时间,然后等待阻塞,到时间了再去循环尝试加锁(这样如果在线程1的接收的时候,阻塞时间还没到,就会浪费时间,并且阻塞,此时就会用到发布订阅),如果线程1提前结束了,它会发布结束,删除对应的lockkey, 其他线程就会收到信息,接收阻塞,去加锁,如果除了线程2还有线程3,此时就会非公平锁去抢占
http://t.csdn.cn/xjCkk