Redis实现CAS的乐观锁

对于经常开发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(){
            boolean flag =true;
            while(flag){
                                cli.watch(incKeyStr);
                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来解决资源重复修改或争用的问题。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容