Redis下分布式锁的实现

来源:互联网 发布:华住集团 知乎 编辑:程序博客网 时间:2024/06/12 01:22
本文可作为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
0 0
原创粉丝点击