文档章节

Redis+Lua——他叫了外援

杨彬Lennon
 杨彬Lennon
发布于 2019/12/04 17:50
字数 1110
阅读 2.2K
收藏 16

    Redis从2.6版本开始引入对Lua脚本的支持,通过在Redis服务器中嵌入Lua环境,Redis客户端可以使用Lua脚本,直接在服务端原子的执行多个Redis命令。

Lua

    Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。

为什么用

刚需

  • 原子操作:Redis 使用单个 Lua 解释器去运行所有脚本,并且,Redis 也保证脚本会以原子性(atomic)的方式执行:当某个脚本正在运行的时候,不会有其他脚本或 Redis 命令被执行。

优势

  • 高效且灵活:Redis的事务实现是基于观察者模式的check-and-set(乐观锁)。相对而言,引入Lua脚本的方式,可以轻松实现在事务方式中难以(无法)实现的业务。
  • 减少网络开销:合并多次执行命令的网络请求,用一个请求完成,减少了网络资源开销和响应时间。
  • 复用性:客户端发送的脚本会永久存储在Redis中,这意味着其他客户端可以复用这一脚本而不需要使用代码完成同样的逻辑。此特性基于 Script Load 命令。

应用

    比如有这样一个需求:每当有新的用户注册到我的平台,为其分配一个客户经理,要求每分配30次,切换一个客户经理,我们只有两个客户经理可用。这个注册服务部署了N个实例。你可以考虑一下如何使用Redis的事务实现。

JAVA实现

	/** 通过计数器获取客户经理姓名的lua脚本 */
	/** 客户经理资源被抽象成node(环形/单向) */
	static final String ASSIGN_ACCOUNT_MANAGER_LUA_SCRIPT = "local node1 = {next = nil, value = '王尼玛经理'}\n" +
			"local node2 = {next = node1, value = '王速卡经理'}\n" +
			"node1.next = node2\n" +
			"local currentNode = node1\n" +
			"\n" +
			"local currentAccountManagerkey = KEYS[1]\n" +
			"local currentAccountManager = redis.call('get', currentAccountManagerkey)\n" +
			"\n" +
			"if currentAccountManager then\n" +
			"    if currentNode.value ~= currentAccountManager then\n" +
			"        currentNode = currentNode.next\n" +
			"    end\n" +
			"else\n" +
			"    currentAccountManager = node1.value\n" +
			"end\n" +
			"\n" +
			"local counterKey = KEYS[2]\n" +
			"local counter = redis.call('incr', counterKey)\n" +
			"\n" +
			"if(tonumber(counter) == 30) then\n" +
			"    redis.call('set', counterKey, 0)\n" +
			"    redis.call('set', currentAccountManagerkey, currentNode.next.value)\n" +
			"end\n" +
			"\n" +
			"return currentAccountManager";
	/** 客户经理姓名key*/
	static final String CURRENT_ACCOUNT_MANAGER_KEY = "assignAccountManager:account_manager";
	/** 客户经理分配计数器key*/
	static final String COUNTER_KEY = "assignAccountManager:assign_counter";

jedis方式 

@Component
public class JedisUtil {

    private JedisUtil() {
		
	}

	@Autowired
	private JedisPool jedisPool;    

	 /**
	 * 注册脚本。将脚本 script 添加到脚本缓存中,但并不立即执行这个脚本
	 * 如果给定的脚本已经在缓存里面了,那么不执行任何操作。
	 * 在脚本被加入到缓存之后,通过 EVALSHA 命令,可以使用脚本的 SHA1 校验和来调用这个脚本。
	 * 脚本可以在缓存中保留无限长的时间,直到执行 SCRIPT FLUSH 为止。
	 * @param script 脚本字符串
	 * @return 脚本 sha id
	 */
	public String scriptLoad(String script) {
		Assert.hasLength(script, "parameter [script] must not be null");
		Jedis jedis = null;
		String  luaLoad = null;
		try {
			jedis = jedisPool.getResource();
			luaLoad  = jedis.scriptLoad(script);
			return luaLoad;
		} catch (Exception e) {

		} finally {
			if(jedis != null){
				jedis.close();
			}
		}
		return null;
	}

	/**
	 * 根据给定的 sha1 校验码,执行缓存在服务器中的脚本
	 * @param scriptShaId 脚本 sha id
	 * @param keys key列表
	 * @param args arg列表
	 * @return 脚本执行结果
	 */
	public Object evalsha(String scriptShaId, List<String> keys, List<String> args) {
		Assert.hasLength(scriptShaId, "parameter [scriptShaId] must not be null");
		Assert.notNull(keys, "parameter [keys] must not be null");
		Assert.notNull(args, "parameter [args] must not be null");
		Jedis jedis = null;
		Object  val = null;
		try {
			jedis = jedisPool.getResource();
			if (jedis.scriptExists(scriptShaId)) {
				val = jedis.evalsha(scriptShaId, keys, args);
				return val;
			}
		} catch (Exception e) {

		} finally {
			if(jedis != null){
				jedis.close();
			}
		}
		return null;
	}
}
    /**
     * 为新注册的用户分配客户经理
     * @return 客户经理姓名
     */
    public String assignAccountManagerForNewAccount() {
        List<String> keys = Arrays.asList(CURRENT_ACCOUNT_MANAGER_KEY, COUNTER_KEY);
        return (String) jedis.evalsha(jedis.scriptLoad(ASSIGN_ACCOUNT_MANAGER_LUA_SCRIPT), keys, Collections.<String>emptyList());
    }

