基于redis手写一个分布式锁

简介

分布式锁,名字就看的出来是应用于分布式系统中的锁,我们平时熟知的synchronized等一系列锁是在一个jvm内实现的,而很多大型的系统通常都是采用分布式部署开发,不在一个jvm内运行所以synchronized等锁就失效了,分布式锁就是解决分布式系统中锁的问题,下面我们来手写一个基于redis的分布式锁来了解一下分布式锁的原理。本文源码地址:https://github.com/itwwj/frame.git 中的redis项目,其他的项目请忽略。

一、分布式锁原理

  • 互斥性
  • 高可用性
  • 可重入性
  • 阻塞
  • 锁超时

二、java实现redis的分布式锁

依赖:

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
            <version>2.8.1</version>
        </dependency>
  </dependencies>

配置文件application.yml:

spring:
  redis:
    host: 192.168.1.177 #服务器地址
    password: root # 密码
    port: 6379 #端口
    database: 0 #数据库索引默认为0
    lettuce:
      pool:
        max-idle: 10
        min-idle: 10
        max-active: 100

定义分布式锁接口:

public interface DistributedLock {

    /**
     * 默认超时时间
     * 单位:毫秒
     */
    long TIMEOUT_MILLIS = 5000;

    /**
     * 重试次数
     */
    int RETRY_TIMES = 100;

    /**
     * 每次重试后等待的时间
     * 单位:毫秒
     */
    long SLEEP_MILLIS = 100;

    /**
     * 获取锁
     *
     * @param key key
     * @return 成功/失败
     */
    default boolean lock(String key) {
        return lock(key, TIMEOUT_MILLIS, RETRY_TIMES, SLEEP_MILLIS);
    }

    /**
     * 获取锁
     *
     * @param key        key
     * @param retryTimes 重试次数
     * @return 成功/失败
     */
    default boolean lock(String key, int retryTimes) {
        return lock(key, TIMEOUT_MILLIS, retryTimes, SLEEP_MILLIS);
    }

    /**
     * 获取锁
     *
     * @param key         key
     * @param retryTimes  重试次数
     * @param sleepMillis 获取锁失败的重试间隔 单位:毫秒
     * @return 成功/失败
     */
    default boolean lock(String key, int retryTimes, long sleepMillis) {
        return lock(key, TIMEOUT_MILLIS, retryTimes, sleepMillis);
    }

    /**
     * 获取锁
     *
     * @param key    key
     * @param expire 获取锁超时时间
     * @return 成功/失败
     */
    default boolean lock(String key, long expire) {
        return lock(key, expire, RETRY_TIMES, SLEEP_MILLIS);
    }

    /**
     * 获取锁
     *
     * @param key        key
     * @param expire     获取锁超时时间
     * @param retryTimes 重试次数
     * @return 成功/失败
     */
    default boolean lock(String key, long expire, int retryTimes) {
        return lock(key, expire, retryTimes, SLEEP_MILLIS);
    }

    /**
     * 获取锁
     *
     * @param key         key
     * @param expire      获取锁超时时间
     * @param retryTimes  重试次数
     * @param sleepMillis 获取锁失败的重试间隔
     * @return 成功/失败
     */
    boolean lock(String key, long expire, int retryTimes, long sleepMillis);

    /**
     * 释放锁
     *
     * @param key key值
     * @return 释放结果
     */
    boolean releaseLock(String key);
}

redis分布式锁实现:

@Slf4j
public class RedisDistributedLock implements DistributedLock {
    private static final String UNLOCK_LUA;

    /*
     * 通过lua脚本释放锁,来达到释放锁的原子操作
     */
    static {
        UNLOCK_LUA = "if redis.call(\"get\",KEYS[1]) == ARGV[1] " +
                "then " +
                "    return redis.call(\"del\",KEYS[1]) " +
                "else " +
                "    return 0 " +
                "end ";
    }

    private final RedisTemplate<String, Object> redisTemplate;
    private final ThreadLocal<String> lockFlag = new ThreadLocal<>();

    public RedisDistributedLock(RedisTemplate<String, Object> redisTemplate) {
        super();
        this.redisTemplate = redisTemplate;
    }

    @Override
    public boolean lock(String key, long expire, int retryTimes, long sleepMillis) {
        boolean result = setRedis(key, expire);
        // 如果获取锁失败,按照传入的重试次数进行重试
        while (!result && retryTimes-- > 0) {
            try {
                log.debug("get redisDistributeLock failed, retrying..." + retryTimes);
                Thread.sleep(sleepMillis);
            } catch (InterruptedException e) {
                log.warn("Interrupted!", e);
                Thread.currentThread().interrupt();
            }
            result = setRedis(key, expire);
        }
        return result;
    }

