springBoot整合Redis(四、整合redis 实现分布式锁)

        在单机环境,我们使用最多的是juc包里的单机锁,但是随着微服务分布式项目的普及,juc里的锁是不能控制分布锁环境的线程安全的,因为单机锁只能控制同个进程里的线程安全,不能控制多节点的线程安全,所以就需要使用分布式锁

        分布式锁的原理就不说太多了,主要讲讲基于redis的分布式锁的用法。

一、redis分布式锁原理

        实际上不管在java中怎么实现redis锁,其实底层都是使用 redis的setnxexpire 命令 来实现的。命令行操作如下:

setnx命令:
        SETNX是SET if not exists的简写,设置key的值,如果key值不存在,则可以设置,否则不可以设置,这个有点像juc中cas锁的原理

# setnx命令,相当于set和nx命令一起用
setnx tkey aaa

EX : 设置指定的到期时间(以秒为单位)。

PX : 设置指定的到期时间(以毫秒为单

NX : 仅在键不存在时设置键。

XX : 只有在键已存在时才设置。

expire命令:
        如果只使用setnx不加上过期时间,手动释放锁时候出现异常,就会导致一直解不了锁,所以还是要加上expire命令来设置过期时间。

  • 保证原子性

但是又有一个问题,设置过期时间时候报错了,也同样会导致锁释放不了,所以为了保证原子性,需要这两个命令一起执行

# set tkey过期时间10秒,nx:如果键不存在时设置
set tkey aaa ex 10 nx

二、Jedis 实现分布式锁 

        1)常用命令:

         如果不存在就获取锁 ,获取成功返回 1 : jedis.setnx("lock", "1")
         设置锁的过期时间: jedis.expire("lock", 10);
         删除锁:jedis.del("lock");

        也可以使用set方法:jedis.set(String key, String value, String nxxx, String expx, int time)
         例如:jedis.set(key, val, "NX", "PX", expireTime); 成功返回 OK 。
        jedis高版本得依赖 使用这个api::set(String key, String value, SetParams params)
        效果是一样的,使用方式: jedis.set("lock", "1", new SetParams().nx().ex(10L)))
        一般建议使用下面得方式。

        2)简单实现:
public static void main(String[] args) {

        
        for(int i = 0;i < 10 ;i++ ){
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {

                    Boolean b = true;
                    while (b) {
                        //连接本地的 Redis 服务
                        Jedis jedis = new Jedis("",6379,0);
                        // 如果 Redis 服务设置了密码,需要用下面这行代码输入密码
                        jedis.auth("");
//                        System.out.println("连接成功");
//                        //查看服务是否运行
//                        System.out.println("服务正在运行: "+jedis.ping());


                        if (jedis.setnx("lock", "1") == 1) {
                            // 获取锁成功
                            jedis.expire("lock", 10); // 设置锁的过期时间
                            System.out.println("2222222222222");
                            b= false;
                            jedis.del("lock");
                        }

                    }

                }
            });

            thread.start();

        }

有很多封装好的工具类,网上可以自行搜索一下。

三、基于RedisTemplate 实现分布式锁 

1)常用命令:
    获取锁以及设置锁过期时间:
        redisTemplate.opsForValue().setIfAbsent("lockKey", "lockValue", 10, TimeUnit.SECONDS)
    删除锁:
        redisTemplate.opsForValue().getOperations().delete("lockKey");

 2) 简单使用:
        redis配置:

@Configuration
@Slf4j
public class RedisConfig  {

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        
        RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration("localhost", 6379);
        redisStandaloneConfiguration.setPassword(RedisPassword.of("123456"));

        LettuceClientConfiguration clientConfiguration = LettuceClientConfiguration.builder()
                .clientOptions(ClientOptions.builder().timeoutOptions(TimeoutOptions.enabled()).build())
                .build();

