redis的原子操作有两种方式:
1.把多个操作在redis中实现成一个操作,也就是单命令操作。
我们先看看redis本身单命令操作:
redis是使用单线程来串行处理客户端请求操作命令的,所以当redis处理某个命令的时候,其他命令是无法执行的,这相当于命令操作是互斥执行的,当然redis的快照生成、AOF重写等操作是由子线程或者后台线程执行的,也就是和主线程的操作是并行执行的。只不过这些操作只是读取数据不会修改数据。

虽然redis单个操作可以原子性的执行,但在实际应用中数据修改包含多个操作,至少包括读数据、数据增减、写回数据三个操作,这显然就不是单个命令操作了,那该怎么办呢?
redis提供了INCR/DECR命令,把这三个命令转变为一个原子操作了。
例如对商品的id库存值-1

DECR id 

2.将多个操作写入到一个Lua脚本中,以原子的方式执行一个Lua脚本。
如下所示,如果两个线程同时执行value = INCR(ip),就会导致value=2,就无法为这个ip设置60超时时间。

//获取ip对应的访问次数
current = GET(ip)
//如果超过访问次数超过20次,则报错
IF current != NULL AND current > 20 THEN
    ERROR "exceed 20 accesses per second"
ELSE
    //如果访问次数不足20次,增加一次访问计数
    value = INCR(ip)
    //如果是第一次访问,将键值对的过期时间设置为60s后
    IF value == 1 THEN
        EXPIRE(ip,60)
    END
    //执行其他操作
    DO THINGS
END

所以就需要通过Lua进行设置,


local current
current = redis.call("incr",KEYS[1])
if tonumber(current) == 1 then
    redis.call("expire",KEYS[1],60)
end

然后执行redis-cli --eval lua.script keys , args 命令就可以保证并发的原子性

Redis如何实现分布式锁?

在分布式场景下,锁变量需要由一个共享存储系统来维护,只有这样,多个客户端才可以通过访问共享存储系统来访问锁变量。
这样我们得到分布式锁的两个需求:
1.分布式锁的加锁和释放锁的过程涉及到多个操作。所以在实现分布式锁时,我们需要保证这些锁操作的原子性
2.由于锁变量保存在存储系统中,如果存储系统发生故障或者宕机就会导致客户端无法进行锁操作,在实现分布式锁时,我们需要考虑保证共享存储系统的可靠性,进而保证锁的可靠性。

基于单个redis实现的分布式锁
在这里插入图片描述

我们先来看一下redis中有哪些单命令操作实现加锁操作?
通过SETNX和DEL命令完成加锁和释放锁操作,

// 加锁
SETNX lock_key 1
// 业务逻辑
DO THINGS
// 释放锁
DEL lock_key

但是使用SETNX和DEL存在两个风险:
1.如果客户端在执行加锁后的业务逻辑操作出现异常,就会导致锁无法释放,解决办法:给锁变量设置一个过期时间。

SET key value [EX seconds | PX milliseconds]  [NX]

2.不同用户操作导致的锁冲突,可以通过给每一个客户端设置唯一性的标识

// 加锁, unique_value作为客户端唯一性的标识
SET lock_key unique_value NX PX 10000

其中unique_value代表客户端的唯一标识,可以用一个随机生成的字符串表示,PX 10000表示lock_key会在10s后过期。
如何释放锁呢?其中KEYS[1]表示lock_key,ARGV[1]表示当前客户端的唯一标识

//释放锁 比较unique_value是否相等,避免误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

基于多个redis节点实现高可用的分布式锁
分布式锁算法RedLock:基本思想是如果客户端能够和半数以上的实例完成加锁操作,那么我们就可以认为客户端成功的获得了分布式锁了,否则加锁失败。
RedLock算法的实现需要有N个独立的Redis实例,会被分为3个步骤来完成操作:
1.客户端的获取当前时间。
2.客户端会按顺序依次向N个redis实例执行加锁操作。
3.一旦客户端完成了所有和redis实例的加锁操作,客户端就需要计算整个加锁过程的总耗时。客户端只有在满足下面的这两个条件时,才能认为是加锁成功。
条件一:客户端从超过半数(大于等于 N/2+1)的 Redis 实例上成功获取到了锁;
条件二:客户端获取锁的总耗时没有超过锁的有效时间。

在满足了这两个条件后,我们需要重新计算这把锁的有效时间,计算的结果是锁的最初有效时间减去客户端为获取锁的总耗时。如果锁的有效时间已经来不及完成共享数据的操作了,我们可以释放锁,以免出现还没完成数据操作,锁就过期了的情况。
当然,如果客户端在和所有实例执行完加锁操作后,没能同时满足这两个条件,那么,客户端向所有 Redis 节点发起释放锁的操作。
在 Redlock 算法中,释放锁的操作和在单实例上释放锁的操作一样,只要执行释放锁的 Lua 脚本就可以了。这样一来,只要 N 个 Redis 实例中的半数以上实例能正常工作,就能保证分布式锁的正常工作了。

事务机制:redis能实现ACID属性吗?

redis如何实现事务?
1.MULTI表示显示的开启事务
2.客户端把事务中本身要执行的操作发送给服务器端,redis会把这些命令放入到队列中
3.EXEC表示开始执行事务提交的


