nx set 怎么实现的原子性_【redis进阶(1)】redis的Lua脚本控制(原子性)

[toc]

为什么要用lua

减少网络开销:本来5次网络请求的操作,可以用一个请求完成,原先5次请求的逻辑放在redis服务器上完成。使用脚本,减少了网络往返时延。

原子操作:Redis会将整个脚本作为一个整体执行,中间不会被其他进程或者进程的命令插入。(最重要)

复用:客户端发送的脚本会永久存储在Redis中,意味着其他客户端可以复用这一脚本而不需要使用代码完成同样的逻辑。

串讲lua

相关链接与参考

主要用到的语法: 注释,变量,方法调用和声明,循环,流程控制

编辑器与调试

下载安装:http://luabinaries.sourceforg...

IDE编辑器:Settings -> Plugins -> Marketplace -> 搜索并安装EmmyLua

redis执行lua

eval

使用EVAL命令对 Lua 脚本进行求值

EVAL script numkeys key [key ...] arg [arg ...]

[info] numkeys : keys的数量有几个。这是一个必传的参数,即使没有keys也要传个0;

# 注意redis的计数是从1开始的

127.0.0.1:6379> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second

1) "key1"

2) "key2"

3) "first"

4) "second"

脚本缓存

EVAL命令会将脚本添加到脚本缓存中,并且会立即对输入的脚本进行求值。

如果给定的脚本已经在缓存里面了,那么不做动作。

在脚本被加入到缓存之后,通过 EVALSHA 命令,可以使用脚本的 SHA1 校验和来调用这个脚本。

脚本可以在缓存中保留无限长的时间,直到执行SCRIPT FLUSH为止。

redis> SCRIPT LOAD "return 'hello moto'"

"232fd51614574cf0867b83d384a5e898cfd24e5a"

# 判断脚本是否存在

redis> SCRIPT EXISTS 232fd51614574cf0867b83d384a5e898cfd24e5a

1) (integer) 1

redis> EVALSHA 232fd51614574cf0867b83d384a5e898cfd24e5a 0

"hello moto"

# 清空缓存

redis> SCRIPT FLUSH

OK

call与pcall

在 Lua 脚本中,可以使用两个不同函数来执行 Redis 命令,它们分别是:

redis.call()

redis.pcall()

# 0表示没有keys

> eval "return redis.call('set','foo','bar')" 0

OK

# 以参数的形式传入

> eval "return redis.call('set',KEYS[1],'bar')" 1 foo

OK

redis.call()和redis.pcall()的唯一区别在于它们对错误处理的不同。redis.pcall()出错时并不引发(raise)错误,而是返回一个带err域的 Lua 表(table) ,用于表示错误:

redis 127.0.0.1:6379> EVAL "return redis.pcall('get', 'foo')" 0

(error) ERR Operation against a key holding the wrong kind of value

redis中已预先加载的lua库

Redis 内置的 Lua 解释器加载了以下 Lua 库:

base

table

string

math

debug

cjson

cmsgpack

其中cjson库可以让 Lua 以非常快的速度处理 JSON 数据,除此之外,其他别的都是 Lua 的标准库。

每个 Redis 实例都保证会加载上面列举的库,从而确保每个 Redis 脚本的运行环境都是相同的。

全局变量保护

为了防止不必要的数据泄漏进 Lua 环境, Redis 脚本不允许创建全局变量。如果一个脚本需要在多次执行之间维持某种状态,它应该使用 Redis key 来进行状态保存。

redis 127.0.0.1:6379> eval 'a=10' 0

(error) ERR Error running script (call to f_933044db579a2f8fd45d8065f04a8d0249383e57): user_script:1: Script attempted to create global variable 'a'

日志打印

在 Lua 脚本中,可以通过调用redis.log函数来写 Redis 日志(log):

redis.log(loglevel,message)

其中,message参数是一个字符串,而loglevel参数可以是以下任意一个值:

redis.LOG_DEBUG

redis.LOG_VERBOSE

redis.LOG_NOTICE

redis.LOG_WARNING

打印的日志在redis日志文件中,redis的日志文件可以在其配置里面找logfile。默认是没有的。redis必须带配置文件启动,如果直接启动的话,它会使用默认配置(而且并不存在这个默认配置文件,所以不要想改它)。

redis-cli测试脚本

[root@test-02 bin]# cat test.lua

return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}

# 通过逗号来分割key和arg,注意,这个逗号必须前后要有空格

[root@test-02 bin]# ./redis-cli --eval test.lua key1 key2 , first second

1) "key1"

2) "key2"

3) "first"

4) "second"

[info] 注意: 逗号前后必须要有空格。

PHP中调用

$redis->eval($lua,array('key1','key2','first','second'),2)

$lua = <<

SCRIPT;

//对应的redis命令如下 eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second

$s = $redis->eval($lua,array('key1','key2','first','second'),2);

实战redis的lua脚本应用

频率控制

10秒内只能访问3次。 后续该脚本可以在nginx或者程序运行脚本中直接使用,判断返回是否为0,就0就不让其继续访问。

-- redis-cli --eval ratelimiting.lua rate.limitingl:127.0.0.1 , 10 3

-- rate.limitingl + 1

local times = redis.call('incr',KEYS[1])

-- 第一次访问的时候加上过期时间10秒(10秒过后从新计数)

if times == 1 then

redis.call('expire',KEYS[1], ARGV[1])

end

-- 注意,从redis进来的默认为字符串,lua同种数据类型只能和同种数据类型比较

if times > tonumber(ARGV[2]) then

return 0

end

return 1

以上,如果不使用redis+lua,那高并发下incr和expire就会出现原子性破坏,造成expire执行多次浪费

延时队列