        return new LettuceConnectionFactory(redisStandaloneConfiguration, clientConfiguration);

    }

    /**
     * 通用redisTemplate采用GenericJackson2JsonRedisSerializer序列化value
     * @param lettuceConnectionFactory
     * @return
     */
    @Bean
    @Resource
    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(lettuceConnectionFactory);
        // 设置key的序列化方式,采用StringRedisSerializer
        GenericToStringSerializer<String> keySerializer = new GenericToStringSerializer<>(String.class);
        redisTemplate.setKeySerializer(keySerializer);
        redisTemplate.setHashKeySerializer(keySerializer);
        // 设置value的序列化方式,采用Jackson2JsonRedisSerializer
        GenericJackson2JsonRedisSerializer valueSerializer = new GenericJackson2JsonRedisSerializer();
        redisTemplate.setValueSerializer(valueSerializer);
        redisTemplate.setHashValueSerializer(valueSerializer);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
    
}

简单调用方法:

public static void main(String[] args) {

        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(RedisConfig.class);

        // 获取 RedisTemplate Bean
        RedisTemplate<String, String> redisTemplate = context.getBean(RedisTemplate.class);

        for(int i = 0;i < 10 ;i++ ){
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {

                    Boolean b = true;
                    while (b) {
                        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(RedisConfig.class);

                        // 获取 RedisTemplate Bean
                        RedisTemplate<String, String> redisTemplate = context.getBean(RedisTemplate.class);

                        if (redisTemplate.opsForValue().setIfAbsent("aaa", "1", 10, TimeUnit.SECONDS)) {
                            System.out.println("2222222222222");
                            b= false;
                        }
                        redisTemplate.opsForValue().getOperations().delete("aaa");
                        // 关闭应用上下文
                        context.close();
                    }

                }
            });
            thread.start();
        }

    }

简单使用封装类:

@Slf4j
@Service
@RequiredArgsConstructor
public class RedisLockService {


    private final StringRedisTemplate stringRedisTemplate;

    //获取尝试时间
    private final long EXPIRE_TIME = 5 * 1000;

    /**
     * 加锁
     * @param key   String key = "dec_store_lock_" + "eth";
     * @param value 当前时间+超时时间   long time = System.currentTimeMillis() + TIMOUT;
     * @return
     */
    public boolean tryLock(String key, String value) {
        if (stringRedisTemplate.opsForValue().setIfAbsent(key, value, EXPIRE_TIME, TimeUnit.SECONDS)) {
            return true;
        }
        //currentValue=A 这两个线程的value都是B 其中一个线程拿到锁
        String currentValue = stringRedisTemplate.opsForValue().get(key);
        //如果锁过期
        if (!StringUtils.isEmpty(currentValue) && Long.parseLong(currentValue) < System.currentTimeMillis()) {
            //获取上一个锁的时间 如果高并发的情况可能会出现已经被修改的问题  所以多一次判断保证线程的安全
            String oldValue = stringRedisTemplate.opsForValue().getAndSet(key, value);
            if (!StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue)) {
                return true;
            }
        }
        return false;
    }


    /**
     * 加锁
     * @param key   String key = "dec_store_lock_" + "eth";
     * @param value 当前时间+超时时间   long time = System.currentTimeMillis() + TIMOUT;
     * @return
     */
    public boolean tryLock(String key, String value,long expireTime,long waitTime) {
        long nowTime = System.currentTimeMillis();
        while ((System.currentTimeMillis()) - nowTime < waitTime){
            if (Boolean.TRUE.equals(stringRedisTemplate.opsForValue().setIfAbsent(key, value, expireTime, TimeUnit.MILLISECONDS))) {
                return true;
            }
        }
        return false;
    }

    /**
     * 解锁
     * @param key
     * @param value
     * @return
     */
    public void unlock(String key, String value) {
        String currentVaule = stringRedisTemplate.opsForValue().get(key);
        try {
            if (!StringUtils.isEmpty(currentVaule) && currentVaule.equals(value)) {
                stringRedisTemplate.opsForValue().getOperations().delete(key);
            }
        } catch (Exception e) {
            log.error("解锁失败 ",e);
        }

    }
}

四、基于lua脚本实现分布式锁

        在使用分布式锁时,需要注意的情况就是不同的线程删除的了相同的锁,在多线程情况下,就会造成逻辑上的混乱,所以我们解决这种情况的方法就是,不同的线程,使用不同的key。当然有些业务就是要使用相同的key。所以在删除锁的时候我们最好能使用lua脚本的方式,这样在删除的时候是原子性的,就能避免这种情况。

        

Lua脚本(unlock.lua)

if(redis.call('get', KEYS[1]) == ARGV[1]) then
    return redis.call('del', KEYS[1])
