本文可作为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循环中。
如果负载很大,事务就会不断的重试!
数据库的那种锁叫悲观锁,我用的时候你不能用。
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,开始上代码
下面就是释放锁了:
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手上)
亲爱的朋友们,你们明白了么。
那具体怎么使用这个锁呢?
参考资料
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
其实,对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,能咋么?我直接删除了就是了么。反正只要我不还锁,别人都无法获得锁,能有啥问题么。
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