#开启事务
127.0.0.1:6379> MULTI
OK
#将a:stock减1,
127.0.0.1:6379> DECR a:stock
QUEUED
#将b:stock减1
127.0.0.1:6379> DECR b:stock
QUEUED
#实际执行事务
127.0.0.1:6379> EXEC
1) (integer) 4
2) (integer) 9

原子性
第一种情况:事务操作入队时,redis检查出错误命令。
EXEC执行事务的时候,如果提交的命令有错误会直接报错。

#开启事务
127.0.0.1:6379> MULTI
OK
#发送事务中的第一个操作,但是Redis不支持该命令,返回报错信息
127.0.0.1:6379> PUT a:stock 5
(error) ERR unknown command `PUT`, with args beginning with: `a:stock`, `5`, 
#发送事务中的第二个操作,这个操作是正确的命令,Redis把该命令入队
127.0.0.1:6379> DECR b:stock
QUEUED
#实际执行事务,但是之前命令有错误,所以Redis拒绝执行
127.0.0.1:6379> EXEC
(error) EXECABORT Transaction discarded because of previous errors.

第二种情况:事务操作入队时,redis没有检查出错误命令。这种情况就不符合事务的原子性

#开启事务
127.0.0.1:6379> MULTI
OK
#发送事务中的第一个操作,LPOP命令操作的数据类型不匹配,此时并不报错
127.0.0.1:6379> LPOP a:stock
QUEUED
#发送事务中的第二个操作
127.0.0.1:6379> DECR b:stock
QUEUED
#实际执行事务,事务第一个操作执行报错
127.0.0.1:6379> EXEC
1) (error) WRONGTYPE Operation against a key holding the wrong kind of value
2) (integer) 8

在这里你可能会有一个疑问:MySQL的在执行事务的时候,会有回滚机制,当事务执行发生错误的时候,事务中的所有操作都会撤销。但是redis中并没有提供回滚机制。虽然redis中提供了DISCARD命令,但是这个命令只能用来主动放弃事务执行,把暂存队列情况起不到回滚的效果。

#读取a:stock的值4
127.0.0.1:6379> GET a:stock
"4"
#开启事务
127.0.0.1:6379> MULTI 
OK
#发送事务的第一个操作,对a:stock减1
127.0.0.1:6379> DECR a:stock
QUEUED
#执行DISCARD命令,主动放弃事务
127.0.0.1:6379> DISCARD
OK
#再次读取a:stock的值,值没有被修改
127.0.0.1:6379> GET a:stock
"4"

第三种情况:在执行事务的EXEC命令的时候,redis实例发生故障导致事务执行失败。
在这种情况下,如果redis开启了AOF日志,那么只有一部分事务操作会被记录到AOF日志中。我们使用redis-check-aof工具检查AOF日志文件,这个工具可以把未完成的事务操作从AOF文件中剔除掉,这样一来,我们使用 AOF 恢复实例后,事务操作不会再被执行,从而保证了原子性。

一致性:数据库中的数据在事务执行前后是一致的
情况一:命令入队时就报错
在这种情况下,事务本身就会被放弃执行,所以保证了一致性
情况二:命令入队时没报错,执行时报错了
在这种情况下,有错误的命令不会被执行,正确的命令可以正常执行,也不会改变数据库的一致性。
情况三:EXEC 命令执行时实例发生故障
在这种情况下,实例故障后会进行重启,这就和数据恢复的方式有关了,我们要根据实例是否开启了 RDB 或 AOF 来分情况讨论下。
如果我们没有开启 RDB 或 AOF,那么,实例故障重启后,数据都没有了,数据库是一致的。

如果我们使用了 RDB 快照,因为 RDB 快照不会在事务执行时执行,所以,事务命令操作的结果不会被保存到 RDB 快照中,使用 RDB 快照进行恢复时,数据库里的数据也是一致的。

如果我们使用了 AOF 日志,而事务操作还没有被记录到 AOF 日志时,实例就发生了故障,那么,使用 AOF 日志恢复的数据库数据是一致的。如果只有部分操作被记录到了 AOF 日志,我们可以使用 redis-check-aof 清除事务中已经完成的操作,数据库恢复后也是一致的。

所以,总结来说,在命令执行错误或 Redis 发生故障的情况下,Redis 事务机制对一致性属性是有保证的。接下来,我们再继续分析下隔离性。

隔离性:隔离性是指数据库在执行一个事务的时候,其他操作无法获取到正在执行事务访问的数据
分为两种情况:
1.并发操作在EXEC命令前执行,此时隔离性保障需要使用WATCH机制来实现,否则隔离性将无法保障。
WATCH机制的作用,在事务执行前监控一个或者多个键的值变化情况,当事务调用EXEC命令执行时,watch机制会检查键是否被其他客户端修改了。如果修改了,就放弃事务执行,避免事务的隔离性被破坏。然后,客户端可以再次执行事务,此时,如果没有并发修改事务数据的操作了,事务就能正常执行,隔离性也得到了保证。
在这里插入图片描述
2.并发操作在EXEC命令后执行,此时隔离性能保证

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