redis的分布式锁

#   redis的分布式锁

** 问题场景**

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

数据库中事务的四大特性:

原子性:事务包含的操作要么全部成功,要么全部失败回滚

一致性:事务执行前后必须一致

持久性:事务一旦提交,就是永久性的

隔离性 每个用户开启的事务,不能影响其他事物

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

在redis 中,占位一般用setnx 指令,先进来的线程先占位,等线程执行完了后,用del来释放位子

RedisConfig

public interface  RedisConfig {
    void call(Jedis jedis);
}

 Redis

public class Redis {
    private JedisPool jedisPool;
    public Redis(){
        GenericObjectPoolConfig<Jedis> config=new GenericObjectPoolConfig();
        //最大连接数
        config.setMaxTotal(1000);
        //最大等待时间 如果是-1 等带时间没有限制
        config.setMaxWaitMillis(3000);
        //最大空闲数
        config.setMaxIdle(300);
        /**
         * reids 地址
         * Reids 端口
         * Reids 链接超时时间
         * reids 密码
         */
        jedisPool=new JedisPool(config,"116.62.203.162",6379,300,"494936");
    }
    public void execute( RedisConfig redisConfig){
        try(Jedis jedis = jedisPool.getResource()){
            redisConfig.call(jedis);
        }
    }
}

lockRest

public class LockTest {
    public static void main(String[] args) {
        Redis redis = new Redis();
        redis.execute(jedis -> {
            //在redis 中,占位一般用setnx 指令,先进来的线程先占位,等线程执行完了后,用del来释放位子
            Long setnx = jedis.setnx("k1", "v1");
            if(setnx == 1){
                //setnxs 不会存在已经存在的值
                String set = jedis.set("name", "aike");
                String name = jedis.get("name");
                System.out.println(name);
                jedis.del("k1");
            }else {
                //有人占位,停止/暂缓操作
            }
        });
    }
}

当然这样是有问题的,如果代码业务挂了或者抛出异常后,会导致del指令没有被调用。这样陷入死循环中,k1 无法释放,后面来的请求全部堵死

要解决这个问题,就是给锁添加一个过期时间,确保在一定的时间后能都得到释放 

public class LockTest {
    public static void main(String[] args) {
        Redis redis = new Redis();
        redis.execute(jedis -> {
            //在redis 中,占位一般用setnx 指令,先进来的线程先占位,等线程执行完了后,用del来释放位子
            Long setnx = jedis.setnx("k1", "v1");
            if(setnx == 1){
                //给锁添加一个过期时间,防止锁无法释放出现阻塞
                jedis.expire("k1",5);
                //setnxs 不会存在已经存在的值
                String set = jedis.set("name", "aike");
                String name = jedis.get("name");
                System.out.println(name);
                jedis.del("k1");
            }else {
                //有人占位,停止/暂缓操作
            }
        });
    }
}

这样也有问题, 就是获取锁和设置过期时间,如果服务器挂掉,这时候锁被占用,无法释放,会造成死锁。因为获取锁和设置过期时间是两个操作。不具备原子性。

   为了解决这个问题,setnx 和 expire 可以通过一个命令一起执行,

public class LockTest {
    public static void main(String[] args) {
        Redis redis = new Redis();
        redis.execute(jedis -> {
            String set1 = jedis.set("k1", "v1", new SetParams().nx().ex(5));
            System.out.println(set1);
            if(set1 !=null && "ok".equals(set1)){
            //在redis 中,占位一般用setnx 指令,先进来的线程先占位,等线程执行完了后,用del来释放位子

                //给锁添加一个过期时间,防止锁无法释放出现阻塞
                jedis.expire("k1",5);
                //setnxs 不会存在已经存在的值
                String set = jedis.set("name", "aike");
                String name = jedis.get("name");
                System.out.println(name);
                jedis.del("k1");
            }else {
                //有人占位,停止/暂缓操作
            }
        });
    }
}

##   解决超时问题

为了防止业务代码在执行时抛出异常,我们给每个所添加了超时时间,超时之后锁,会自动释放,但这是这样会有一个新问题,如果开始执行业务代码,业务代码执行8秒,这样第一个线程任务还没有执行成功就已经释放了,此时第二个线程会获取到锁开始执行,当第二个任务执行到3秒时,第一个任务也执行完了,此时第一个线程会释放锁,但释放的是第二个线程的锁,释放之后,第三个线程进来了

 对于这个问题我们有两个角度入手:

