Redis实现CAS的乐观锁

作者: 一只小哈 | 来源:发表于2016-07-21 22:43 被阅读6304次

    对于经常开发Web的Coder们,经常会有这样的需求,就是在多机的分布式环境下,有时候需要限制多台机器上的请求修改同一份资源。对于单机的环境下,我们通常可以用同步或者锁去避免多线程下的竞态条件。以java为例,我们可以用synchronized或者ReentrantLock,去做资源访问的同步。但这是JVM和操作系统提供给我们的特性,但是对于分布式环境下我们没有这些便利条件。所以我们需要引入一个外部的Observer去实现这样的一个分布式锁,Zookeeper是一个比较好的解决方案,但是Zookeeper还是比较重的,我们可以用Redis实现这样一个锁。
    乐观锁基于CAS思想,是不具有互斥性,不会产生锁等待而消耗资源,但是需要反复的重试,但也是因为重试的机制,能比较快的响应。在实现CAS之前,需要了解一下Redis的事务机制。
    Redis事务:
    我们可以用Mysql事务机制来理解Redis的事务机制,但也有所不同,Mysql的事务的形式如下:
    openSession()
    update()
    insert()
    commit()
    如果在update和insert之间出现错误,那么会触发rollback(),Redis的事务用到了MULTI和EXEC命令,事务的形式如下:
    MULTI
    SET
    HSET
    EXEC
    和Mysql的事务不同,Redis会将所有EXEC命令之前的命令放入一个QUEUE中,当遇到EXEC时批量执行QUEUE中的命令,但是 Redis的事务是不支持回滚的,它只是顺序的执行命令,并批量返回结果,但是对于极端情况下,事务在没有完全执行完时宕机,导致事务日志只写入部分,这样在重启时会产生错误,用aof的修复工具修复后可以进行启动。
    在了解了事务机制后,我们还不足以实现乐观锁,还需要了解一个命令——Watch,Watch命令可以监控Redis中的一个key,当Key发生变化时终止事务的提交。先看一个正确的例子:

    127.0.0.1:6379> set locktest 1
    OK
    127.0.0.1:6379> get locktest
    "1"
    127.0.0.1:6379> watch locktest
    OK
    127.0.0.1:6379> multi
    OK
    127.0.0.1:6379> set locktest 3
    QUEUED
    127.0.0.1:6379> exec
    1) OK
    127.0.0.1:6379> get locktest
    "3"
    127.0.0.1:6379> 
    

    但是在multi的过程中如果locktest的值发生变化又会怎样?

    127.0.0.1:6379> set locktest 1
    OK
    127.0.0.1:6379> get locktest
    "1"
    127.0.0.1:6379> watch locktest
    OK
    127.0.0.1:6379> multi
    OK
    127.0.0.1:6379> set locktest 3
    QUEUED
    127.0.0.1:6379> exec
    (nil)
    127.0.0.1:6379> get locktest
    "2"
    127.0.0.1:6379> 
    

    这里我们用另一个Client在Multi之后将locktest修改为2,课件在执行事务的时候返回为nil,表示执行失败。
    那么我们就可以用上述两种命令实现一个乐观锁,代码如下:

    package com.redis.lock;
    
    import java.util.List;
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.atomic.AtomicInteger;
    import redis.clients.jedis.Jedis;
    import redis.clients.jedis.JedisPool;
    import redis.clients.jedis.JedisPoolConfig;
    import redis.clients.jedis.Transaction;
    /**
     * topic:利用redis的事务,实现一个乐观锁
     * 
     * @author zhiming
     *
     */
    public class RedisWatchLock {
    
        private static final String redisHost = "10.0.5.86";
        
        private static final int port = 6381;
        
        private static JedisPoolConfig config;
        
        private static JedisPool pool;
        
        private static ExecutorService service;
        
        private static int ThLeng=10;
        
        private static CountDownLatch latch;
        
        private static AtomicInteger Countor = new AtomicInteger(0);
        static{
            //利用Redis连接池,保证多个线程利用多个连接,充分模拟并发性
            config = new JedisPoolConfig();
            config.setMaxIdle(10);
            config.setMaxWaitMillis(1000);
            config.setMaxTotal(30);
            pool = new JedisPool(config, redisHost, port);
            //利用ExecutorService 管理线程
            service = Executors.newFixedThreadPool(10);
            //CountDownLatch保证主线程在全部线程结束之后退出
            latch = new CountDownLatch(ThLeng);
        }
        
        public static void main(String args[]){
            int ThLeng = 10;
            String ThreadNamePrefix = "thread-";
            Jedis cli = pool.getResource();
            cli.del("redis_inc_key");//先删除既定的key
            cli.set("redis_inc_key", String.valueOf(1));//设定默认值
            for(int i =0;i<ThLeng;i++){
                Thread th = new Thread(new TestThread(pool));
                th.setName(ThreadNamePrefix+i);
                System.out.println(th.getName()+"inited...");
                service.submit(th);
            }
            service.shutdown();
            try {
                latch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("all sub thread sucess");
            System.out.println("countor is "+Countor.get());
            String countStr = cli.get("redis_inc_key");
            System.out.println(countStr);
        }
        
        public static class TestThread implements Runnable {
            private String incKeyStr = "redis_inc_key";
            private Jedis cli;
            private JedisPool pool;
            public TestThread(JedisPool pool) {
                cli = pool.getResource();
                this.pool = pool;
                
            }
            public void run() {
                try{
                
                    for (int i = 0; i < 100; i++) {
                        actomicAdd();
                    }
                }catch(Exception e){
                    pool.returnBrokenResource(cli);
                }
                finally{
                    pool.returnResource(cli);
                    latch.countDown();
                }
            }
            
            public void actomicAdd(){
                cli.watch(incKeyStr);
                boolean flag =true;
                while(flag){
                    String countStr = cli.get("redis_inc_key");
                    int countInt = Integer.parseInt(countStr);
                    int expect = countInt+1;
                    Transaction tx = cli.multi();                   
                    tx.set(incKeyStr, String.valueOf(expect));
                    List<Object> list = tx.exec();
                    //如果事务失败了exec会返回null
                    if(list==null){
                        System.out.println("multi shut down");
                        continue;
                    }
                    else{
                        //如果达到期望值那么结束while循环
                        flag=false;
                    }
                    System.out.println("my expect num is "+expect);         
                    System.out.println("seting....");   
                }
                Countor.incrementAndGet();  
            }
            
        }
        
    }
    

    这样我们就利用Redis实现了一个类似于Java 的原子类的功能。在实际的Web开发中,我们可以利用redis来解决资源重复修改或争用的问题。

    相关文章

      网友评论

      • b5d8912347be:作者你好,我觉得代码存在问题,watch语句应该在while语句块里面才对
      • 慕慕宝贝:redis使用事务难点:
        1、会大大拖慢速度
        2、在redis集群环境下 好像是不支持事务命令的吧

      本文标题:Redis实现CAS的乐观锁

      本文链接:https://www.haomeiwen.com/subject/fdobjttx.html