Skip to content

Commit

Permalink
support multi transaction
Browse files Browse the repository at this point in the history
  • Loading branch information
HDT3213 committed Jun 14, 2021
1 parent 9d03314 commit 67c385e
Show file tree
Hide file tree
Showing 50 changed files with 1,911 additions and 1,114 deletions.
17 changes: 11 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,17 @@
`Godis` is a golang implementation of Redis Server, which intents to provide an example of writing a high concurrent
middleware using golang.

Godis implemented most features of redis, including 5 data structures, ttl, publish/subscribe, geo and AOF persistence.

Godis can run as a server side cluster which is transparent to client. You can connect to any node in the cluster to
access all data in the cluster.

Godis has a concurrent core, so you don't have to worry about your commands blocking the server too much.
Key Features:

- support string, list, hash, set, sorted set
- ttl
- publish/suscribe
- geo
- aof and aof rewrite
- Transaction. The `multi` command is Atomic and Isolated. If any errors are encountered during execution, godis will rollback the executed commands
- server side cluster which is transparent to client. You can connect to any node in the cluster to
access all data in the cluster.
- a concurrent core, so you don't have to worry about your commands blocking the server too much.

If you could read Chinese, you can find more details in [My Blog](https://www.cnblogs.com/Finley/category/1598973.html).

Expand Down
13 changes: 8 additions & 5 deletions README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@

Godis 是一个用 Go 语言实现的 Redis 服务器。本项目旨在为尝试使用 Go 语言开发高并发中间件的朋友提供一些参考。

Godis 实现了 Redis 的大多数功能,包括5种数据结构、TTL、发布订阅、地理位置以及 AOF 持久化。

Godis 支持集群模式,集群对客户端是透明的只要连接上集群中任意一个节点就可以访问集群中所有数据。

Godis 是并行工作的, 无需担心您的操作会阻塞整个服务器.
关键功能:
- 支持 string, list, hash, set, sorted set 数据结构
- 自动过期功能(TTL)
- 地理位置
- AOF 持久化及AOF重写
- 事务. Multi 命令开启的事务具有`原子性``隔离性`. 若在执行过程中遇到错误, godis 会回滚已执行的命令
- 内置集群模式. 集群对客户端是透明的, 您可以像使用单机版 redis 一样使用 godis 集群
- 并行引擎, 无需担心您的操作会阻塞整个服务器.

可以在[我的博客](https://www.cnblogs.com/Finley/category/1598973.html)了解更多关于
Godis 的信息。
Expand Down
97 changes: 0 additions & 97 deletions aof.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@ package godis
import (
"github.com/hdt3213/godis/config"
"github.com/hdt3213/godis/datastruct/dict"
List "github.com/hdt3213/godis/datastruct/list"
"github.com/hdt3213/godis/datastruct/lock"
"github.com/hdt3213/godis/datastruct/set"
SortedSet "github.com/hdt3213/godis/datastruct/sortedset"
"github.com/hdt3213/godis/lib/logger"
"github.com/hdt3213/godis/lib/utils"
"github.com/hdt3213/godis/redis/parser"
Expand Down Expand Up @@ -148,100 +145,6 @@ func (db *DB) aofRewrite() {
db.finishRewrite(file)
}

var setCmd = []byte("SET")

func stringToCmd(key string, bytes []byte) *reply.MultiBulkReply {
args := make([][]byte, 3)
args[0] = setCmd
args[1] = []byte(key)
args[2] = bytes
return reply.MakeMultiBulkReply(args)
}

var rPushAllCmd = []byte("RPUSH")

func listToCmd(key string, list *List.LinkedList) *reply.MultiBulkReply {
args := make([][]byte, 2+list.Len())
args[0] = rPushAllCmd
args[1] = []byte(key)
list.ForEach(func(i int, val interface{}) bool {
bytes, _ := val.([]byte)
args[2+i] = bytes
return true
})
return reply.MakeMultiBulkReply(args)
}

var sAddCmd = []byte("SADD")

func setToCmd(key string, set *set.Set) *reply.MultiBulkReply {
args := make([][]byte, 2+set.Len())
args[0] = sAddCmd
args[1] = []byte(key)
i := 0
set.ForEach(func(val string) bool {
args[2+i] = []byte(val)
i++
return true
})
return reply.MakeMultiBulkReply(args)
}

var hMSetCmd = []byte("HMSET")

func hashToCmd(key string, hash dict.Dict) *reply.MultiBulkReply {
args := make([][]byte, 2+hash.Len()*2)
args[0] = hMSetCmd
args[1] = []byte(key)
i := 0
hash.ForEach(func(field string, val interface{}) bool {
bytes, _ := val.([]byte)
args[2+i*2] = []byte(field)
args[3+i*2] = bytes
i++
return true
})
return reply.MakeMultiBulkReply(args)
}

var zAddCmd = []byte("ZADD")

func zSetToCmd(key string, zset *SortedSet.SortedSet) *reply.MultiBulkReply {
args := make([][]byte, 2+zset.Len()*2)
args[0] = zAddCmd
args[1] = []byte(key)
i := 0
zset.ForEach(int64(0), int64(zset.Len()), true, func(element *SortedSet.Element) bool {
value := strconv.FormatFloat(element.Score, 'f', -1, 64)
args[2+i*2] = []byte(value)
args[3+i*2] = []byte(element.Member)
i++
return true
})
return reply.MakeMultiBulkReply(args)
}

// EntityToCmd serialize data entity to redis command
func EntityToCmd(key string, entity *DataEntity) *reply.MultiBulkReply {
if entity == nil {
return nil
}
var cmd *reply.MultiBulkReply
switch val := entity.Data.(type) {
case []byte:
cmd = stringToCmd(key, val)
case *List.LinkedList:
cmd = listToCmd(key, val)
case *set.Set:
cmd = setToCmd(key, val)
case dict.Dict:
cmd = hashToCmd(key, val)
case *SortedSet.SortedSet:
cmd = zSetToCmd(key, val)
}
return cmd
}

func (db *DB) startRewrite() (*os.File, int64, error) {
db.pausingAof.Lock() // pausing aof
defer db.pausingAof.Unlock()
Expand Down
39 changes: 19 additions & 20 deletions aof_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ package godis

import (
"github.com/hdt3213/godis/config"
"github.com/hdt3213/godis/datastruct/utils"
utils2 "github.com/hdt3213/godis/lib/utils"
"github.com/hdt3213/godis/lib/utils"
"github.com/hdt3213/godis/redis/reply"
"io/ioutil"
"os"
Expand Down Expand Up @@ -34,31 +33,31 @@ func TestAof(t *testing.T) {
for i := 0; i < size; i++ {
key := strconv.Itoa(cursor)
cursor++
execSet(aofWriteDB, utils2.ToBytesList(key, utils2.RandString(8), "EX", "10000"))
execSet(aofWriteDB, utils.ToCmdLine(key, utils.RandString(8), "EX", "10000"))
keys = append(keys, key)
}
for i := 0; i < size; i++ {
key := strconv.Itoa(cursor)
cursor++
execRPush(aofWriteDB, utils2.ToBytesList(key, utils2.RandString(8)))
execRPush(aofWriteDB, utils.ToCmdLine(key, utils.RandString(8)))
keys = append(keys, key)
}
for i := 0; i < size; i++ {
key := strconv.Itoa(cursor)
cursor++
execHSet(aofWriteDB, utils2.ToBytesList(key, utils2.RandString(8), utils2.RandString(8)))
execHSet(aofWriteDB, utils.ToCmdLine(key, utils.RandString(8), utils.RandString(8)))
keys = append(keys, key)
}
for i := 0; i < size; i++ {
key := strconv.Itoa(cursor)
cursor++
execSAdd(aofWriteDB, utils2.ToBytesList(key, utils2.RandString(8)))
execSAdd(aofWriteDB, utils.ToCmdLine(key, utils.RandString(8)))
keys = append(keys, key)
}
for i := 0; i < size; i++ {
key := strconv.Itoa(cursor)
cursor++
execZAdd(aofWriteDB, utils2.ToBytesList(key, "10", utils2.RandString(8)))
execZAdd(aofWriteDB, utils.ToCmdLine(key, "10", utils.RandString(8)))
keys = append(keys, key)
}
aofWriteDB.Close() // wait for aof finished
Expand Down Expand Up @@ -105,44 +104,44 @@ func TestRewriteAOF(t *testing.T) {
for i := 0; i < size; i++ {
key := "str" + strconv.Itoa(cursor)
cursor++
execSet(aofWriteDB, utils2.ToBytesList(key, utils2.RandString(8)))
execSet(aofWriteDB, utils2.ToBytesList(key, utils2.RandString(8)))
execSet(aofWriteDB, utils.ToCmdLine(key, utils.RandString(8)))
execSet(aofWriteDB, utils.ToCmdLine(key, utils.RandString(8)))
keys = append(keys, key)
}
// test ttl
for i := 0; i < size; i++ {
key := "str" + strconv.Itoa(cursor)
cursor++
execSet(aofWriteDB, utils2.ToBytesList(key, utils2.RandString(8), "EX", "1000"))
execSet(aofWriteDB, utils.ToCmdLine(key, utils.RandString(8), "EX", "1000"))
ttlKeys = append(ttlKeys, key)
}
for i := 0; i < size; i++ {
key := "list" + strconv.Itoa(cursor)
cursor++
execRPush(aofWriteDB, utils2.ToBytesList(key, utils2.RandString(8)))
execRPush(aofWriteDB, utils2.ToBytesList(key, utils2.RandString(8)))
execRPush(aofWriteDB, utils.ToCmdLine(key, utils.RandString(8)))
execRPush(aofWriteDB, utils.ToCmdLine(key, utils.RandString(8)))
keys = append(keys, key)
}
for i := 0; i < size; i++ {
key := "hash" + strconv.Itoa(cursor)
cursor++
field := utils2.RandString(8)
execHSet(aofWriteDB, utils2.ToBytesList(key, field, utils2.RandString(8)))
execHSet(aofWriteDB, utils2.ToBytesList(key, field, utils2.RandString(8)))
field := utils.RandString(8)
execHSet(aofWriteDB, utils.ToCmdLine(key, field, utils.RandString(8)))
execHSet(aofWriteDB, utils.ToCmdLine(key, field, utils.RandString(8)))
keys = append(keys, key)
}
for i := 0; i < size; i++ {
key := "set" + strconv.Itoa(cursor)
cursor++
member := utils2.RandString(8)
execSAdd(aofWriteDB, utils2.ToBytesList(key, member))
execSAdd(aofWriteDB, utils2.ToBytesList(key, member))
member := utils.RandString(8)
execSAdd(aofWriteDB, utils.ToCmdLine(key, member))
execSAdd(aofWriteDB, utils.ToCmdLine(key, member))
keys = append(keys, key)
}
for i := 0; i < size; i++ {
key := "zset" + strconv.Itoa(cursor)
cursor++
execZAdd(aofWriteDB, utils2.ToBytesList(key, "10", utils2.RandString(8)))
execZAdd(aofWriteDB, utils.ToCmdLine(key, "10", utils.RandString(8)))
keys = append(keys, key)
}
time.Sleep(time.Second) // wait for async goroutine finish its job
Expand All @@ -167,7 +166,7 @@ func TestRewriteAOF(t *testing.T) {
}
}
for _, key := range ttlKeys {
ret := execTTL(aofReadDB, utils2.ToBytesList(key))
ret := execTTL(aofReadDB, utils.ToCmdLine(key))
intResult, ok := ret.(*reply.IntReply)
if !ok {
t.Errorf("expected int reply, actually %s", ret.ToBytes())
Expand Down
2 changes: 1 addition & 1 deletion cluster/client_pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func (f *connectionFactory) MakeObject(ctx context.Context) (*pool.PooledObject,
c.Start()
// all peers of cluster should use the same password
if config.Properties.RequirePass != "" {
c.Send(utils.ToBytesList("AUTH", config.Properties.RequirePass))
c.Send(utils.ToCmdLine("AUTH", config.Properties.RequirePass))
}
return pool.NewPooledObject(c), nil
}
Expand Down
2 changes: 1 addition & 1 deletion cluster/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ func makeArgs(cmd string, args ...string) [][]byte {
return result
}

// return peer -> keys
// return peer -> writeKeys
func (cluster *Cluster) groupBy(keys []string) map[string][]string {
result := make(map[string][]string)
for _, key := range keys {
Expand Down
2 changes: 1 addition & 1 deletion cluster/com.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func (cluster *Cluster) returnPeerClient(peer string, peerClient *client.Client)
}

// relay relays command to peer
// cannot call Prepare, Commit, Rollback of self node
// cannot call Prepare, Commit, execRollback of self node
func (cluster *Cluster) relay(peer string, c redis.Connection, args [][]byte) redis.Reply {
if peer == cluster.self {
// to self db
Expand Down
52 changes: 8 additions & 44 deletions cluster/del.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import (
"strconv"
)

// Del atomically removes given keys from cluster, keys can be distributed on any node
// if the given keys are distributed on different node, Del will use try-commit-catch to remove them
// Del atomically removes given writeKeys from cluster, writeKeys can be distributed on any node
// if the given writeKeys are distributed on different node, Del will use try-commit-catch to remove them
func Del(cluster *Cluster, c redis.Connection, args [][]byte) redis.Reply {
if len(args) < 2 {
return reply.MakeErrReply("ERR wrong number of arguments for 'del' command")
Expand All @@ -18,7 +18,7 @@ func Del(cluster *Cluster, c redis.Connection, args [][]byte) redis.Reply {
}
groupMap := cluster.groupBy(keys)
if len(groupMap) == 1 && allowFastTransaction { // do fast
for peer, group := range groupMap { // only one group
for peer, group := range groupMap { // only one peerKeys
return cluster.relay(peer, c, makeArgs("DEL", group...))
}
}
Expand All @@ -27,14 +27,14 @@ func Del(cluster *Cluster, c redis.Connection, args [][]byte) redis.Reply {
txID := cluster.idGenerator.NextID()
txIDStr := strconv.FormatInt(txID, 10)
rollback := false
for peer, group := range groupMap {
args := []string{txIDStr}
args = append(args, group...)
for peer, peerKeys := range groupMap {
peerArgs := []string{txIDStr, "DEL"}
peerArgs = append(peerArgs, peerKeys...)
var resp redis.Reply
if peer == cluster.self {
resp = prepareDel(cluster, c, makeArgs("PrepareDel", args...))
resp = execPrepare(cluster, c, makeArgs("Prepare", peerArgs...))
} else {
resp = cluster.relay(peer, c, makeArgs("PrepareDel", args...))
resp = cluster.relay(peer, c, makeArgs("Prepare", peerArgs...))
}
if reply.IsErrorReply(resp) {
errReply = resp
Expand Down Expand Up @@ -63,39 +63,3 @@ func Del(cluster *Cluster, c redis.Connection, args [][]byte) redis.Reply {
}
return errReply
}

// args: PrepareDel id keys...
func prepareDel(cluster *Cluster, c redis.Connection, args [][]byte) redis.Reply {
if len(args) < 3 {
return reply.MakeErrReply("ERR wrong number of arguments for 'preparedel' command")
}
txID := string(args[1])
keys := make([]string, 0, len(args)-2)
for i := 2; i < len(args); i++ {
arg := args[i]
keys = append(keys, string(arg))
}
txArgs := makeArgs("DEL", keys...) // actual args for cluster.db
tx := NewTransaction(cluster, c, txID, txArgs, keys)
cluster.transactions.Put(txID, tx)
err := tx.prepare()
if err != nil {
return reply.MakeErrReply(err.Error())
}
return &reply.OkReply{}
}

// invoker should provide lock
func commitDel(cluster *Cluster, c redis.Connection, tx *Transaction) redis.Reply {
keys := make([]string, len(tx.args))
for i, v := range tx.args {
keys[i] = string(v)
}
keys = keys[1:]

deleted := cluster.db.Removes(keys...)
if deleted > 0 {
cluster.db.AddAof(reply.MakeMultiBulkReply(tx.args))
}
return reply.MakeIntReply(int64(deleted))
}
Loading

0 comments on commit 67c385e

Please sign in to comment.