事务
redis中的事务是一组命令的集合。使得一个事务中的redis命令要么全执行,要么全不执行
使用方式: multi 和exec完成
multi: 告诉redis将同一个事务的命令存储起来。 之后发送两个SADD, redis返回queued表示命令进入了等待执行的事务队列中。
exec:则是告知redis按照发送顺序执行命令。一旦客户端发送exec名,所有命令将执行,即使此后客户端断线,没有关系。
错误处理
1>语法错误。 命令不存在或者命令参数个数不对。
只要有一个命令有语法错误,执行exec命令后redis就会直接返回错误,连正确的命令也不会执行。
2> 运行错误。指的是命令执行时出现错误。比如使用散列类型的命令操作集合类型的键。这种错误在实际执行前redis是无法发现的。所以在事务里这样的命令会被redis接受并执行。如果事务里的一条命令出现了运行错误,事务的其他命令
依然会继续执行(包括出错命令后面的命令)
redis的事务没有提供回滚的功能,因此开发者必须在事务执行后自己将数据库复原到原来执行前的状态。
这两种错误,其中语法错误是能在开发是找出并解决的。
WATCH命令介绍
在一个事务中只有当所有命令都依次执行完才能得到每个结果的返回值,可是有些情况需要先获得一条命令的返回值。然后在根据这个值执行下一条命令。
比如get,set 命令实现increase时候就会出现竞态条件。
如果将两个命令放到一个事务中呢, 但是由于事务中的每个命令的执行结果都是一起返回的,无法将前一条命令的结果作为下一条命令的参数。也就是在set前无法获取到get命令的返回值。
解决思路:将get获得键值后保证改键值不被其他客户端修改,直到函数执行完成后才允许其他客户端操作,这样也可以防止竞态条件。
watch命令可以监控一个键或者多个键,一旦其中一个键被修改(删除),之后的事务就不会执行,监控一直持续到exec命令(事务中的命令是在exec之后才执行的,所以在multi命令后可以修改watch监控的键值)
watch后面的事务不执行,key的结果还是2
这样,通过watch命令实现incr命令
对于上面函数来说,由于watch命令只是当被监控的键值被修改后阻止后面事务的执行,如果这次执行失败了,exec没有执行成功,key还在在watch的,不能保证其他客户端不修改这个值,也就是还是可能阻止其他事务执行的,必须重新执行这个函数。
执行exec命令后会取消对所有键的监控,如果不想执行事务中的命令可以使用UNwatch命令来取消监控
举个例子来说,实现hsetnx命令的功能
def hsetxx($key,$field,$value)
watch $key
$isFieldExists = HEXISTS $key,$field
if $isFieldExists is 1
MULTI
HSET $key,$field,$value
EXEC
else
UNWATCH
return $isFieldExists
如果键不存在,就不执行事务中的命令,并取消监控
过期时间
实际使用开发中,遇到一些时效的数据,比如限时优惠活动,缓存或验证码等。过了一定的时间就需要删除这些数据。在关系数据库中需要额外的一个字段记录到期时间,然后定期检测删除过期数据。而在redis中可以使用expire命令设置一个键的过期时间,到时间redis自动删除它。
命令
1. expire key seconds seconds参数表示键的过期时间,单位是秒
返回: 1 表示成功 0 表示 键不存在或者设置失败
2. ttl命令获取key的剩余时间(秒),如果键不存在ttl命令返回的是-2,-1表示键永久存在(这是建立一个键后的默认情况)
note: 2.6版中无论键不存在还是没有过期时间都返回-1,直到2.8才区分返回-2和-1
3. 取消键的过期时间(将键恢复成永久的),则可以使用persist命令,如果过期时间被成功清除则返回1,否则返回0
(因为键不存在或者键本来就是永久的)
除了persist命令外,使用set或者getset命令为键赋值也会同时清除键的过期时间,但是其他对键值进行操作的
命令(INCR,LPUSH,HSET,ZREM)均不会影响键的过期时间
4. 精确控制键的过期时间 pexpire命令, pexpire key ms (过期时间为毫秒)
另外还有两个相对不常用的命令,expireat 和 pexpireat
expireat命令和expire命令的区别在于前者使用unix时间作为第二个参数表示键的过期时刻
pexpireat名和expireat命令的区别是前者的单位是ms
过期时间的应用
1. 实现访问频率的限制
需要实现每个ip每分钟只能访问100个页面,给每个ip设置一个key 过期时间设置为60秒,每次访问自增一次。
超过一百次,限制访问
伪代码:
$isKeyExists = EXISTS rate.limitling:$IP
if $isKeyExists is 1
$times = INCR rate.limitling:$IP
if $times>100
print 访问频率超过限制,稍后再试
exist
else
INCR rate.limitling:$IP
EXPIRE $keyName,60
如果设置过期时间失败,前面的命令执行成功,那么ip最多访问100次
解决:将 incr和expire命令放在一个事务里,修改伪代码如下
multi
INCR rate.limiting:$IP
EXPIRE $keyname,60
exec
继续深入:
如果一个用户在一分钟的第一秒访问了一次博客,同一分钟的最后一秒访问了9次,又在下一分钟的第一秒访问了10秒,
这样的访问是可以通过限制的访问频率限制的,但是实际上用户在两秒内访问了19次博客,这和每分钟访问10次的限制差距较大。
优化:
对每个用户,使用一个列表类型的键来记录他最近10次的访问博客时间,一旦键中的元素超过了10个,就判断时间最早的元素据现在的时间是否小于1分钟,如果是表示用户最近一分钟的访问次数超过了10次,如果不是键现在的时间加入到列表中,将最早的删除。
$listLength = llen rate.limiting:$IP
if $listLength<10
LPUSH rate.limiting:$IP,now()
else
$time = LINDEX rate.limiting:$IP,-1
if now() - $time <60
print 访问频率超过限制,稍后再试
else
LPUSH rate.limiting:$IP,now()
LTRIM rate.limiting:$IP,0,9
当要限制"A时间最多访问B次"时,如果B较大,此方法会占用较多的存储空间,除此之外也会出现竞态条件的问题
2. 实现缓存
为了提高网站的负载能力,需要将一些访问频率较高的但是对CPU或IO资源消耗较大的结果缓存起来。并设置这些缓存过一段时间自动过期。
实际开发中利用缓存作为接口的令牌,防止多次提交,每次请求过来的时候校验是否key存在,如果存在就
抛异常多次提交,不存在就设值,并设置过期时间,接口执行完毕将key删除(放在finally中)
String key ="Y803_" + ganNo+ "_" + rpBatch;
long flag = jedis.setnx(key, String.valueOf(System.currentTimeMillis()));
if(flag ==1)
{
jedis.expire(key, time);
}else
{
throw new GessException(MarketErrCode.YX2037, "不能重复提交");
}
redis作为缓存存在的问题
当服务器内存有限时候,如果大量使用缓存键而且过期时间设置过长会导致redis占满内存;另一方面如果为了防止redis占用内存过大而将缓存键的过期时间设得太短,就可能导致缓存命中率过低并且大量内存白白闲置。实际开发中很难为缓存键设置合理的过期时间。
解决: 限制redis能使用的最大内存,并让redis按照一定的规则淘汰不需要的缓存键。
修改配置文件maxmemory参数,限制redis最大可用内存大小,当超出限制redis根据maxmemory-policy指定策略删除key