Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Redis

เชี่ยวชาญ Redis Scripting ด้วย Lua: คู่มือปฏิบัติจริง

คุณเคยได้ยินว่า Redis มีภาษาสคริปต์ฝังตัว แต่ยังไม่ได้ลอง? นี่คือทัวร์สิ่งที่คุณต้องเข้าใจเพื่อใช้พลังของ Lua กับ Redis server ของคุณ

Hello, Lua!

สคริปต์ Redis Lua แรกของเราแค่คืนค่าโดยไม่ได้โต้ตอบกับ Redis อย่างมีความหมาย:

local msg = "Hello, world!" return msg

ง่ายที่สุดแล้ว บรรทัดแรกตั้งค่าตัวแปร local ด้วยข้อความ บรรทัดที่สองคืนค่าจาก Redis server ไปยัง client บันทึกไฟล์นี้เป็น hello.lua และรันแบบนี้:

redis-cli --eval hello.lua

ปัญหาการเชื่อมต่อ?

ตัวอย่าง redis-cli นี้สมมติว่าคุณรัน Redis server ในเครื่องเดียว หากทำงานกับ remote server อย่าง RedisGreen ต้องระบุ host และ port หาปุ่ม Connect บนแดชบอร์ด RedisGreen เพื่อคัดลอกข้อมูลล็อกอินเร็ว

ดูเพิ่ม: Could not connect to Redis at 127.0.0.1:6379: Connection refused.

การรันจะพิมพ์ "Hello, world!" อาร์กิวเมนต์แรกของ EVAL คือ Lua script เต็ม – ที่นี่เราใช้คำสั่ง cat อ่านสคริปต์จากไฟล์ อาร์กิวเมนต์ที่สองคือจำนวน Redis keys ที่สคริปต์จะเข้าถึง สคริปต์ "Hello World" เราไม่เข้าถึง keys ใดๆ จึงใช้ 0

การเข้าถึง Keys และ Arguments

สมมติเราสร้าง URL-shortener ทุกครั้งที่ URL เข้ามา เราอยากเก็บและคืนหมายเลขเฉพาะที่ใช้เข้าถึง URL นั้นภายหลัง

เราจะใช้ Lua script เพื่อได้ unique ID จาก Redis ด้วย INCR และทันทีเก็บ URL ใน hash ที่ keyed โดย unique ID นั้น:

local link_id = redis.call("INCR", KEYS[1]) redis.call("HSET", KEYS[2], link_id, ARGV[1]) return link_id

เราเข้าถึง Redis ครั้งแรกที่นี่ ผ่านฟังก์ชัน call() อาร์กิวเมนต์ของ call() คือคำสั่งส่งไป Redis: เริ่ม INCR <key> แล้ว HSET <key> <field> <value> คำสั่งทั้งสองรัน sequential — Redis ไม่ทำอะไรขณะสคริปต์รัน และรันเร็วมาก

เราเข้าถึงสอง Lua tables คือ KEYS และ ARGV Tables เป็น associative arrays และเป็นกลไกเดียวของ Lua สำหรับจัดโครงสร้างข้อมือ สำหรับจุดประสงค์ของเรา คิดว่าเทียบเท่า array ในภาษาที่คุณคล่อง แต่ระวังสองเรื่องของ Lua ที่มักหลงคนใหม่:

  • Tables เป็น one-based คือ index เริ่มที่ 1 element แรกใน mytable คือ mytable[1] ตัวที่สองคือ mytable[2] ฯลฯ

  • Tables ไม่เก็บค่า nil ได้ หากการดำเนินการให้ table เป็น [ 1, nil, 3, 4 ] ผลลัพธ์จะเป็น [ 1 ] — table ถูก truncate ที่ค่า nil แรก

ตอน invoke สคริปต์ ต้องส่งค่าสำหรับ tables KEYS และ ARGV ด้วย ใน Redis protocol แบบดิบ คำสั่งจะเป็นแบบนี้:

