Redis分布式锁实战

系列文章目录

第一节 Redis的安装
第二节 Redis的五种数据结构(String、Hash、List、Set、ZSet)
第三节 Redis的持久化方式
第四节 Redis主从架构
第五节 Redis哨兵高可用架构
第六节 Redis集群高可用架构
第七节 Redis集群选举原理与脑裂问题
第八节 Redis高可用集群之水平扩展操作实战


Redis分布式锁实战

  • 系列文章目录
  • 前言
  • 一、Redis高并发问题现象
    • 1、设置stock_number值
    • 2、SpringBoot集成Redis
      • pom.xml
      • application.yml
    • 3、编写Controller
    • 4、JMeter模拟高并发场景
  • 二、使用并发锁存在的问题
    • 遗留问题
  • 三、Redis分布式问题解决方案
    • 1、SETNX
    • 2、Redisson分布式锁
      • 加锁实现原理
      • SpringBoot集成Redission
        • pom.xml
        • Redission加入Bean容器
        • controller注入
        • 加锁逻辑
    • 3、Redisson底层实现原理
      • 看门狗的续期时间
      • RLock#lock()
      • RLock#unlock()
  • 四、最终遗留问题


前言

本节介绍Redis分布式锁实战,包含Redis高并发问题现象与Redis分布式问题解决方案两大方面。


一、Redis高并发问题现象

1、设置stock_number值

在redis客户端操作,设置stock_number为50

192.168.75.200:7001> set stock_number 50
OK

2、SpringBoot集成Redis

pom.xml

		
		<dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        
		
        <dependency>
			<groupId>org.springframework.bootgroupId>
			<artifactId>spring-boot-starter-data-redisartifactId>
		dependency>

		
		<dependency>
			<groupId>org.apache.logging.log4jgroupId>
			<artifactId>log4j-coreartifactId>
			<version>2.3version>
		dependency>

		<dependency>
			<groupId>org.slf4jgroupId>
			<artifactId>slf4j-log4j12artifactId>
			<version>1.7.25version>
		dependency>

application.yml

使用redis集群模式配置

server:
  port: 8080

spring:
  redis:
    database: 0
    timeout: 3000
    password: admin
    cluster:
      nodes: 192.168.75.200:7001,192.168.75.200:7002,192.168.75.201:7001,192.168.75.201:7002,192.168.75.202:7001,192.168.75.202:7002
    lettuce:
      pool:
        max-idle: 50
        min-idle: 10
        max-active: 100
        max-wait: 1000

3、编写Controller

package com.xj;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {

    Logger log = LoggerFactory.getLogger(TestController.class);

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @RequestMapping("/decr_number")
    public String decrStock(){
        int stock_number = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock_number")); // jedis.get("stock_number")
        if (stock_number > 0) {
            int remain_stock = stock_number - 1;
            stringRedisTemplate.opsForValue().set("stock_number", remain_stock + ""); // jedis.set(key,value)
            log.info("减库存成功,剩余库存:" + remain_stock);
            return "减库存成功,剩余库存:" + remain_stock;
        } else {
            log.error("减库存失败,库存不足");
            return "减库存失败,库存不足";
        }
    }
}

4、JMeter模拟高并发场景

设置500个并发
Redis分布式锁实战_第1张图片
HTTP请求
Redis分布式锁实战_第2张图片
日志打印结果:
Redis分布式锁实战_第3张图片
从上面的结果打印可知,重复出现剩余相同库存现象,发生了并发问题,引起了超卖现象

二、使用并发锁存在的问题

针对上面出现的并发问题,我们首先可以想到用synchronized或者Lock锁来解决,例如controller代码可以改成如下形式:

@RequestMapping("/decr_number")
    public String decrStock(){
        synchronized (this){
            int stock_number = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock_number")); // jedis.get("stock_number")
            if (stock_number > 0) {
                int remain_stock = stock_number - 1;
                stringRedisTemplate.opsForValue().set("stock_number", remain_stock + ""); // jedis.set(key,value)
                log.info("减库存成功,剩余库存:" + remain_stock);
                return "减库存成功,剩余库存:" + remain_stock;
            } else {
                log.error("减库存失败,库存不足");
                return "减库存失败,库存不足";
            }
        }
    }