end
return 0

 调用代码:

import cn.hutool.core.lang.UUID;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.util.Collections;
import java.util.concurrent.TimeUnit;

public class SimpleRedisLock {
    private String name;
    private StringRedisTemplate stringRedisTemplate;
    private static final String KEY_PREFIX = "lock:";
    private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
    // 加载脚本
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;

    static {
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        // 加载工程resourcecs下的unlock.lua文件
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        UNLOCK_SCRIPT.setResultType(Long.class);
    }
    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }
    
    public boolean tryLock(long timeoutSec) {
        // 获取线程标识
        String threadId = ID_PREFIX +  Thread.currentThread().getId();
        // 获取锁
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);
    }

    public void unlock() {
        // 调用Lua脚本
        stringRedisTemplate.execute(UNLOCK_SCRIPT,
                Collections.singletonList(KEY_PREFIX + name),
                ID_PREFIX +  Thread.currentThread().getId());
    }
}

当前还存在的问题:

  • 不可重入:同一个线程无法多次获取同一把锁(线程1在执行方法1的时候调用了方法2,而方法2也需要锁)
  • 不可重试:获取锁只尝试一次就返回false,没有重试机制;当前直接返回结果。
  • 超时释放:锁超时释放虽然可以避免死锁,但是如果业务执行耗时较长,也会导致锁释放,存在安全隐患。
  • 主从一致性:如果redis提供了主从集群,主存同步存在延迟。当主结点宕机时,从节点尚未同步主结点锁数据,则会造成锁失效。

写的不是特别详细,需要的话可以找找比较详细的资料。平时工作中用到的次数不多,只做了解。

五、基于 Redisson 实现分布式锁

1、依赖:

<!-- 原生,本章使用-->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.13.6</version>
</dependency>
 
<!-- 另一种Spring集成starter,本章未使用 -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.13.6</version>
</dependency>

2、配置类:

@Configuration
public class RedissonConfig {

    /**
     * redis地址
     */
    @Value("${spring.redis.host}")
    private String redisHost;

    /**
     * redis端口号
     */
    @Value("${spring.redis.port}")
    private String redisPort;

    /**
     * redis密码
     */
    @Value("${spring.redis.password}")
    private String redisPassword;


    /**
     * redis的数据库编号
     */
    @Value("${spring.redis.database}")
    private Integer redisDatabase;



    @Bean
    public RedissonClient redissonClient() {
        // 创建 Redisson 客户端连接
        Config config = new Config();
        config.useSingleServer()//单机模式
                .setAddress("redis://"+redisHost+":"+redisPort)//redis服务器地址
                .setDatabase(redisDatabase)//指定数据库编号
                .setPassword(redisPassword)//redis密码
                .setConnectionMinimumIdleSize(10)//连接车最小空闲连接数
                .setConnectionPoolSize(50)//连接池最大线程数
                .setIdleConnectionTimeout(60000)//线程超时时间
                .setConnectTimeout(10000)//客户端程序获取redis链接的超时时间
                .setTimeout(10000);//响应超时时间
        return  Redisson.create(config);
    }
    
}

