redis的分布式锁

一.前言

在但进程中,我们可以用到synchronized、lock之类的同步操作去解决,但是对于分布式架构下多进程的情况下,如何做到跨进程的锁。就需要借助一些第三方手段来完成,本文介绍redis分布锁的合理使用.

二.相关介绍

线程锁:主要用来给方法、代码块加锁。当某个方法或代码使用锁,在同一时刻仅有一个线程执行该方法或该代码段。线程锁只在同一JVM中有效果,因为线程锁的实现在根本上是依靠线程之间共享内存实现的,比如synchronized是共享对象头,显示锁Lock是共享某个变量(state)。

进程锁:为了控制同一操作系统中多个进程访问某个共享资源,因为进程具有独立性,各个进程无法访问其他进程的资源,因此无法通过synchronized等线程锁实现进程锁。

三.开始使用

1.使用思路

(1)加锁
redis中有一个setNx命令,这个命令只有在key不存在的情况下为key设置值,并同时设置失效时间,写值成功即为加锁成功。
(2)解锁
删除redis上的key对应value的数据,要保证获取数据、判断一致以及删除数据三个操作是原子的。

Lua脚本:
当客户端完成了对共享资源的操作之后,执行下面的Redis Lua脚本来释放锁

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end
2.注意事项

(1) 为了避免死锁 需要设设置一个失效时间,时间一般为执行业务时间的2倍.
(2) 为了避免锁误删,需在加锁的时候通过生成一个随机值字符串来当成value加锁.
(3)写入随机值与设置失效时间要同时进行,保证加锁的原子性。

3.使用示例

本例使用springboot框架

1.声明redis操作接口RedisServiceInter

public interface RedisServiceInter {
 	boolean setNx(String key,String value,long time);
    boolean releaseLock(String key,String value);//lu脚本
}

2.接口实现类RedisServiceImpl

@Service
public class RedisServiceImpl implements RedisServiceInter {
    /**
     * @Author lss0555
     * @Description  setNx操作
     **/
    @Override
    public boolean setNx(String key,String value, long time) {
        try {
            RedisCallback<String> callback = (connection) -> {
                JedisCommands commands = (JedisCommands) connection.getNativeConnection();
                return commands.set(key, value, "NX", "PX", time);
            };
            String result = redisTemplate.execute(callback);

            return !StringUtils.isEmpty(result);
        } catch (Exception e) {
            logger.error("set redis occured an exception", e);
        }
        return false;
    }

    //lu脚本,删除key
    @Override
    public boolean releaseLock(String key,String value) {
        // 释放锁的时候,有可能因为持锁之后方法执行时间大于锁的有效期,此时有可能已经被另外一个线程持有锁,所以不能直接删除
        try {
            List<String> keys = new ArrayList<>();
            keys.add(key);
            List<String> args = new ArrayList<>();
            args.add(value);
            // 使用lua脚本删除redis中匹配value的key,可以避免由于方法执行时间过长而redis锁自动过期失效的时候误删其他线程的锁
            // spring自带的执行脚本方法中,集群模式直接抛出不支持执行脚本的异常,所以只能拿到原redis的connection来执行脚本
            RedisCallback<Long> callback = (connection) -> {
                Object nativeConnection = connection.getNativeConnection();
                // 集群模式和单机模式虽然执行脚本的方法一样,但是没有共同的接口,所以只能分开执行
                // 集群模式
                if (nativeConnection instanceof JedisCluster) {
                    return (Long) ((JedisCluster) nativeConnection).eval(UNLOCK_LUA, keys, args);
                }

                // 单机模式
                else if (nativeConnection instanceof Jedis) {
                    return (Long) ((Jedis) nativeConnection).eval(UNLOCK_LUA, keys, args);
                }
                return 0L;
            };
            Long result = redisTemplate.execute(callback);

            return result != null && result > 0;
        } catch (Exception e) {
            logger.error("release lock occured an exception", e);
        } finally {
            // 清除掉ThreadLocal中的数据,避免内存溢出
            //lockFlag.remove();
        }
        return false;
    }

    public static final String UNLOCK_LUA;

    static {
        StringBuilder sb = new StringBuilder();
        sb.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] ");
        sb.append("then ");
        sb.append("    return redis.call(\"del\",KEYS[1]) ");
        sb.append("else ");
        sb.append("    return 0 ");
        sb.append("end ");
        UNLOCK_LUA = sb.toString();
    }
}