EVAL $incrset.lua 2 links:counter links:url https://malcolmgladwellbookgenerator.com/

ตอนเรียก EVAL หลังสคริปต์ ให้ 2 เป็นจำนวน KEYS ที่จะเข้าถึง แล้ว list KEYS และสุดท้ายค่าสำหรับ ARGV

ปกติเวลาสร้างแอปกับ Redis Lua scripts ไลบรารีไคลเอ็นต์ Redis จะจัดการการระบุจำนวน keys ให้ บล็อกโค้ดข้างต้นแสดงเพื่อความสมบูรณ์ แต่นี่คือวิธีง่ายกว่าบรรทัดคำสั่ง:

redis-cli --eval incrset.lua links:counter links:urls , https://malcolmgladwellbookgenerator.com/

ตอนใช้ --eval ตามข้างต้น คอมม่าแยก KEYS[] จาก ARGV[] items

เพื่อให้ชัดเจน นี่คือสคริปต์เดิมอีกครั้ง พร้อม KEYS และ ARGV ขยายแล้ว:

local link_id = redis.call("INCR", "links:counter") redis.call("HSET", "links:urls", link_id, "https://malcolmgladwellbookgenerator.com") return link_id

ตอนเขียน Lua scripts สำหรับ Redis ทุก key ที่เข้าถึงควรเข้าถึงผ่าน table KEYS เท่านั้น Table ARGV ใช้สำหรับ parameter-passing — ที่นี่คือค่า URL ที่เราอยากเก็บ

ตรรกะเงื่อนไข: increx และ hincrex

ตัวอย่างข้างต้นเก็บ link สำหรับ URL-shortener แต่เราต้องติดตามจำนวนครั้งที่ URL ถูกเข้าถึง ด้วยการเก็บ counter ใน hash ใน Redis เมื่อ user มาพร้อม link identifier เราจะเช็คว่ามีอยู่ไหม และเพิ่ม counter ถ้ามี:

if redis.call("HEXISTS", KEYS[1], ARGV[1]) == 1 then return redis.call("HINCRBY", KEYS[1], ARGV[1], 1) else return nil end

ทุกครั้งที่มีคนคลิก shortlink เรารันสคริปต์นี้ติดตามว่า link ถูกแชร์อีกครั้ง เรา invoke สคริปต์ด้วย EVAL ส่ง links:visits สำหรับ single key และ link identifier ที่ได้จากสคริปต์ก่อนหน้าเป็น single argument

สคริปต์จะดูเกือบเหมือนกันโดยไม่มี hashes นี่คือสคริปต์ที่เพิ่มค่า standard Redis key เฉพาะถ้ามันมีอยู่:

if redis.call("EXISTS",KEYS[1]) == 1 then return redis.call("INCR",KEYS[1]) else return nil end

SCRIPT LOAD และ EVALSHA

จำไว้ว่าเมื่อ Redis กำลังรัน Lua script มันจะไม่รันอะไรอื่น สคริปต์ที่ดีที่สุดแค่ขยายคำศัพท์ Redis ของ atomic data operations เล็กๆ ด้วยตรรกะน้อยที่สุดจำเป็น Bugs ใน Lua scripts อาจล็อก Redis server ทั้งหมด — ควรทำให้สั้นและง่ายต่อการ debug

แม้จะสั้นมาก เราก็ไม่ต้องระบุ Lua script เต็มทุกครั้งที่จะรัน ในแอปจริง คุณจะจดทะเบียน Lua scripts ทุกตัวกับ Redis ตอนแอปบูท (หรือตอน deploy) แล้วเรียกสคริปต์ด้วย SHA-1 identifier เฉพาะตัว

redis-cli SCRIPT LOAD "return 'hello world'" # "5332031c6b470dc5a0dd9b4bf2030dea6d65de91" redis-cli EVALSHA 5332031c6b470dc5a0dd9b4bf2030dea6d65de91 0 # "hello world"