使用Jmeter模拟压测验证发生问题已解决

遗留问题

使用并发锁只能解决单实例下的并发问题,对于在集群多实例情况下还是存在分布式并发问题,此种问题只能依靠分布式锁来进行解决。

三、Redis分布式问题解决方案

1、SETNX

@RequestMapping("/decr_number_setnx")
    public String decrStock_1(){
        String lock_key = "product_1000";
        String client_id = UUID.randomUUID().toString();
        try{
            //抢锁:key不存在设置并设置超时时间
            //设置超时时间是为了防止死锁
            //setnx和expire指令一起执行保证原子性
            Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lock_key, client_id, 10, TimeUnit.SECONDS);
            //没抢到锁,返回友好提示
            if(!result){
                log.error("系统繁忙,请稍后再试");
                return "系统繁忙,请稍后再试";
            }
            //抢到锁,执行业务逻辑
            int stock_number = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock_number"));
            if (stock_number > 0) {
                int remain_stock = stock_number - 1;
                stringRedisTemplate.opsForValue().set("stock_number", remain_stock + "");
                log.info("减库存成功,剩余库存:" + remain_stock);
                return "减库存成功,剩余库存:" + remain_stock;
            } else {
                log.error("减库存失败,库存不足");
                return "减库存失败,库存不足";
            }
        } catch (Exception e){
            log.error("Exception:",e);
        } finally {
            //解决锁失效的问题:
            //1、自己的锁自己释放
            //2、当一个线程A执行花费的时间大于设置的过期时间,导致锁失效,此时其它线程B过来加锁成功,然后线程A执行完毕删除了线程B设置的锁,之后经常重复前面的现象
            if (client_id.equals(stringRedisTemplate.opsForValue().get(lock_key))) {
                stringRedisTemplate.delete(lock_key);
            }
        }
        return "error";
    }

对于这种方案,一般高并发方案下可以选择使用,但是无法解决某些线程执行时间超过设置的超时时间,导致其它线程乘虚而入获得锁,即没有实现锁续命的问题,对于锁续命可以使用Redisson。

2、Redisson分布式锁

加锁实现原理

Redis分布式锁实战_第4张图片
从上图可以看出redission提供了锁续命的功能,比SETNX方案更完善。

SpringBoot集成Redission

pom.xml

		<dependency>
			<groupId>org.redissongroupId>
			<artifactId>redissonartifactId>
			<version>3.6.5version>
		dependency>

Redission加入Bean容器

@Bean
    public Redisson redisson() {
        Config config = new Config();
        //集群模式
        if (redisProperties.getCluster() != null) {
            //集群模式配置
            List<String> nodes = redisProperties.getCluster().getNodes();

            List<String> clusterNodes = new ArrayList<>();
            for (int i = 0; i < nodes.size(); i++) {
                clusterNodes.add("redis://" + nodes.get(i));
            }
            ClusterServersConfig clusterServersConfig = config.useClusterServers()
                    .addNodeAddress(clusterNodes.toArray(new String[clusterNodes.size()]));

            if (!StringUtils.isEmpty(redisProperties.getPassword())) {
                clusterServersConfig.setPassword(redisProperties.getPassword());
            }
        } else {
            //单节点配置
            String address = "redis://" + redisProperties.getHost() + ":" + redisProperties.getPort();
            SingleServerConfig serverConfig = config.useSingleServer();
            serverConfig.setAddress(address);
            if (!StringUtils.isEmpty(redisProperties.getPassword())) {
                serverConfig.setPassword(redisProperties.getPassword());
            }
            serverConfig.setDatabase(redisProperties.getDatabase());
        }
        //看门狗的锁续期时间,默认30000ms,这里配置成15000ms
        //config.setLockWatchdogTimeout(15000);
        return (Redisson) Redisson.create(config);
    }

