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字节 以小端模式读取并转为无符号整数。
网友评论