การเรียก SCRIPT LOAD แบบ explicit มักไม่จำเป็นในแอป live เพราะ EVAL โหลดสคริปต์ที่ส่งไปโดยอัตโนมัติ แอปสามารถพยายาม EVALSHA แบบ optimistic และ fallback ไป EVAL ก็ต่อเมื่อสคริปต์ไม่เจอ

ถ้าคุณโปรแกรมเมอร์ Ruby ให้ดู Shopify's Wolverine ที่ทำให้การโหลดและเก็บ Lua scripts ง่ายขึ้นสำหรับ Ruby apps สำหรับ PHP programmers Predis รองรับการเพิ่ม Lua scripts ให้เรียกได้เหมือนคำสั่ง Redis ปกติ หากคุณใช้เครื่องมือเหล่านี้หรืออื่นๆ เพื่อมาตรฐานการโต้ตอบกับ Lua แจ้งเรา — เราสนใจดูว่ามีอะไรอยู่อีก

เมื่อไหร่ควรใช้ Lua?

การสนับสนุน Lua ของ Redis ทับซ้อนกับบล็อก WATCH/MULTI/EXEC ที่จัดกลุ่ม operations ให้รันร่วมกัน แล้วเลือกอย่างไร? ทุก operation ใน MULTI block ต้องเป็นอิสระ แต่ด้วย Lua operations ต่อๆ ไปสามารถพึ่งพาผลลัพธ์ของ operations ก่อนหน้าได้ การใช้ Lua scripts ยังหลีกเลี่ยง race conditions ที่อาจทำให้ slow clients เหลืออาหารเมื่อใช้ WATCH

จากที่เราเห็นที่ RedisGreen แอปส่วนใหญ่ที่ใช้ Lua จะใช้ MULTI/EXEC ด้วย แต่ไม่ใช่ทางกลับกัน สคริปต์ Lua ที่ประสบความสำเร็จส่วนใหญ่เล็ก และแค่ implement ฟีเจอร์เดียวที่แอปต้องการแต่ไม่มีในคำศัพท์ Redis

เยี่ยมชม Library

Redis Lua interpreter โหลด libraries 7 ตัว: base, table, string, math, debug, cjson, และ cmsgpack ตัวแรกๆ เป็น standard libraries ให้คุณทำ basic operations ที่คาดหวังจากภาษาไหนก็ได้ สองตัวท้ายให้ Redis เข้าใจ JSON และ MessagePack — ฟีเจอร์ที่มีประโยชน์มาก และผมสงสัยว่าทำไมไม่เห็นใช้บ่อยกว่านี้

Web apps ที่มี public APIs มักมี JSON กระจายอยู่ทั่วไป อาจมี JSON blobs เยอะเก็บใน Redis keys ธรรมดา และอยากเข้าถึงค่าบางอย่างข้างใน เหมือนเก็บเป็น hash ด้วย Redis JSON support นี่ง่ายมาก:

if redis.call("EXISTS", KEYS[1]) == 1 then local payload = redis.call("GET", KEYS[1]) return cjson.decode(payload)[ARGV[1]] else return nil end

ที่นี่เช็คว่าคีย์มีอยู่ คืน nil น่ะถ้าไม่มี แล้วได้ JSON value จาก Redis parse ด้วย cjson.decode() และคืนค่าที่ขอ

redis-cli set apple '{ "color": "red", "type": "fruit" }' # OK redis-cli --eval json-get.lua apple , type # "fruit"

การโหลดสคริปต์นี้เข้า Redis server ให้คุณจัดการ JSON values ที่เก็บใน Redis เหมือนเป็น hashes หาก objects ของคุณเล็กพอสมควร นี้ค่อนข้างเร็ว แม้ต้อง parse value ทุกครั้งที่เข้าถึง

