Redis下分布式锁的实现

本文可作为redis in action第六章的学习笔记


其实,对redis而言,锁和事务与watch等等是分不开的。
我们先来聊聊事务和watch
事务,在关系型事务上的意思就是:一个事务内的sql,要不全部都执行成功,要么全部都不执行。
不过redis的事务只能部分满足"一荣俱荣,一损俱损"的特性。
怎么说?
在关系型数据库中,一个事务内部如果发生了错误,所有sql就都回滚到初始状态
在redis的事务中,所谓的错误,至少分两种:
1 语法错误
例如 我把set dlf abc 写成了sett dlf abc
2 运行错误
dlf这是个String类型的key,sadd这是对set类型数据做操作的
命令:sadd dlf kkk也就会出错,这就是运行错误
如果在一个事务中,一共三个命令,第二个命令有语法错误,那么三条命令就等于都没有执行。
如果在一个事务中,一共三个命令,第二个命令有运行错误,那么第一三条命令还是执行了的。
所以大家得尽力解决运行错误,你得记得每一个键都是什么类型的。
我记不住呀!
你说你记不住?那你还写什么代码?不会回家看孩子去。


watch命令
watch一般也是跟实物连用的。
线程A,watch某个数据后,如果在线程A执行exec之前,线程B修改了那个数据,那么线程A的事务就会失败。
与此同时,线程B修改的那个数据也已经进入redis了。
所以一般情况下,watch都会包含在一个while循环中。
	while (System.currentTimeMillis() < end) {
            conn.watch(inventory);
            Transaction trans = conn.multi();
	    //...进行事务操作
            List<Object> results = trans.exec();
         
	    // 如果返回的是null 就说明因为watch的域被改变了
            // 事务也就被打断了
            if (results == null){
                continue;
            }
            return true;
        }
那while有什么问题呢?
如果负载很大,事务就会不断的重试!
数据库的那种锁叫悲观锁,我用的时候你不能用。
redis的这个watch机制,叫做乐观锁,就是假定不会有人来打扰我,如果有人打扰(修改了我要操作的数据)我了,那就以别人的数据为准,我再做一遍。
那有什么办法呢?
锁!!


另一方面
我们抛开redis暂且不谈,在单机情况下,java的synchronized关键词能保证同步性。那么多机下呢?
就得用分布式锁了。


也就说,不管是从redis的watch的多次重试上来说,还是从多机互斥上来说,我们都得有一个能支持分布式的不会重试的锁!


OK,我们先说一个命令
SETNX命令(SET if Not eXists)
语法:
SETNX key value
功能:
当且仅当 key 不存在,将key的值设为 value ,并返回1;若给定的 key 已经存在,则 SETNX 不做任何动作,并返回0。


我们设计的redis锁,准确的说保存的就是一个字符串
key是锁的名字,value是一个uuid
OK,开始上代码
     /**
     * 如果过了acquireTimeout时间后,我还没有获得锁,我就放弃了
     * 同时直接返回null
     * 用户获得锁以后,最多使用lockTimeout长时间 过了之后
     * 别的客户端 也就能取到锁了
     */
    public String acquireLockWithTimeout(
        Jedis conn, String lockName, long acquireTimeout, long lockTimeout)
    {
	//
        String identifier = UUID.randomUUID().toString();
        String lockKey = "lock:" + lockName;
        int lockExpire = (int)(lockTimeout / 1000);


        long end = System.currentTimeMillis() + acquireTimeout;
        while (System.currentTimeMillis() < end) {
            //等于1 就说明之前并没有这个key
            if (conn.setnx(lockKey, identifier) == 1){
                conn.expire(lockKey, lockExpire);
                return identifier;
            }
            
	    //代码运行到这里 说明锁已经被别人拿走了
            //等于-1 表示之前也没有设置超时时间
            if (conn.ttl(lockKey) == -1) {
                conn.expire(lockKey, lockExpire);
            }


            try {//等1毫秒再试
                Thread.sleep(1);
            }catch(InterruptedException ie){
                Thread.currentThread().interrupt();
            }
        }


        //别人拿到锁了 我等的时间太久了 老子不等了
        return null;
    }
如果返回的值不为null,那么就说明获得锁了,而且获得的那个identifier,在释放锁的时候也有用。
下面就是释放锁了:
 
 /**
     * 释放成功 返回true 反之返回false
     * @param conn
     * @param lockName
     * @param identifier
     * @return
     */
    public boolean releaseLock(Jedis conn, String lockName, 
    		String identifier) {
        String lockKey = "lock:" + lockName;


        while (true){
            conn.watch(lockKey);
            //如果不相等 会怎么样 为什么有这一步的检查
            if (identifier.equals(conn.get(lockKey))){
                Transaction trans = conn.multi();
                trans.del(lockKey);
                List<Object> results = trans.exec();
                if (results == null){
                    continue;
                }
                return true;
            }
            //unwatch 没有进入事务 就得手动unwatch
            conn.unwatch();
            break;
        }
        return false;
    }
我自己再看到这个代码的时候,很疑惑identifier.equals(conn.get(lockKey)这个是干什么?

就算别人把我的锁的identifier从2dsafd改成了2fsae,能咋么?我直接删除了就是了么。反正只要我不还锁,别人都无法获得锁,能有啥问题么。


假如线程A再2:15获得了锁,并且锁的lockTimeout是3分钟 返回的uuid(就是那个identifier)假如是abc
2:16的时候,线程b想要获得锁,那肯定是获得不了的,线程b阻塞到那了。
到2:18的时候,线程A还没有主动释放锁,但是锁的过期时间已经到了,redis已经删除了那个锁
到2:19的时候线程b就已经获得锁了,返回的uuid假如是edf,并且过期时间也是3分钟,在2:22之前,理论上,只有线程b持有锁。
然后到2:20的时候,线程A来释放锁。如果不检查两个identifier是否相等,线程a就把线程b的锁给删除了
然后2:21线程c就获得了自己本不应该获得的锁(此时线程锁还应该在线程b手上)
亲爱的朋友们,你们明白了么。
那具体怎么使用这个锁呢?
		String locked = acquireLockWithTimeout(conn, lockName,1000);
		//你的代码
                releaseLock(conn, identifier, locked);


参考资料
http://qifuguang.me/2015/09/30/Redis%E4%BA%8B%E5%8A%A1%E4%BB%8B%E7%BB%8D/
https://my.oschina.net/OutOfMemory/blog/300173
http://blog.csdn.net/ugg/article/details/41894947
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值