美文网首页
基于Redis+LUA脚本的CAS方案

基于Redis+LUA脚本的CAS方案

作者: 彳亍口巴 | 来源:发表于2022-01-17 22:25 被阅读0次

cas方案(compare-and-swap),即先比较数据,根据比较结果确定是否更新数据。它实际上是典型的乐观锁,给每一条数据打上版本号,每次更新数据成功,版本号都加1。更新前先比较版本号,确保不会覆盖数据,进而保证了并发访问的数据一致性。

package main

import (
    "encoding/json"
    "fmt"

    "github.com/garyburd/redigo/redis"
)

var redisCli redis.Conn

const (
    CasPrefix = "cas"
)

type Test struct {
    Id   int
    Name string
}

func main() {
    // 10.0.4.16
    var err error
    redisCli, err = redis.Dial("tcp", "127.0.0.1:6379", redis.DialPassword("123456"))
    if err != nil {
        fmt.Println("connect redis error :", err)
        return
    }
    defer redisCli.Close()

    insertOrUpdateData()
    getData()
}

// getData 直接读取redis中的结构体数据
func getData() {
    key := fmt.Sprintf("%s_%s", CasPrefix, "key")
    data, err := redis.Bytes(redisCli.Do("getrange", key, 8, -1))
    if err != nil {
        fmt.Println(err)
        return
    }
    var test Test
    err = json.Unmarshal(data, &test)
    if err != nil {
        fmt.Println(err)
    }
    fmt.Println(test)
}

// insertOrUpdateData 若数据未存在,直接插入,否则比较CAS,若CAS一致,进行修改,否则不做任何操作,返回CAS错误
func insertOrUpdateData() {
    script := `
    -- 输入:参数2, KEYS[1]=目标Key, ARGV[1] =当前对应的值(当前Cas+1 + value ),ARGV[2]= 当前cas
local key     =KEYS[1];
local newValue=ARGV[1];
local checkCAS=tonumber(ARGV[2]);
local oldRaw  =redis.call('getrange', key, 0, 7);
local newCAS  = ( checkCAS >= 4294967295 ) and 0 or ( checkCAS + 1 )
local packValue = struct.pack('<I8c0', newCAS, newValue);  --版本号和值组装起来
-- 数据不存在直接覆盖
if oldRaw == "" then
    redis.call('set', key, packValue);
    return {0, newCAS, 1 } ;
end

local oldCAS = struct.unpack('<I8', oldRaw); --这里是将value中头8字节解析成无符号整数,即数据版本号
-- cas不匹配
if oldCAS ~= checkCAS then
    return {4, oldCAS, 0 } ;
end
redis.call('set', key, packValue);
return {0, newCAS, 2 } ;

-- 请求数据 checkCAS 是上一次拉取的到的版本号, newvalue的前8字节 是  checkCAS+1
`
    key := fmt.Sprintf("%s_%s", CasPrefix, "key")
    test := Test{
        Id:   4,
        Name: "zzr3",
    }
    data, _ := json.Marshal(test)
    res, err := redis.Values(redisCli.Do("eval", script, 1, key, data, 0))
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(res)
}



struct.pack / struct.unpack

struct.pack / struct.unpack官方解释:Library for Converting Data to and from C Structs for Lua 5.1/5.2

脚本中struct.pack 3个参数,第一个是模式字符串,表示如何将后面两个参数表示的数据打包。

模式串'<I8c0'解释:

'<' : 小端模式读写数据。所有设置数据的地方统一用小端模式,c++服务在读取数据时,貌似不能指定大小端,但系统默认是小端模式,所以c++服务读出的数据也没问题。

'I8':表示8字节无符号整数

'c0':表示字符序列,0表示将字符序列全部打包。若是c1,则表示将字符序列第一个字符打包。

struct.pack('<I8c0', newCAS, subVal) 即将第一个整数表示的版本号转换为无符号的8字节整数 填入新bytes数组的前8字节(小端模式), 而subVal以字符序列的形式填入新bytes数组的 后面。这样完成版本号与业务数据的打包。
struct.unpack('<I8', oldRaw) 就比较好理解了,将oldRaw bytes数组前8字节 以小端模式读取并转为无符号整数。

相关文章

网友评论

      本文标题:基于Redis+LUA脚本的CAS方案

      本文链接:https://www.haomeiwen.com/subject/ilshhrtx.html