3、简单锁的优化

总结:为了防止线程处理时间超过锁的自动释放时间后,出现误删除其他线程锁的情况,会在手动删除锁的时候判断该锁是否属于此线程,不属于则不删除。为了实现锁的重入,可以通过redis的hash数据结构来构造锁,hash表的field作为线程唯一id,value设置为重入次数即可。

要防止删除别人的锁其实也很简单,就是把redis中锁的值设置成当前线程的唯一标识,然后删除的时候校对下,如果对不上就不删除了。下面代码采用uuid+当前线程id的方式生成。

package com.dazhiyouqiu.api;
import java.util.UUID;

public class test {

    public static void main(String[] args) {


        //监听线程
        Thread monitor = new Thread(new Runnable() {
            @Override
            public void run() {

                int time = 0;
                Redis ru = new Redis();


                while (time < 100000)
                    try {
                        Thread.sleep(500);
                        time = time + 500;
                        System.out.println("Time:" + time + "---->" + ru.get("lock"));
                    } catch (Exception e) {

                    }


            }
        });

        monitor.start();


        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                Redis ru = new Redis();


                //加锁,默认没获得锁
                Boolean notGetLock = true;
                String uuid= UUID.randomUUID().toString()+"_"+Thread.currentThread().getId();

                //一直等到获得锁,再往下执行
                String getLockResult = null;
                while (notGetLock) {
                    getLockResult = ru.set("lock",uuid, 5);
                    if (getLockResult != null)
                        notGetLock = false;

                }


                try {
                    Thread.sleep(6000);
                } catch (Exception e) {

                }


                //解锁
                if(ru.get("lock").equals(uuid))
                    ru.delete("lock");
                else {
                    System.out.println("当前线程"+Thread.currentThread().getName()+"无法删除锁,uuid不对");
                }
            }
        });

        thread1.start();


        //等待5s后再启动线程2
        try {
            Thread.sleep(5000);
        } catch (Exception e) {

        }


        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                Redis ru = new Redis();


                //加锁,默认没获得锁
                Boolean notGetLock = true;
                //生成UUID作为value存储在redis
                String uuid= UUID.randomUUID().toString()+"_"+Thread.currentThread().getId();

                //一直等到获得锁,再往下执行
                String getLockResult = null;
                while (notGetLock) {
                    getLockResult = ru.set("lock", uuid, 5);
                    if (getLockResult != null)
                        notGetLock = false;

                }


                try {
                    Thread.sleep(4000);
                } catch (Exception e) {

                }

                //解锁
                if(ru.get("lock").equals(uuid))
                    ru.delete("lock");
                else {
                    System.out.println("当前线程"+Thread.currentThread().getName()+"无法删除锁,uuid不对");
                }
            }
        });

        thread2.start();
    }

}

运行结果如下:

Time:500---->ee9bf6b2-82b5-4891-9d41-3c3761ad05c8_14
Time:1000---->ee9bf6b2-82b5-4891-9d41-3c3761ad05c8_14
Time:1500---->ee9bf6b2-82b5-4891-9d41-3c3761ad05c8_14
Time:2000---->ee9bf6b2-82b5-4891-9d41-3c3761ad05c8_14
Time:2500---->ee9bf6b2-82b5-4891-9d41-3c3761ad05c8_14
Time:3000---->ee9bf6b2-82b5-4891-9d41-3c3761ad05c8_14
Time:3500---->ee9bf6b2-82b5-4891-9d41-3c3761ad05c8_14
Time:4000---->ee9bf6b2-82b5-4891-9d41-3c3761ad05c8_14
Time:4500---->ee9bf6b2-82b5-4891-9d41-3c3761ad05c8_14
Time:5000---->af60b26e-ca2b-4be9-99de-3e1dc53abd18_17
Time:5500---->af60b26e-ca2b-4be9-99de-3e1dc53abd18_17
当前线程Thread-1无法删除锁,uuid不对
Time:6000---->af60b26e-ca2b-4be9-99de-3e1dc53abd18_17
Time:6500---->af60b26e-ca2b-4be9-99de-3e1dc53abd18_17
Time:7000---->af60b26e-ca2b-4be9-99de-3e1dc53abd18_17
Time:7500---->af60b26e-ca2b-4be9-99de-3e1dc53abd18_17
Time:8000---->af60b26e-ca2b-4be9-99de-3e1dc53abd18_17
Time:8500---->af60b26e-ca2b-4be9-99de-3e1dc53abd18_17
Time:9000---->null
Time:9500---->null
Time:10000---->null
Time:10500---->null
Time:11000---->null
Time:11500---->null

这样看似能解决问题,但是我们在第一节说过,如果在java中将两个原子操作拼接在一起的话依旧会有并发问题。我们在后面将查询锁和删除的操作分开了,所以就不是原子操作了。为了解决这个问题我们采用lua语言来操作。采用lua语言来操作redis的好处有这些:

减少网络开销:多个请求通过脚本一次发送,减少网络延迟
原子操作:将脚本作为一个整体执行,中间不会插入其他命令,无需使用事务
复用:客户端发送的脚本永久存在redis中,其他客户端可以复用脚本
可嵌入性:可嵌入JAVA,C#等多种编程语言,支持不同操作系统跨平台交互

所以代码可以改成如下:

package com.dazhiyouqiu.api;


import java.util.UUID;

public class test {


