Redis实现分布式锁(非Redis集群)JAVA实现版本

Redis实现分布式锁(非Redis集群)JAVA实现版本

题外话

小弟第一次发表博客,水平有限,如有错误,请及时指出。

简介

本文是在redis单机前提下,保证分布式锁不会出现BUG;如果redis是集群,那么在极端情况下会出现锁失效;

  1. 设计思路
    根据redis中的setnx命令,可实现最简单的锁。setnx的返回值(0或1)可理解为是否加锁成功。
  2. setnx引起的问题
    1.假如定义锁的key为lock,那么当线程1获得锁后,该什么时候去删除锁?可在finally中删除锁。
    2.如果线程1出现某些异常,导致锁没有被删除,出现了死锁,该如何解决?设置锁的过期时间。
    3.如果锁的过期时间小于业务代码的执行时间,也就是说,代码还没执行完,锁就已经失效了,如何解决?可在获取锁后,启动一个线程去定时更新锁的过期时间。

代码

测试环境是springboot+redis。
注意:如果没有这个方法,请升级spring-data-redis的版本stringRedisTemplate.opsForValue().setIfAbsent(lockKey, id, 2, TimeUnit.SECONDS);

获取锁的代码,代码中包含了验证锁是否有效

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * 分布式锁
 */
@Component
public class RedisLock {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    //这个map的作用是:防止stringRedisTemplate.delete(lockKey);删除失败,导致更新时间的那个线程会一直存在,一直更新失效时间,从而导致死锁
    public volatile static Map<String, String> lockMap = new HashMap<>(1);
    public void test1(int i) {
        String lockKey = "lockKey";
        /*
            生成id,防止在高并发情况下被其他线程删除锁;其实这种误删除的情况只会存在键过期的情况下,
            但是锁的过期时间其实是会一直更新的,所以这里是防止锁失效,防范于未然。
            上面举例:假如在不更新过期时间的情况下,方法执行需要15s,但是键只存在10s,那么线程1在10s后就会释放锁,
            线程2加上锁以后,线程2执行5s时,线程1的finally中就会去释放锁,这样就会导致后面一系列的锁失效
         */
        String id = UUID.randomUUID().toString();

        try {
            boolean flag = false;
            int flagNum = 0;
            //循环拿锁,并且最多拿10次
            while (!flag/* && flagNum < 10*/) {
                flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, id, 3, TimeUnit.SECONDS);
                flagNum++;
            }
            //加锁成功
            if (flag) {
                //加锁成功后,在jvm中放一个加锁标记
                lockMap.put(id, id);
                //起一个线程去更新这个锁的过期时间
                ExecutorService threadPool = MyThreadPoolExecutor2.getThreadPool();
                threadPool.submit(new Runnable() {
                    @Override
                    public void run() {
                        boolean idFlag = true;
                        while (idFlag) {
                            try {
                                //定时检查锁是还是否存在;检查时间=过期时间的1/3
                                Thread.sleep(800);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                            //判断该线程是否还持有这把锁
                            //这里是个bug,判断是否持有锁和刷新锁时间,不是原子性的。应该用lua脚本写一起
                            idFlag = id.equals(stringRedisTemplate.opsForValue().get(lockKey)) && lockMap.get(id) != null;
                            if (idFlag) {
                                //更新锁的过期时间
                                stringRedisTemplate.expire(lockKey, 3, TimeUnit.SECONDS);
                                System.out.println("我更新了时间::" + i);
                                System.out.println("过期时间为::" + stringRedisTemplate.getExpire(lockKey));
                            }

                        }
                    }
                });

                System.out.println("线程::" + i + "::拿到锁了" + Thread.currentThread().getName());
                //这里写自己的业务 以下为测试数据
                String test = "test";
                String sum = stringRedisTemplate.opsForValue().get(test);
                stringRedisTemplate.opsForValue().set(test, String.valueOf(Integer.valueOf(sum) + 1));
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        } finally {
            lockMap.remove(id);
            //这里是个BUG,判断锁是否属于当前线程后,再去释放锁,两个步骤不是原子性的;
            //如果判断当前锁是属于当前线程的,但是刚判断完,当前线程的锁就自动释放了,然后再去执行delete,那是不是把别的线程的锁释放了呢
            if (id.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
                System.out.println("删除的id::" + id);
                stringRedisTemplate.delete(lockKey);
            }
        }
    }
}

测试入口

@RestController
public class RedisLockController {
    @Autowired
    private RedisLock redisLock;