Zset 里面存储的是 Value/Score 键值对,我们将 Value 存储为序列化的任务消息,Score 存储为下一次任务消息运行的时间(Deadline),然后轮询 Zset 中 Score 值大于 Now 的任务消息进行处理。

# 生产延时消息

zadd(queue-key, now_ts+5, task_json)

# 消费延时消息

while True:

task_json = zrevrangebyscore(queue-key, now_ts, 0, 0, 1)

if task_json:

grabbed_ok = zrem(queue-key, task_json)

if grabbed_ok:

process_task(task_json)

else:

sleep(1000) // 歇 1s

当消费者是多线程或者多进程的时候,这里会存在竞争浪费问题。当前线程明明将 task_json 从 Zset 中轮询出来了,但是通过 Zrem 来争抢时却抢不到手。

这时就可以使用 LUA 脚本来解决这个问题,将轮询和争抢操作原子化,这样就可以避免竞争浪费。

local res = nil

local tasks = redis.pcall("zrevrangebyscore", KEYS[1], ARGV[1], 0, "LIMIT", 0, 1)

if #tasks > 0 then

local ok = redis.pcall("zrem", KEYS[1], tasks[1])

if ok > 0 then

res = tasks[1]

end

end

return res

自增ID

local key = KEYS[1]

local id = redis.call('get',key)

if(id == false)

then

redis.call('set',key,1)

return key.."0001"

else

redis.call('set',key,id+1)

return key..string.format('%04d',id + 1)

end

通过lua使get和set命令原子化,杜绝高并发下的

秒杀或者抢红包

业务需求: 每次只允许领取10个红包

操作流程:判断是否能抢->抢到红包->记录抢到红包的人->异步发红包

解决问题:高并发下的红包超发(或者商品超卖),判断能否抢和抢一定要原子性的捆绑在一起,否则就会出现超发

-- 抢红包脚本

--[[

--red:list 为 List 结构,存放预先生成的红包金额

red:draw_count:u:openid 为 k-v 结构,用户领取红包计数器

red:draw为 Hash 结构,存放红包领取记录

red:task 也为 List 结构,红包异步发放队列

openid 为用户的openid

]]--

local openid = KEYS[1]

local isDraw = redis.call("HEXISTS","red:draw",openid)

-- 已经领取

if isDraw ~= 0 then

return true

end

-- 领取太多次了

local times = redis.call("INCR","red:draw_count:u:"..openid)

if times and tonumber(times) > 9 then

return 0

end

local number = redis.call("RPOP","red:list")

-- 没有红包

if not number then

return {}

end

-- 领取人昵称为Fhb,头像为 https:// xxxxxx

local red = {money=number,name=KEYS[2] , pic = KEYS[3] }

-- 领取记录

redis.call("HSET","red:draw",openid,cjson.encode(red))

-- 处理队列

red["openid"] = openid

redis.call("RPUSH","red:task",cjson.encode(red))

return true

分布式锁

Redis在 2.6以前的版本用setnx做分布式锁的时候,会出现setnx 和 expire遭到原子性破坏的可能,必须要配合lua脚本来实现原子性。但在2.6.12 版本开始,为 SET 命令增加了一系列选项:

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

EX seconds:设置指定的过期时间,单位秒。

PX milliseconds:设置指定的过期时间,单位毫秒。

NX:仅当key不存在时设置值。

XX:仅当key存在时设置值。

可以看出来, SET 命令的天然原子性完全可以取代 SETNX 和 EXPIRE 命令。

/**

* redis排重锁

* @param $key

* @param $expires

* @param int $value

* @return mixed

*/

public function redisLock($key, $expires, $value = 1)

{

//在key不存在时,添加key并$expires秒过期

return $this->redis->set($key, $value, ['nx', 'ex' => $expires]);

}

[info] 总结:凡是需要多条redis命令需要捆绑在一起原子性操作的,都要使用lua来实现。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java Redis分布式锁是一种常用的实现方式,可以通过Redis的原子操作来实现分布式锁的功能。下面是一个Java Redis分布式锁的代码示例: ```java import redis.clients.jedis.Jedis; public class RedisDistributedLock { private static final String LOCK_KEY = "lock_key"; private static final int LOCK_EXPIRE_TIME = 30000; // 锁的过期时间,单位毫秒 private static final int ACQUIRE_TIMEOUT = 5000; // 获取锁的超时时间,单位毫秒 private Jedis jedis; public RedisDistributedLock(Jedis jedis) { this.jedis = jedis; } public boolean acquireLock() { long startTime = System.currentTimeMillis(); try { while (true) { // 尝试获取锁 String result = jedis.set(LOCK_KEY, "locked", "NX", "PX", LOCK_EXPIRE_TIME); if ("OK".equals(result)) { return true; // 获取锁成功 } // 判断是否超时 long currentTime = System.currentTimeMillis(); if (currentTime - startTime > ACQUIRE_TIMEOUT) { return false; // 获取锁超时 } // 等待一段时间后重试 Thread.sleep(100); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); return false; } } public void releaseLock() { jedis.del(LOCK_KEY); } } ``` 上述代码中,我们使用了Redis的`set`命令来尝试获取锁。如果获取成功,则返回"OK";如果获取失败,则说明锁已被其他线程占用,需要等待一段时间后重试。在获取锁的过程中,我们设置了超时时间,避免长时间等待。 使用示例: ```java import redis.clients.jedis.Jedis; public class Main { public static void main(String[] args) { Jedis jedis = new Jedis("localhost", 6379); RedisDistributedLock lock = new RedisDistributedLock(jedis); if (lock.acquireLock()) { try { // 获取到锁后执行业务逻辑 System.out.println("执行业务逻辑..."); } finally { lock.releaseLock(); // 释放锁 } } else { System.out.println("获取锁超时"); } jedis.close(); } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值