controller注入

@Autowired
private Redisson redisson;

加锁逻辑

@RequestMapping("/decr_number_redission")
    public String decrStock_2(){
        String lock_key = "product_1000";
        RLock lock = redisson.getLock(lock_key);
        try{
            //加锁
            lock.lock();
            //抢到锁,执行业务逻辑
            int stock_number = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock_number"));
            if (stock_number > 0) {
                int remain_stock = stock_number - 1;
                stringRedisTemplate.opsForValue().set("stock_number", remain_stock + "");
                log.info("减库存成功,剩余库存:" + remain_stock);
                return "减库存成功,剩余库存:" + remain_stock;
            } else {
                log.error("减库存失败,库存不足");
                return "减库存失败,库存不足";
            }
        } catch (Exception e){
            log.error("Exception:",e);
        } finally {
            //释放锁
            lock.unlock();
        }
        return "error";
    }

3、Redisson底层实现原理

看门狗的续期时间

在config对象里面初始化为30s

	@Bean
    public Redisson redisson() {
        Config config = new Config();
    }
public Config() {
        this.transportMode = TransportMode.NIO;
        #默认续期时间30s
        this.lockWatchdogTimeout = 30000L;
        this.keepPubSubOrder = true;
        this.addressResolverGroupFactory = new DnsAddressResolverGroupFactory();
}

RLock#lock()

RLock为一个接口,继承了Lock

public interface RLock extends Lock, RExpirable, RLockAsync {
    void lockInterruptibly(long var1, TimeUnit var3) throws InterruptedException;

    boolean tryLock(long var1, long var3, TimeUnit var5) throws InterruptedException;

    void lock(long var1, TimeUnit var3);

    void forceUnlock();

    boolean isLocked();

    boolean isHeldByCurrentThread();

    int getHoldCount();
}

lock()方法

public interface Lock {
    void lock();
    .......
}

lock()的实现在RedissonLock类中:

public void lock() {
        try {
        	//调用带中断的lock方法
            this.lockInterruptibly();
        } catch (InterruptedException var2) {
        	//捕获中断异常,设置中断标志,业务线程自行停止线程
            Thread.currentThread().interrupt();
        }
}

会继续调用lockInterruptibly()方法,获取锁,不成功则订阅释放锁的消息,获得消息前阻塞,得到释放通知后再去循环获取锁。

public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
		//获取当前线程ID
        long threadId = Thread.currentThread().getId();
        //加锁操作失败,返回锁的剩余超时时间;返回null,说明加锁成功
        Long ttl = this.tryAcquire(leaseTime, unit, threadId);
        //其它线程加锁失败,继续执行
        if (ttl != null) {
            RFuture<RedissonLockEntry> future = this.subscribe(threadId);
            this.commandExecutor.syncSubscription(future);

            try {
            	//自旋加锁
                while(true) {
                    ttl = this.tryAcquire(leaseTime, unit, threadId);
                    //说明加锁成功,自旋结束
                    if (ttl == null) {
                        return;
                    }
					//等待一段时间,继续执行
                    if (ttl >= 0L) {
                        this.getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                    } else {
                        this.getEntry(threadId).getLatch().acquire();
                    }
                }
            } finally {
                this.unsubscribe(future, threadId);
            }
        }
    }

private Long tryAcquire(long leaseTime, TimeUnit unit, long threadId) {
        return (Long)this.get(this.tryAcquireAsync(leaseTime, unit, threadId));
}

tryAcquireAsync方法:

private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {
		#如果设置了超时时间,直接调用 tryLockInnerAsync
        if (leaseTime != -1L) {
            return this.tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
        } else {
        	##如果leaseTime==-1,则默认超时时间为30s
            RFuture<Long> ttlRemainingFuture = this.tryLockInnerAsync(this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
            ttlRemainingFuture.addListener(new FutureListener<Long>() {
                public void operationComplete(Future<Long> future) throws Exception {
                    if (future.isSuccess()) {
                        Long ttlRemaining = (Long)future.getNow();
                        if (ttlRemaining == null) {
                        	#实现锁续命
                            RedissonLock.this.scheduleExpirationRenewal(threadId);
                        }

                    }
                }
            });
            return ttlRemainingFuture;
        }
    }

tryLockInnerAsync方法:

<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        this.internalLockLeaseTime = unit.toMillis(leaseTime);
        /**
        *执行lua脚本,加锁操作
        *1、exists判断锁key是否存在
        *2、不存在,hset添加锁key,hash结构
        *3、pexpire设置锁key的过期时间
        *4、同线程hexists查看哈希表的指定字段是否存在(如果哈希表含有给定字段,返回1)
        *5、存在,则执行hincrby指令,实现可重入锁
        *6、再次执行pexpire刷新过去时间
        *7、其它线程两个判断都没有执行,最后执行pttl返回剩余过期时间
        **/
        return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, command, "if (redis.call('exists', KEYS[1]) == 0) then 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]);", Collections.singletonList(this.getName()), new Object[]{this.internalLockLeaseTime, this.getLockName(threadId)});
    }

scheduleExpirationRenewal方法,每到过期时间1/3就去重新刷一次,如果key不存在则停止刷新:

private void scheduleExpirationRenewal(final long threadId) {
        if (!expirationRenewalMap.containsKey(this.getEntryName())) {
        	//TimerTask实现定时任务
            Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
                public void run(Timeout timeout) throws Exception {
                	//执行lua脚本实现锁续命
                    RFuture<Boolean> future = RedissonLock.this.commandExecutor.evalWriteAsync(RedissonLock.this.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.singletonList(RedissonLock.this.getName()), new Object[]{RedissonLock.this.internalLockLeaseTime, RedissonLock.this.getLockName(threadId)});
                    future.addListener(new FutureListener<Boolean>() {
                        public void operationComplete(Future<Boolean> future) throws Exception {
                            RedissonLock.expirationRenewalMap.remove(RedissonLock.this.getEntryName());
                            if (!future.isSuccess()) {
                                RedissonLock.log.error("Can't update lock " + RedissonLock.this.getName() + " expiration", future.cause());
                            } else {
                                if ((Boolean)future.getNow()) {
                                    RedissonLock.this.scheduleExpirationRenewal(threadId);
                                }

                            }
                        }
                    });
                }
            }, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS);
            if (expirationRenewalMap.putIfAbsent(this.getEntryName(), task) != null) {
                task.cancel();
            }

        }
    }

RLock#unlock()

解锁入口:

public void unlock() {
        Boolean opStatus = (Boolean)this.get(this.unlockInnerAsync(Thread.currentThread().getId()));
        if (opStatus == null) {
            throw new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: " + this.id + " thread-id: " + Thread.currentThread().getId());
        } else {
            if (opStatus) {
                this.cancelExpirationRenewal();
            }

        }
    }

unlockInnerAsync方法,同样也是lua脚本保证解锁操作的原子性:

protected RFuture<Boolean> unlockInnerAsync(long threadId) {
        return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, "if (redis.call('exists', KEYS[1]) == 0) then 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); 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.asList(this.getName(), this.getChannelName()), new Object[]{LockPubSub.unlockMessage, this.internalLockLeaseTime, this.getLockName(threadId)});
    }

四、最终遗留问题

Redis是典型的AP模型,也就是保证高可用,和数据的最终一致性。在实现分布式锁时,可能会出现线程A设置完锁后,master挂掉,slave提升为master,因为异步复制的特性(主从之间的数据同步),线程A设置的锁丢失了,这时候线程B设置锁也能够成功,导致线程A和B同时拥有锁。
针对这个问题,redission提供了redlock(超过半数节点加锁成功,才算加锁成功)来解决这个问题。


你可能感兴趣的:(分布式,redis,分布式,java)