学习一下分布式锁 尝试手写 基于lettuce

1.redis客户端 jedis和lettuce
网上大部分是基于jedis的分布式锁 jedis是多线程下不是线程安全的 lettuce是基于netty线程安全性能高,
springboot2.0都采用lettuce,所以分布式锁最好基于lettuce
2.分布式锁原理
采用原子操作如下:
如果不存在key就设置这个key 存在这个key就阻塞住等待锁key自动或手动释放
3.多线程或多进程下存在多问题
当同步代码块阻塞时间过长,导致代码还没执行完,但是此时超过了持有锁的超时时间(key的过期时间)锁会自动被释放,之后同步代码块执行完毕后又会去执行释放锁的操作(类似unlock)。在锁超时之后,未手动释放锁之前如果其他进程重新设了一把锁,当前进程之后的手动释放又会把其他进程设置的新锁释放掉,所以在手动释放锁之前一定要核对value的值是否和设置锁的时候一致的,这样就不会把其他进程的锁给释放掉。
置key时 value一定要随机而且唯一(就像uuid)原因如上;

4.贴代码

@Component
@Slf4j
public class RedisLock {
    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 存储当前线程  设置锁对应的value   用来释放锁时进行校验
     */
    private final ThreadLocal<String> lockValue = new ThreadLocal<>();

    private final String keyLock = "lock";


    /**
     * 获取锁 (带超时时间)
     *
     * @param timeout
     * @param expireTime
     * @return
     * @throws InterruptedException
     */
    public boolean tryLockWithTimeout(Integer timeout, Integer expireTime) {
        String value = UUID.randomUUID().toString();
        //获取未来过期时间点
        long invalidTime = System.currentTimeMillis() + timeout * 1000;
        boolean flag = false;
        while (System.currentTimeMillis() < invalidTime) {
            flag = tryLock(keyLock, value, expireTime);
            if (flag) {
                break;
            } else {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    log.error(e.toString());
                }
            }

        }
        if (false == flag) {
            log.error("竞争锁失败");
        }
        return flag;
    }

    /**
     * 获取锁 (一直抢到为止) 阻塞锁
     *
     * @param expireTime 锁失效时间
     * @return
     */
    public boolean blockLock(Long expireTime) throws InterruptedException {
        String value = UUID.randomUUID().toString();
        boolean flag = false;
        while (true) {
            flag = tryLock(keyLock, value, expireTime);
            if (flag) {
                break;
            } else {
                Thread.sleep(10);
            }

        }
        if (false == flag) {
            log.error("竞争锁失败");
        }
        return flag;
    }

    /**
     * 释放锁
     *
     * @return
     */
    public boolean unlock() {
        //重试次数
        Integer retryTimes = 3;
        //先拿到当前锁对应的值(理解为版本号)
        String value =(String)redisTemplate.opsForValue().get(keyLock);
        Boolean flag = false;
        //如果相等 锁应该未被释放  不相等则锁已过期自动释放此时如果有锁应为其他进程的锁
        String s = lockValue.get();
        if (lockValue.get().equals(value)) {
            while (retryTimes >0) {
                try {
                    flag = redisTemplate.delete(keyLock);
                    break;
                } catch (Exception e) {
                    log.error("释放锁异常");
                    retryTimes--;
                }
            }
        }else {

        }
        return flag;
    }

    /**
     * 设置锁 如果不存在key设置成功,
     *
     * @param key        redis key
     * @param value      redis value
     * @param expireTime key过期时间 好吗
     * @return
     */
    private boolean tryLock(String key, String value, long expireTime) {
        try {
            String result = (String) redisTemplate.execute((RedisCallback) connection -> {
                try {
                    String redisResult = null;
                    //获取连接
                    Object nativeConnection = connection.getNativeConnection();
                    //获取序列化工具 (不使用这个工具序列化 redistemplate会取不到值)
                    RedisSerializer keySerializer = redisTemplate.getKeySerializer();
                    //序列化
                    byte[] keyByte = keySerializer.serialize(key);
                    byte[] valueByte = keySerializer.serialize(value);
                    //单机模式
                    if (nativeConnection instanceof RedisAsyncCommands) {
                        RedisAsyncCommands commands = (RedisAsyncCommands) nativeConnection;
                        redisResult = commands.getStatefulConnection()
                                .sync()
                                .set(keyByte, valueByte, SetArgs.Builder.nx().ex(expireTime));
                    } else if (nativeConnection instanceof RedisAdvancedClusterAsyncCommands) {
                        //集群模式
                        RedisAdvancedClusterAsyncCommands clusterAsyncCommands = (RedisAdvancedClusterAsyncCommands) nativeConnection;
                        redisResult = clusterAsyncCommands.getStatefulConnection()
                                .sync()
                                .set(keyByte, valueByte, SetArgs.Builder.nx().ex(expireTime));
                    } else {
                        log.error("REDISLIBMISTCH");
                    }
                    return redisResult;
                } catch (Exception e) {
                    log.error("Failed to lock, closing connection");
                    connection.close();
                    return "";
                }

            });
            //如果成功设置锁 则将value逸出
            boolean eq = "OK".equals(result);
            if (eq) {
                lockValue.set(value);
            }
            return eq;
        } catch (Exception e) {
            log.error("设置锁异常");
            return false;
        }
    }
}

测试代码:
为了模拟线程不安全 用一个Integer 变量多线程累加 累加不是原子操作而且需要依赖于当前状态。
1.写一个需要执行的任务(加锁同步)

public class task implements Runnable {
   //没用内置锁不能保证内存的可见性   为了保证可见性加个voliatile
    private static volatile Integer times = 0;

    public static  void doTask() {
        RedisLock redisLock = (RedisLock) SpringUtil.getBean("redisLock");
        boolean b = redisLock.tryLockWithTimeout(10, 100);
        if(b==false){
            return;
        }
        while (times <= 20) {

                System.out.println(times++ + " : id:" + Thread.currentThread().getId());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        redisLock.unlock();

    }

    @Override
    public void run() {
       task.doTask();
    }
}

不加锁同步任务

public class task implements Runnable {

    private static volatile Integer times = 0;

    public static  void doTask() {
        while (times <= 20) {
                System.out.println(times++ + " : id:" + Thread.currentThread().getId());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public void run() {
       task.doTask();
    }
}

测试test:

@RunWith(SpringRunner.class)
@SpringBootTest
public class LockdemoApplicationTests {
@Autowired
private ApplicationContext applicationContext;
    @Test
    public void contextLoads() {
        ThreadPoolExecutor executor=new ThreadPoolExecutor(3,3,50, TimeUnit.SECONDS,new LinkedBlockingQueue<>(10));
        executor.execute(new task());
        executor.execute(new task());
        executor.execute(new task())while (true){
         Thread.sleep(1000);
        }
    }
}

测试结果:
1.不加锁
学习一下分布式锁 尝试手写 基于lettuce_第1张图片
总共三个线程在执行 id 分别为 16,17,18
累加过程中出现问题。

2.加分布式锁的累加

学习一下分布式锁 尝试手写 基于lettuce_第2张图片
只有16号线程在执行 其余线程被阻塞,阻塞超过了指定的时间(这里我设置的是10秒) 就会放弃竞争。
从结果来看是安全的累加 分布式锁达到效果


更新 写了一个优化版 并用aop做了封装
https://blog.csdn.net/qq_36559868/article/details/102462807

你可能感兴趣的:(分布式锁)