Redis基本使用-分布式锁

问题场景:

例如:一个简单的用户操作,一个线程去修改用户的状态,首先从数据库中读出用户的状态,然后在内存中进行修改,修改完成后,再回去。在单线程中,这个操作没有问题,但是在多线程中,由于读取、修改、存这是三个操作,不是原子操作,所以在多线程中,这样会出现问题。

对于这种问题,我们可以使用分布式锁来限制程序的并发执行

分布式锁实现的思路很简单,就是进来一个线程先占位,当别的线程进来操作时,发现已经有人占位了,就会放弃或者稍后再试

环境:

public interface CallWithJedis {
    void call(Jedis jedis);
}
public class Redis {
    private JedisPool pool;

    public Redis() {
        GenericObjectPoolConfig config = new GenericObjectPoolConfig();
        //连接池最大空闲数
        config.setMaxIdle(300);
        //最大连接数
        config.setMaxTotal(1000);
        //连接最大等待时间,如果是-1表示没有限制
        config.setMaxWaitMillis(30000);
        //在空闲时检查有效性
        config.setTestOnBorrow(true);
        pool=new JedisPool(config,"ip地址",6379,30000,"Ishton");
    }

    public void execute(CallWithJedis callWithJedis){
        try(Jedis jedis=pool.getResource()){
            callWithJedis.call(jedis);
        }
    }
}

在 Redis 中,占位一般使用**setnx **指令,先进来的线城先占位,线城的操作执行完成后,再调用 **del **指令释放位子

分布式锁操作

//分布式锁操作
    /**
     * 缺陷:
     *如果代码业务的执行过程中抛出或者挂了,这样,会导致del指令没有调用。
     *这样K1无法释放,后面来的请求全部堵塞在这里,锁也永远得不到释放。
     **/
    public void test1(){
        Redis redis = new Redis();
        redis.execute(jedis -> {
            //锁
            Long setnx = jedis.setnx("k1", "v1");
            if (setnx == 1){
                jedis.set("name","zhixin");
                String name = jedis.get("name");
                System.out.println(name);
                jedis.del("k1");
            }else {
                //有人占位,停止/暂缓  操作
            }
        });

    }

优化1:

优化分布式锁:给锁添加一个过期时间,确保锁在一定的时间之后,能够得到释放

//优化分布式锁:给锁添加一个过期时间,确保锁在一定的时间之后,能够得到释放
    /**
     * 缺陷:
     * 在获取锁和设置过期时间如果服务器突然挂了,这个时候锁被占用,无法及时得到释放
     *也会造成死锁,因为获取锁和设置过期时间是两个操作,不具备原子性
     **/
    public void test2(){
        Redis redis = new Redis();
        redis.execute(jedis -> {
            //锁
            Long setnx = jedis.setnx("k1","v1");
            if (setnx==1){
                //添加过期时间,防止应用在运行过程中抛出异常导致锁无法及时得到释放
                jedis.expire("k1", 5);
                //没人占位
                jedis.set("name","xiaoxin");
                String name = jedis.get("name");
                System.out.println(name);
                //释放资源
                jedis.del("k1");
            }else{
                //有人占位,停止/暂缓 操作
            }
        });
    }

优化2:

优化锁过期时间:从Redis2.8开始,setnx和expire可以通过一个命令一起来执行

//优化锁过期时间:从Redis2.8开始,setnx和expire可以通过一个命令一起来执行
    public void test3(){
        Redis redis = new Redis();
        redis.execute(jedis -> {
            //优化锁
            String set = jedis.set("k1", "v1", new SetParams().nx().ex(5));
            if (set!=null&&"OK".equals(set)){
                //业务代码
                jedis.set("name","zhixin");
                String name = jedis.get("name");
                System.out.println(name);
                //释放锁
                jedis.del("k1");

            }else{
                //有人占位:停止/暂缓 操作
            }
        });

    }

分布式锁超时问题

为了防止业务代码在执行的时候抛出异常,我们给每一个锁添加了一个超时时间,超时之后,锁会被自动释放,但是这也带来了一个新的问题:如果要执行的业务非常好使,可能会出现紊乱。

案例:第一个线程首先获取到锁,然后开始执行业务代码,但是业务代码比较耗时,执行了8秒,这样,会在第一个线程的任务还未执行成功锁就会被释放了,此时第二个锁会获取到锁开始执行,在第二个线程执行了3秒,第一个线程也执行完了,此时第一个线程会释放锁,但是注意,它释放的是第二个线程的锁,释放之后,第三个线程进来了。