3.声明RedisLock锁实现Lock接口

@Component
public class RedisLock implements Lock {
    private Logger logger = LoggerFactory.getLogger(RedisLock.class);
    public static final String UNLOCK_LUA;
    static {
        StringBuilder sb = new StringBuilder();
        sb.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] ");
        sb.append("then ");
        sb.append("    return redis.call(\"del\",KEYS[1]) ");
        sb.append("else ");
        sb.append("    return 0 ");
        sb.append("end ");
        UNLOCK_LUA = sb.toString();
    }

    @Resource
    RedisServiceInter redisServiceInter;

    private static  final  String  KEY="KEY";

    private ThreadLocal<String> local=new ThreadLocal<>();

    //加锁
    @Override
    public void lock() {
        //1.尝试加锁
        if(tryLock()){
            return;
        }
        //2.加锁失败 当前任务休眠一段时间
        try {
            Thread.sleep(100);
        }catch (Exception e){
            e.printStackTrace();
        }
        //3.递归调用,再次重新加锁
        lock();
    }


    //尝试加锁
    @Override
    public boolean tryLock() {
        //产生随机值
        String uuid=UUID.randomUUID().toString();
        //设置随机值以及设置失效时间
        boolean ret = redisServiceInter.setNx(KEY, uuid, 100);
        if(ret){
            //加锁成功后,为当前threadlocal设置随机值,以便解锁的时候使用
            local.set(uuid);
        }
        return ret;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }

    //解锁
    @Override
    public void unlock() {
        boolean b = redisServiceInter.releaseLock(KEY, local.get());
        //清理ThreadLocal中的数据,避免内存溢出
        local.remove();
        if(!b){
            logger.error("解锁失败");
        }
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    @Override
    public Condition newCondition() {
        return null;
    }
}

4.在Controller中接口测试下
模拟有10个线程在不断的争抢1000张票

@RestController
public class RedisLockController {
    private Logger logger = LoggerFactory.getLogger(RedisLockController.class);
    ExecutorService executorService= Executors.newFixedThreadPool(10);
    @Resource
    RedisLock lock;
    private static int num=1000;

    @GetMapping("/lock")
    public String redislockTest(){
        num=1000;
        for (int i=0;i<10;i++){
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    locks();
                }
            });
        }
        return "ok";
    }

    private void locks() {
        while (num>0){
            lock.lock();
            try {
                if(num>0){
                    logger.info("抢到第"+num--+"个");
                }
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }
    }
}

结果

018-12-06 10:46:23 25049 [pool-1-thread-6] INFO c.e.s.c.RedisLockController - 抢到第1000个
2018-12-06 10:46:23 25049 [pool-1-thread-6] INFO c.e.s.c.RedisLockController - 抢到第999个
2018-12-06 10:46:23 25050 [pool-1-thread-6] INFO c.e.s.c.RedisLockController - 抢到第998个



2018-12-06 10:46:23 25050 [pool-1-thread-6] INFO c.e.s.c.RedisLockController - 抢到第10个
2018-12-06 10:46:23 25063 [pool-1-thread-6] INFO c.e.s.c.RedisLockController - 抢到第9个
2018-12-06 10:46:23 25063 [pool-1-thread-6] INFO c.e.s.c.RedisLockController - 抢到第8个
2018-12-06 10:46:23 25064 [pool-1-thread-6] INFO c.e.s.c.RedisLockController - 抢到第7个
2018-12-06 10:46:23 25065 [pool-1-thread-6] INFO c.e.s.c.RedisLockController - 抢到第6个
2018-12-06 10:46:23 25065 [pool-1-thread-6] INFO c.e.s.c.RedisLockController - 抢到第5个
2018-12-06 10:46:23 25066 [pool-1-thread-6] INFO c.e.s.c.RedisLockController - 抢到第4个
2018-12-06 10:46:23 25067 [pool-1-thread-6] INFO c.e.s.c.RedisLockController - 抢到第3个
2018-12-06 10:46:23 25067 [pool-1-thread-6] INFO c.e.s.c.RedisLockController - 抢到第2个
2018-12-06 10:46:23 25068 [pool-1-thread-6] INFO c.e.s.c.RedisLockController - 抢到第1个

猜你喜欢

转载自blog.csdn.net/u010520146/article/details/84848514