    @RequestMapping("/test")
    public String test() {
        ExecutorService threadPool = MyThreadPoolExecutor.getThreadPool();
        for (int i = 0; i < 100; i++) {
            threadPool.submit(new Run(i,redisLock));
        }
        return "抢锁";
    }

}

相关类

import com.redis.utils.RedisLock;

/**
 * Created by Administrator on 2020/9/11.
 */
public class Run implements Runnable{

    private int i;
    private RedisLock redisLock;

    public Run(int i, RedisLock redisLock) {
        this.i = i;
        this.redisLock = redisLock;
    }

    @Override

    public void run() {
        /*try {
            Thread.sleep(2800);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }*/
        redisLock.test1(i);
    }
}

Redlock

该代码还未解决redis集群下产生的问题,如果redis主节点刚加好锁,在这时,主节点挂了,别的线程在从节点上也能获取到锁,因为主节点在挂的时候,加锁信息还未同步到从节点上。
解决方案:可用redlock。参考文档和视频:
https://www.cnblogs.com/rgcLOVEyaya/p/RGC_LOVE_YAYA_1003days.html
https://www.bilibili.com/video/BV1Vt4y1D7BH?p=5
总结一下:实现起来很复杂,如果真的需要做redis集群的分布式锁,还是用zk吧。

redisson实现分布式锁

redis分布式锁也可使用redisson,这是别人封装好的。
Redisson好像也没有解决主从集群情况下,主挂了出现的问题。
以下为单机redis加锁的测试代码

		<dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.10.4</version>
        </dependency>
@Component
public class RedisLock {

    @Autowired
    private Redisson redisson;
    
    public void redisson(int i) {
        String lockKey = "lockKey";
        RLock lock = redisson.getLock(lockKey);
        try {
        	//lock这里会阻塞,直到抢到锁为止。锁的默认过期时间为30s
            lock.lock();
            System.out.println("线程::" + i + "::拿到锁了" + Thread.currentThread().getName());
            //这里写自己的业务 以下为测试数据
            String test = "test";
            String sum = stringRedisTemplate.opsForValue().get(test);
            stringRedisTemplate.opsForValue().set(test, String.valueOf(Integer.valueOf(sum) + 1));
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        } finally {
            lock.unlock();
        }
    }

}
import org.redisson.Redisson;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

@Configuration
public class RedissonConfig {

    @Bean
    public Redisson redisson() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(0);
        return (Redisson) Redisson.create(config);
    }

}
@RestController
public class RedisLockController {
    @Autowired
    private RedisLock redisLock;

    @RequestMapping("/test")
    public String test() {
        ExecutorService threadPool = MyThreadPoolExecutor.getThreadPool();
        for (int i = 0; i < 100; i++) {
            threadPool.submit(new Run(i,redisLock));
        }
        return "抢锁";
    }

}
public class Run implements Runnable{

    private int i;
    private RedisLock redisLock;

    public Run(int i, RedisLock redisLock) {
        this.i = i;
        this.redisLock = redisLock;
    }

    @Override

    public void run() {
        /*try {
            Thread.sleep(2800);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }*/
        redisLock.redisson(i);
    }
}

总结

redis锁虽然迸发高,但是使用不如zk的分布式锁,接下来会研究zk的分布式锁。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

IT界的老菜鸟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值