RedisTemplate方式

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 为新注册的用户分配客户经理
     * @return 客户经理姓名
     */
    public String assignAccountManagerForNewAccount() {
        List<String> keys = Arrays.asList(CURRENT_ACCOUNT_MANAGER_KEY, COUNTER_KEY);
        // 指定 lua 脚本,并且指定返回值类型
        DefaultRedisScript<String> redisScript = new DefaultRedisScript<>(ASSIGN_ACCOUNT_MANAGER_LUA_SCRIPT, String.class);
        // 参数一:redisScript,参数二:key列表
        return redisTemplate.execute(redisScript, keys);
    }    


相对于文字描述,代码和适量的注释可以直抒胸臆

© 著作权归作者所有

杨彬Lennon
粉丝 12
博文 11
码字总数 18504
作品 0
昌平
程序员
私信 提问
加载中

评论(2)

杨彬Lennon
杨彬Lennon 博主
Redis集群模式要求单个Lua脚本操作的Key必须在同一节点上
linshi2019
linshi2019
在集群模式下,如果lua里操作不同slot下的数据,是否能保证线程安全?
京东抢购服务高并发实践

作者:张子良,京东高级开发工程师,在京东负责抢购后端服务系统架构和开发工作。 服务介绍 限时抢购又称闪购,英文Flash sale,起源于法国网站Vente Privée。闪购模式即是以互联网为媒介的...

fdhay
2016/09/12
348
2
被同事传谣喜欢上司,该怎么办?

老话说,清者自清。 真的假不了,假的真不了,时间会检验一切,她愿意怎么说,就怎么说…… 上面的话,不——要——信! 这么佛系的处理,只会让坏人更缺德。 你的沉默,就是她的实锤,你的懦...

明哥聊求职
2018/09/14
0
0
秒杀技术选型

一、控制库存的技术选型有五个选择 (1)mysql乐观锁 (2)mysql悲观锁 (3)redis的原子操作incr (4)redis的watch(redis乐观锁) (5)redis执行lua脚本 二、测试结果(8核16G,300个线程...

钟主华
2016/06/30
2.2K
15
时间紧迫寻找api接口开发外援或外包

自身技术团队开发的B2C+SNS网站因时间紧迫寻找api接口开发外援或外包: 工作内容:括新浪微博、腾讯微博、twitter、网易微博、搜狐微博、人民网微博、同学网、做啥等微博接口 工作形式:外援...

Zstar
2012/08/25
340
2
深度游戏中心游戏专题更新说明(03-11)——像素游戏

锯齿轮廓和点阵字体是像素游戏的典型风格,虽然画面一般让人感觉比较粗糙,但往往都具有比较高的游戏性,特别是玩过那个时代游戏的80后人群中,有许多对这种画面风格感兴趣的。为此,小D本周...

cxbii
2014/03/11
410
0

没有更多内容

加载失败,请刷新页面

加载更多

00-Java 面试准备

面试之前 面试前准备简历需要注意的几个方面: 写简历、改简历,这个一定要干的。简历有两个作用,一个是吸引别人,能让别人邀请你去面试,这是前提;另一个是引导面试的人,让面试的人问你所...

源程序
今天
54
0
OSChina 周二乱弹 —— 大王(@罗马的王)颜值制霸Osc社区

Osc乱弹歌单(2020)请戳(这里) 【今日歌曲】 @巴拉迪维 :Lunik的单曲《Seeing You Soar》 I hope you’re smiling,When seeing me soar. #今日歌曲推荐# 《Seeing You Soar》- Lunik 手...

小小编辑
今天
75
0
wordcount代码

1.写出map类 public class WCMapper extends Mapper<LongWritable,Text,Text,LongWritable>{ @Override protected void map(LongWritable key,Text value,Context context)throws IOExcepti......

七宝1
今天
59
0
Spring Batch 小任务(Tasklet)步骤

Chunk-Oriented Processing不是处理 step 的唯一方法。 考虑下面的一个场景,如果你仅仅需要调用一个存储过程,你可以在 ItemReader 中实现这个调用,然后在存储过程完成调用后返回 null。这...

honeymoose
今天
67
0
Linux日志分析

1. Linux日志文件的类型 2. 系统服务日志 2.1 syslogd的简介 2.2 syslogd的配置和使用 2.3 日志的安全性设置 2.4 远程日志记录服务 3. 日志的轮替 3.1 logrotate简介 3.2 logrotate的配置 3....

JiaMing
昨天
67
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部