— 业务代码的执行时间小于锁释放时间

— 将锁的value 设置为随机字符串,每次释放锁的时候,都无比较随机字符串是否一致,如果一致再去释放,否则不释放;

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

lua脚本的优势:

— 使用方便,redis 中内置了lua脚本

— Lua  脚本可以在Redis 服务端原子的执行多个reids命令

 

使用lua脚本两中种思路:

提前在reids服务端写好Lua脚本,然后在Java 客户端去调用脚本(推荐)

 

##  消息队列

Reids 适用于简单的场景。

## 小插曲 

 

redis 做消息队列,使用List数据结构就可以实先,我们可以使用lpush/rpush 操作来实现入队,然后使用lpop/rpop来实现出队

 在客户端(java 端),我们会维护一个死循环来不停的从队列中读取消息,并处理,

如果有消息就处理,没消息就会陷入死循环,这种死循环会造成资源浪费。这时候可以使用blpop/brpop        

##  延迟消息队列

延迟消息队列可以是通过zset来实现的,因为有个score ,我们可以把时间做为score, 将时间作为value存在redis 中,然后通过轮询的方式,去不断读取消息出来

如果消息是字符串,直接发送即可,如果是对象,则需要对对象经行序列化,这里我们使用的Json 来实现序列化和反序列化

package com.king;

public class JavaMessgae {
    private String id;
    private Object data;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    @Override
    public String toString() {
        return "JavaMessgae{" +
                "id='" + id + '\'' +
                ", data=" + data +
                '}';
    }
    
}

消息队列


public class DelaMsgQueue {

    private Jedis jedis;

    private String queue;

    public DelaMsgQueue(Jedis jedis,String queue){
        this.jedis=jedis;
        this.queue=queue;
    }

    /***
     * 消息入队
     * @param data 要发送的消息
     */
    public void queue(Object data){
        JavaMessgae msg = new JavaMessgae();
        msg.setId(UUID.randomUUID().toString());
        msg.setData(data);
        //序列化
        try {
            String s = new ObjectMapper().writeValueAsString(msg);
//            消息发送
            jedis.zadd(queue,System.currentTimeMillis()+5000,s);

        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
    }

    /**
     * 消息消费
     */
    public void loop(){
        //消息不中断就
        while(Thread.interrupted()){
            //读取时间在0到当前时间
             Set<String> strings = jedis.zrangeByScore(queue, 0, System.currentTimeMillis(), 0, 1);
             //如果为空则休息500毫秒
            if(strings.isEmpty()){


                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    //如果出现异常就退出去
                    break;
                }
                continue;
            }
            //如果读取到了这条消息,则直接加载
            String next = strings.iterator().next();
            if(jedis.zrem(queue,next)>0){
                //抢到了,接下来直接处理业务
                try {
                    JavaMessgae javaMessgae = new ObjectMapper().readValue(next, JavaMessgae.class);
                    System.out.println("搜到消息"+javaMessgae);
                } catch (JsonProcessingException e) {
                    e.printStackTrace();
                }
            }
        }     
   
   
   
    }

}

package com.king.config;

import redis.clients.jedis.Jedis;

public class DelaMsgTest {
    public static void main(String[] args) {
        Redis redis = new Redis();
        redis.execute(jedis -> {
            //消息队列
            DelaMsgQueue delaMsgQueue = new DelaMsgQueue(jedis, "jjavaboy-delay-queue");
//           构造一个生产者
            Thread producer = new Thread() {

                @Override
                public void run() {
                    for (int i = 0; i < 5; i++) {
                        delaMsgQueue.queue("www.java.com" + i);
                    }
                }
            };
//构造一个消费者
            Thread consumer = new Thread() {
                @Override
                public void run() {
                    delaMsgQueue.loop();
                }
            };
            //启动
            producer.start();
            consumer.start();
            //休息7秒 停止程序
            try {
                Thread.sleep(7000);
                consumer.interrupt();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        });

    }
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值