对于这个问题,可以从两个方面入手:

  • 尽量避免在获取锁之后,执行耗时操作
  • 可以在锁上做文章,将锁的value设置为一个随机字符串,每次释放的时候,都会比较字符串是否一致,如果一致,再去释放,否则,不释放

对于第二种方案,由于释放锁的时候,要去查看锁的value,第二个比较value的值是否正确,第三步释放锁,有三个步骤,很明确三个步骤不具备原子性,为了解决这个问题得引用Lua脚本

Lua脚本的优势

  • 使用方便,Redis中内置了对Lua脚本的支持
  • Lua脚本可以在Redis服务端原子的执行多个Redis命令
  • 由于案例在很大程度上会影响到Redis性能,而使用Lua脚本可以让多个命令一次执行,可以有效解决网络给Redis带来的性能问题

在 Redis 中,使用 Lua 脚本,大致上两种思路:

  1. 提前在 Redis 服务端写好 Lua 脚本,然后在 Java 客户端去调用脚本(推荐)。
  2. 可以直接在 Java 端去写 Lua 脚本,写好之后,需要执行时,每次将脚本发送到 Redis 上去执行。

首先在 Redis 服务端创建 Lua 脚本,内容如下:

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

操作

[root@iZwz910dfd1z50our2z30kZ ~]# cd /usr/local/redis-5.0.13/
[root@iZwz910dfd1z50our2z30kZ redis-5.0.13]# mkdir lua
[root@iZwz910dfd1z50our2z30kZ redis-5.0.13]# cd lua
[root@iZwz910dfd1z50our2z30kZ lua]# vi releaseWhereValueEqual.lua
[root@iZwz910dfd1z50our2z30kZ lua]# cat releaseWhereValueEqual.lua 
if redis.call("get",KEYS[1])==ARGV[1] then
        return redis.call("del",KEY[1])
else 
        return 0
end

可以给 Lua 脚本求一个 SHA1 和

[root@iZwz910dfd1z50our2z30kZ redis-5.0.13]# cat lua/releaseWhereValueEqual.lua |redis-cli -a xiaozhong script load --pipe
Warning: Using a password with ‘-a‘ or ‘-u‘ option on the command line interface may not be safe.
"b8059ba43af6ffe8bed3db65bac35d452f8115d8"

cat lua/releaseWhereValueEqual.lua |redis-cli -a xiaozhong script load --pipe

相当于在Redis缓存中添加了这个文件

script load 这个命令会在 Redis 服务器中缓存 Lua 脚本,并返回脚本内容的 SHA1 校验和,然后在Java 端调用时,传入 SHA1 校验和作为参数,这样 Redis 服务端就知道执行哪个脚本了。

在Java端调用这个脚本

public class LuaTest {
    public static void main(String[] args) {
        Redis redis = new Redis();
        redis.execute(jedis -> {
            String value = UUID.randomUUID().toString();
            String k1 = jedis.set("k1", value, new SetParams().nx().ex(5));
            if (k1!=null&&"OK".equals(k1)){
                jedis.set("site","www.zhixin.cn");
                String site = jedis.get("site");
                System.out.println(site);
                jedis.evalsha("b8059ba43af6ffe8bed3db65bac35d452f8115d8", Arrays.asList("k1"),Arrays.asList(value));
            }else {
                System.out.println("没拿到锁");
            }
        });
        /**
         * 拿不到锁
        for (int i = 0; i < 2; i++) {
            redis.execute(jedis -> {
                String value = UUID.randomUUID().toString();
                String k1 = jedis.set("k1", value, new SetParams().nx().ex(5));
                if (k1!=null&&"OK".equals(k1)){
                    jedis.set("site","www.zhixin.cn");
                    String site = jedis.get("site");
                    System.out.println(site);
                    jedis.evalsha("b8059ba43af6ffe8bed3db65bac35d452f8115d8", Arrays.asList("k1"),Arrays.asList(value));
                }else {
                    System.out.println("没拿到锁");
                }
            });
        }
         **/
    }
}

Redis基本使用-分布式锁

上一篇:.Net Framework 4.0安装cmd命令


下一篇:hadoop 2.8.5安装步骤