    public static void main(String[] args) {


        //监听线程
        Thread monitor = new Thread(new Runnable() {
            @Override
            public void run() {

                int time = 0;
                Redis ru = new Redis();


                while (time < 100000)
                    try {
                        Thread.sleep(500);
                        time = time + 500;
                        System.out.println("Time:" + time + "---->" + ru.get("lock"));
                    } catch (Exception e) {

                    }


            }
        });

        monitor.start();


        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                Redis ru = new Redis();


                //加锁,默认没获得锁
                Boolean notGetLock = true;
                String uuid= UUID.randomUUID().toString()+"_"+Thread.currentThread().getId();

                //一直等到获得锁,再往下执行
                String getLockResult = null;
                while (notGetLock) {
                    getLockResult = ru.set("lock",uuid, 5);
                    if (getLockResult != null)
                        notGetLock = false;

                }


                try {
                    Thread.sleep(6000);
                } catch (Exception e) {

                }


                //解锁
                if(ru.deleteWithValue("lock",uuid)){
                    System.out.println("当前线程"+Thread.currentThread().getName()+"手动解锁成功");
                }
                 else {
                    System.out.println("当前线程"+Thread.currentThread().getName()+"无法删除锁,uuid不对");
                }
            }
        });

        thread1.start();


        //等待5s后再启动线程2
        try {
            Thread.sleep(5000);
        } catch (Exception e) {

        }


        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                Redis ru = new Redis();


                //加锁,默认没获得锁
                Boolean notGetLock = true;
                //生成UUID作为value存储在redis
                String uuid= UUID.randomUUID().toString()+"_"+Thread.currentThread().getId();

                //一直等到获得锁,再往下执行
                String getLockResult = null;
                while (notGetLock) {
                    getLockResult = ru.set("lock", uuid, 5);
                    if (getLockResult != null)
                        notGetLock = false;

                }


                try {
                    Thread.sleep(4000);
                } catch (Exception e) {

                }

                //解锁
                if(ru.deleteWithValue("lock",uuid)){
                    System.out.println("当前线程"+Thread.currentThread().getName()+"手动解锁成功");
                }
                else {
                    System.out.println("当前线程"+Thread.currentThread().getName()+"无法删除锁,uuid不对");
                }
            }
        });

        thread2.start();
    }

}

其中ru.deleteWithValue()方法的代码如下:

 public boolean deleteWithValue(String key,String value) {
        String luaScript = "if redis.call('get',KEYS[1]) == ARGV[1] then " +
                "return redis.call('del',KEYS[1]) else return 0 end";
        return jedis.eval(luaScript, Collections.singletonList(key), Collections.singletonList(value)).equals(1L);
    }

测试的结果如下:

Time:500---->d92dcae5-876c-4778-bef5-a2c33effaf15_14
Time:1000---->d92dcae5-876c-4778-bef5-a2c33effaf15_14
Time:1500---->d92dcae5-876c-4778-bef5-a2c33effaf15_14
Time:2000---->d92dcae5-876c-4778-bef5-a2c33effaf15_14
Time:2500---->d92dcae5-876c-4778-bef5-a2c33effaf15_14
Time:3000---->d92dcae5-876c-4778-bef5-a2c33effaf15_14
Time:3500---->d92dcae5-876c-4778-bef5-a2c33effaf15_14
Time:4000---->d92dcae5-876c-4778-bef5-a2c33effaf15_14
Time:4500---->d92dcae5-876c-4778-bef5-a2c33effaf15_14
Time:5000---->3f9bacca-f3de-4e3e-b1d5-be2cdc03f76f_17
Time:5500---->3f9bacca-f3de-4e3e-b1d5-be2cdc03f76f_17
当前线程Thread-1无法删除锁,uuid不对
Time:6000---->3f9bacca-f3de-4e3e-b1d5-be2cdc03f76f_17
Time:6500---->3f9bacca-f3de-4e3e-b1d5-be2cdc03f76f_17
Time:7000---->3f9bacca-f3de-4e3e-b1d5-be2cdc03f76f_17
Time:7500---->3f9bacca-f3de-4e3e-b1d5-be2cdc03f76f_17
Time:8000---->3f9bacca-f3de-4e3e-b1d5-be2cdc03f76f_17
Time:8500---->3f9bacca-f3de-4e3e-b1d5-be2cdc03f76f_17
当前线程Thread-3手动解锁成功
Time:9000---->null
Time:9500---->null
Time:10000---->null

不过最好避免同时有两个线程同时访问同一个锁的情况,所以会在某个线程占用锁的时候创建一个守护线程,当锁快要超时的时候,守护线程给锁的过期时间续上。当线程结束的时候守护线程也会跟着结束,所以不用担心守护线程会遗留造成问题。
至于重入锁的实现可以在这基础上做修改,通过redis的hash数据结构来构造锁,hash表的field作为线程唯一id,value设置为重入次数即可。同一线程多次获取锁时将value加1,解锁时将value减1。当value为0时锁才会进行释放。感兴趣的同学可以自己试着实现该特性。

至此我们通过redis实现了简单的单机redis的分布式锁,其核心就是通过set的原子操作和lua语言运行redis的原子特性来实现的。但是当面对redis集群时我们也束手无策,当某个redis服务器宕机时,没法查询其他机器,也没法确保当前的锁是否已经被获取。

因此下面将介绍Redisson实现Redis分布式锁:RedLock。

你可能感兴趣的:(3、简单锁的优化)