4、Redis高并发分布式锁

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值