3、简单使用:

        常用api:

        1) RLock.lock()     直接加锁,解锁

        RLock lock1 = redissonClient.getLock(KEY_LOCKED);
        log.error("lock1 clas: {}", lock1.getClass());
        lock1.lock();//加锁
        // 处理业务逻辑
        // ........
        lock1.unlock();//解锁

        2)RLock.lock(long var1, TimeUnit var3);        加锁时增加过期时间,到时自动释放锁。

        RLock lock1 = redissonClient.getLock(KEY_LOCKED);
        // 500s 后自动释放锁
        lock1.lock(500, TimeUnit.SECONDS);
        try {
            Thread.sleep(TIME_LOCKED);
        } catch (InterruptedException ignore) {
            // ignore
        }

        3) RLock.tryLock(long time, TimeUnit unit)    尝试一定时间去获取锁,返回Boolean值,获取成功返回 true ,获取失败返回false。 一般用这个,不会直接加锁。

          RLock lock1 = redissonClient.getLock(KEY_LOCKED);
          boolean b = lock1.tryLock(7, TimeUnit.SECONDS);
          if (!b) {
              log.info(Thread.currentThread().getName() + " \t 获取锁失败");
              return;
          }
          log.info(Thread.currentThread().getName() + " \t 获取锁");
          lock1.unlock();

        4) RLock.tryLock(long var1, long var3, TimeUnit var5)     接收3个参数,第一个指定最长等待时间waitTime,第二个指定最长持有锁的时间 holdTime, 第三个是时间单位

        RLock lock1 = redissonClient.getLock(KEY_LOCKED);
        // 尝试获取锁7s, 最多占有锁2s,超过后自动释放,调用unlock可以提前释放。
        boolean b = lock1.tryLock(7, 2, TimeUnit.SECONDS);
        if (!b) {
             log.info(Thread.currentThread().getName() + " \t 获取锁失败");
             return;
         }


         // 如果是当前线程持有锁,手动释放
         if (lock1.isHeldByCurrentThread()) {
             lock1.unlock();
         }

        公平锁:

        我们使用  RLock lock1 = redissonClient.getLock(KEY_LOCKED);  直接获取的锁,默认的是非公平锁。
        如果我们要使用公平锁,也就是说先到先得的形式,可以使用下面的方式获取:
        RLock lock1 = redissonClient.getFairLock(KEY_LOCKED);
        其他操作一样。

       

        读写锁:

  • 多个线程可以同时读取共享资源,读取操作不会阻塞其他读取操作。
  • 写操作会阻塞所有读取操作和写操作,只有当没有线程在读取或写入时,写操作才能执行。
  • 读写锁适用于读操作远远多于写操作的场景,可以提高并发性能。

        也就是说读写锁分为 读 和 写,当多个线程都在读的时候,没有问题,都可以正常读,但是当有线程写的时候,就会锁住 读 和 写的所有操作。就是说你要是读随便读,但是如果一旦有写,那么其他的读和写都会被锁住。确保只有一个写在进行。

        1)获取一个读写锁实例: 

RWLockReadWriteLock rwLock = redisson.getReadWriteLock("myReadWriteLock");

        2)获取读锁和写锁

使用读写锁实例,可以分别获取读锁和写锁:

RLock readLock = rwLock.readLock();
RLock writeLock = rwLock.writeLock();

        3)示例代码

 private static void lock4() {
        for (int i = 0; i < 3; i++) {
            try {
                Thread.sleep(1 * 1000);
            } catch (InterruptedException e) {
            }

            new Thread(new Runnable() {
                @Override
                public void run() {
                    log.info(Thread.currentThread().getName() + " \t 运行");
                    RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(KEY_LOCKED);

                    readWriteLock.readLock().lock();
                    log.info(Thread.currentThread().getName() + " \t 获取读锁");
                    try {
                        // 模拟处理逻辑用时5s
                        Thread.sleep(5 * 1000);
                    } catch (InterruptedException e) {
                    }
                    readWriteLock.readLock().unlock();
                    log.info(Thread.currentThread().getName() + " \t 释放读锁");

                    readWriteLock.writeLock().lock();
                    log.info(Thread.currentThread().getName() + " \t 获取写锁");
                    try {
                        // 模拟处理逻辑用时5s
                        Thread.sleep(5 * 1000);
                    } catch (InterruptedException e) {
                    }
                    readWriteLock.writeLock().unlock();
                    log.info(Thread.currentThread().getName() + " \t 释放写锁");
                }
            }).start();
        }
    }

 会发现读锁可以同时获取,但是写锁只能有一个!

删除锁:

        一般我们不会直接去进行解锁操作  lock.unlock(); 会在解锁条件前先进行判断一下                Objects.nonNull(lock)确保lock不是nulllock.isLocked()检查锁是否被任何线程持有,而lock.isHeldByCurrentThread()检查锁是否被请求它的当前线程持有。

if (Objects.nonNull(lock) && lock.isLocked() && lock.isHeldByCurrentThread()) {
      lock.unlock();
}

六、Redisson 原理

        

redisson 官网中文地址:
目录 · redisson/redisson Wiki · GitHub

【Redis笔记】分布式锁及4种常见实现方法-CSDN博客

基于RedisTemplate的redis分布式锁, 以及注解实现_@redistemplete注解-CSDN博客

Redis搭配RedisTemplate实现分布式锁实战实例_redistemplate 分布式锁-CSDN博客

  • 8
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值