หากทำงาน internal API สำหรับระบบที่ต้องการประสิทธิภาพ อาจเลือก MessagePack แทน JSON เนื่องจากเล็กและเร็วกว่า โชคดีที่กับ Redis (เหมือนที่ส่วนใหญ่) MessagePack เป็น drop-in replacement สำหรับ JSON เกือบหมด:

if redis.call("EXISTS", KEYS[1]) == 1 then local payload = redis.call("GET", KEYS[1]) return cmsgpack.unpack(payload)[ARGV[1]] else return nil end

การคำนวณตัวเลข

Lua และ Redis มี type systems ต่างกัน จึงสำคัญต้องเข้าใจว่าค่าอาจเปลี่ยนแปลงเมื่อข้ามพรมแดน Redis-Lua เมื่อตัวเลขมาจาก Lua กลับไปยัง Redis client มันจะกลายเป็น integer — ตัวเลขหลังจุดทศนิยมจะถูกทิ้ง:

local indiana_pi = 3.2 return indiana_pi

ตอนรันสคริปต์นี้ Redis จะคืน integer 3 — คุณสูญเสียส่วนน่าสนใจของ pi ดูง่าย แต่ซับซ้อนขึ้นเมื่อเริ่มโต้ตอบกับ Redis กลางค้างสคริปต์ ตัวอย่าง:

local indiana_pi = 3.2 redis.call("SET", "pi", indiana_pi) return redis.call("GET", "pi")

ค่าที่ได้ที่นี่เป็น string: "3.2" ทำไม? Redis ไม่มี numeric type เฉพาะ ตอนเรา SET ค่าแรก Redis เก็บเป็น string สูญเสียบันทึกว่า Lua คิดว่าค่านี้เป็น float ตอนดึงค่าออกมา ตัวมันยังคงเป็น string

Values ใน Redis ที่เข้าถึงด้วย GET/SET ควรคิดว่าเป็น strings เว้นแต่ numeric operations อย่าง INCR และ DECR รันกับมัน operations พิเศษเหล่านี้จะคืนค่า integer replies (และจัดการค่าที่เก็บตามกฎคณิตศาสตร์) แต่ "ชนิด" ของค่าที่เก็บใน Redis ยังคงเป็น string value

Gotchas: สรุป

นี่คือข้อผิดพลาดที่พบบ่อยที่สุดเมื่อทำงานกับ Lua ใน Redis:

  • Tables เป็น one-based ใน Lua ต่างจากภาษายอดนิยมส่วนใหญ่ Element แรกใน KEYS table คือ KEYS[1] ตัวที่สองคือ KEYS[2] ฯลฯ

  • ค่า nil จบ table ใน Lua ดังนั้น [ 1, 2, nil, 3 ] จะกลายเป็น [1, 2] อัตโนมัติ อย่าใช้ค่า nil ใน tables

  • redis.call จะ raise exception-style Lua errors ในขณะที่ redis.pcall จะ trap errors อัตโนมัติและคืนค่าเป็น tables ที่ตรวจสอบได้

  • ตัวเลข Lua ถูกแปลงเป็น integers เมื่อส่งไป Redis — ทุกอย่างหลังจุดทศนิยมหายไป แปลง floating point numbers เป็น strings ก่อนคืนค่า

  • ระบุ keys ทั้งหมดที่ใช้ใน Lua scripts ของคุณใน table KEYS มิฉะนั้นสคริปต์อาจพังใน Redis เวอร์ชันอนาคต

  • Lua scripts เหมือน operation อื่นๆ ใน Redis: ไม่มีอะไรรันขณะพวกมัน execute คิดว่าสคริปต์เป็นวิธีขยายคำศัพท์ Redis server — ทำให้สั้นและตรงประเด็น

เพิ่มเติมอ่านต่อ

มีทรัพยากรดีๆ สำหรับ Lua และ Redis ออนไลน์เยอะ — นี่คือบางอย่างที่ผมใช้:

  • EVAL Docs
  • RedisGreen's Lua Script Library
  • Lua Reference Manual
  • Lua Tutorial Directory