    private boolean setRedis(final String key, final long expire) {
        //可重入性锁
        if (lockFlag.get() != null && lockFlag.get().equals(redisTemplate.opsForValue().get(key))) {
            return true;
        }
        try {
            return redisTemplate.execute((RedisCallback<Boolean>) connection -> {
                String uuid = UUID.randomUUID().toString();
                lockFlag.set(uuid);
                byte[] keyByte = redisTemplate.getStringSerializer().serialize(key);
                byte[] uuidByte = redisTemplate.getStringSerializer().serialize(uuid);
                return (boolean) connection.set(keyByte, uuidByte, Expiration.from(expire, TimeUnit.MILLISECONDS),
                        RedisStringCommands.SetOption.ifAbsent());
            });
        } catch (Exception e) {
            log.error("设置redis锁发生异常", e);
        }
        return false;
    }

    @Override
    public boolean releaseLock(String key) {
        // 释放锁的时候,有可能因为持锁之后方法执行时间大于锁的有效期,此时有可能已经被另外一个线程持有锁,所以不能直接删除
        try {
            // 使用lua脚本删除redis中匹配value的key,可以避免由于方法执行时间过长而redis锁自动过期失效的时候误删其他线程的锁
            // spring自带的执行脚本方法中,集群模式直接抛出不支持执行脚本的异常,所以只能拿到原redis的connection来执行脚本
            return redisTemplate.execute((RedisCallback<Boolean>) connection -> {
                byte[] scriptByte = redisTemplate.getStringSerializer().serialize(UNLOCK_LUA);
                return connection.eval(scriptByte, ReturnType.BOOLEAN, 1
                        , redisTemplate.getStringSerializer().serialize(key)
                        , redisTemplate.getStringSerializer().serialize(lockFlag.get()));
            });
        } catch (Exception e) {
            log.error("释放redis锁发生异常", e);
        } finally {
            lockFlag.remove();
        }
        return false;
    }
}

注册bean:

@Configuration
public class RedisConfig {
    /**
     * 分布式锁
     *
     * @param redisTemplate redis
     * @return 分布式锁
     */
    @Bean
    @ConditionalOnMissingBean
    public DistributedLock redisDistributedLock(RedisTemplate redisTemplate) {
        return new RedisDistributedLock(redisTemplate);
    }
}

测试:

这里我们使用jMeter来进行并发测试
先来测试没有使用分布式锁的代码

    @RequestMapping("test")
    public void testLock() {
        if (testint > 0) {
            testint = testint - 1;
            log.info("testint          :" + testint);
        }
    }

在这里插入图片描述
可以看出没有加锁会出现线程问题
接下来我们测试一下自己手写的分布式锁:

@Slf4j
@RestController
@RequiredArgsConstructor
public class DistributedLockController {

    private final DistributedLock distributedLock;
    private static Integer testint = 10;
    private static final String KEY = "test";

    @RequestMapping("distributedLock")
    public void distributeLock() {
        try {
            boolean test = distributedLock.lock(KEY);
            if (test && testint > 0) {
                testint = testint - 1;
                log.info("testint          :" + testint);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            distributedLock.releaseLock(KEY);
        }
    }
}

在这里插入图片描述
这次的数据就规矩多了,手写分布式锁成功。

三、使用redisson实现分布式锁

项目中通常是不会使用手写的分布式锁,自己手写只是了解分布式锁的原理,当然要是不怕填坑可以冒险尝试,下面我们使用一下redisson的分布式锁
引入依赖:

        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
        </dependency>

配置文件application.yml:

spring:
  redis:
    host: 192.168.1.177 #服务器地址
    password: root # 密码
    port: 6379 #端口
    database: 0 #数据库索引默认为0
    lettuce:
      pool:
        max-idle: 10
        min-idle: 10
        max-active: 100
redisson:
  address: redis://${spring.redis.host}:${spring.redis.port}
  password: ${spring.redis.password}

配置类:


/**
 * @author jie
 */
@Data
@ConfigurationProperties(prefix = "redisson")
public class RedissonProperties {

    private int timeout = 3000;

    private String address;

    private String password;

    private int database = 0;

    private int connectionPoolSize = 64;

    private int connectionMinimumIdleSize = 10;

    private int slaveConnectionPoolSize = 250;

    private int masterConnectionPoolSize = 250;
    
}

bean配置:

   /**
 * @author jie
 */
@EnableCaching
@Configuration
@ConditionalOnClass(RedisConnectionFactory.class)
@EnableConfigurationProperties({RedisProperties.class, RedissonProperties.class})
public class RedisConfig {

    /**
     * key 的生成策略
     */
    @Bean
    public KeyGenerator keyGenerator() {
        return (target, method, objects) -> {
            StringBuilder sb = new StringBuilder();
            sb.append(target.getClass().getName());
            sb.append(COLON);
            sb.append(method.getName());
            for (Object obj : objects) {
                if (obj != null) {
                    sb.append(COLON);
                    sb.append(obj.toString());
                }
            }
            return sb.toString();
        };
    }

    /**
     * 用于 @Cacheable 相关注解
     *
     * @param redisConnectionFactory 链接工厂
     * @return 缓存管理器
     */
    @Bean(name = "cacheManager")
    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        RedisCacheConfiguration defConfig = getDefConf();
        return RedisCacheManager.builder(redisConnectionFactory)
                .cacheDefaults(defConfig)
                .build();
    }

    private RedisCacheConfiguration getDefConf() {
        RedisCacheConfiguration def = RedisCacheConfiguration.defaultCacheConfig()
                .disableCachingNullValues()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new RedisObjectSerializer()));
        def.entryTtl(Duration.ofDays(1));
        def.computePrefixWith(cacheName -> cacheName.concat(COLON));
        return def;
    }


    /**
     * RedisTemplate配置
     *
     * @param factory redis链接工厂
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        setSerializer(factory, template);
        return template;
    }

    private void setSerializer(RedisConnectionFactory factory, RedisTemplate template) {
        RedisObjectSerializer redisObjectSerializer = new RedisObjectSerializer();
        RedisSerializer stringSerializer = new StringRedisSerializer();
        template.setKeySerializer(stringSerializer);
        template.setHashKeySerializer(stringSerializer);
        template.setHashValueSerializer(redisObjectSerializer);
        template.setValueSerializer(redisObjectSerializer);
        template.setConnectionFactory(factory);
    }

    /**
     * stringRedisTemplate 配置
     *
     * @param factory
     * @return
     */
    @Bean
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
        StringRedisTemplate template = new StringRedisTemplate();
        setSerializer(factory, template);
        return template;
    }

    @Bean
    @ConditionalOnMissingBean
    public RedisUtils getRedisOps(RedisTemplate redisTemplate) {
        return new RedisUtils(redisTemplate, true);
    }

    @Bean
    @ConditionalOnMissingBean
    public SuperBaseRedisOps redisPlusOps(RedisUtils redisUtils) {
        return new RedisOpsImpl(redisUtils);
    }

    /**
     * 分布式锁(自己实现的)
     *
     * @param redisTemplate redis
     * @return 分布式锁
     */
    // @Bean
    // @ConditionalOnMissingBean
    public DistributedLock redisDistributedLock(RedisTemplate redisTemplate) {
        return new RedisDistributedLock(redisTemplate);
    }

    /**
     * 分布式锁(redisson的)
     *
     * @param redissonClient redis
     * @return 分布式锁
     */
    @Bean
    @ConditionalOnMissingBean
    public DistributedLock redisDistributedLock(RedissonClient redissonClient) {
        return new RedissonDistributedLock(redissonClient);
    }

    @Bean
    @ConditionalOnMissingBean
    public RedissonClient getredisson(RedissonProperties redissonProperties) {
        Config config = new Config();
        SingleServerConfig serverConfig = config.useSingleServer()
                .setAddress(redissonProperties.getAddress())
                .setTimeout(redissonProperties.getTimeout())
                .setConnectionPoolSize(redissonProperties.getConnectionPoolSize())
                .setConnectionMinimumIdleSize(redissonProperties.getConnectionMinimumIdleSize());
        if (StrUtil.isNotBlank(redissonProperties.getPassword())) {
            serverConfig.setPassword(redissonProperties.getPassword());
        }
        return Redisson.create(config);
    }
}

测试:
由于我们只改变了实现,所以测试代码不需要修改(体现出了设计模式的强大!)

@Slf4j
@RestController
@RequiredArgsConstructor
public class DistributedLockController {

    private final DistributedLock distributedLock;
    private static Integer testint = 20;
    private static final String KEY = "test";

    @RequestMapping("distributedLock")
    public void distributeLock() {
        try {
            boolean test = distributedLock.lock(KEY);
            if (test && testint > 0) {
                testint = testint - 1;
                log.info("testint          :" + testint);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            distributedLock.releaseLock(KEY);
        }
    }

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值