Repository: juicedata/juicefs
Pull request #3132 (2022-12-29 03:46:43)

meta/tkv: refactor - wrap kvTxn with a struct
`append` and `incrBy` are retained if we want the optimization of FoundationDB.

codecov-commenter: # [Codecov](https://codecov.io/gh/juicedata/juicefs/pull/3132?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=juicedata) Report
Base: **55.99**% // Head: **56.08**% // Increases project coverage by **`+0.08%`** :tada:
> Coverage data is based on head [(`ccb65c7`)](https://codecov.io/gh/juicedata/juicefs/pull/3132?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=juicedata) compared to base [(`638a381`)](https://codecov.io/gh/juicedata/juicefs/commit/638a3810ffb70cf61182ff2ab3671fdc727588b2?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=juicedata).
> Patch coverage: 96.66% of modified lines in pull request are covered.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #3132      +/-   ##
==========================================
+ Coverage   55.99%   56.08%   +0.08%     
==========================================
  Files         141      141              
  Lines       32667    32667              
==========================================
+ Hits        18292    18320      +28     
+ Misses      12419    12396      -23     
+ Partials     1956     1951       -5     
```


| [Impacted Files](https://codecov.io/gh/juicedata/juicefs/pull/3132?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=juicedata) | Coverage Δ | |
|---|---|---|
| [pkg/meta/tkv.go](https://codecov.io/gh/juicedata/juicefs/pull/3132/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=juicedata#diff-cGtnL21ldGEvdGt2Lmdv) | `71.56% <94.87%> (ø)` | |
| [pkg/meta/tkv\_badger.go](https://codecov.io/gh/juicedata/juicefs/pull/3132/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=juicedata#diff-cGtnL21ldGEvdGt2X2JhZGdlci5nbw==) | `74.86% <100.00%> (ø)` | |
| [pkg/meta/tkv\_etcd.go](https://codecov.io/gh/juicedata/juicefs/pull/3132/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=juicedata#diff-cGtnL21ldGEvdGt2X2V0Y2QuZ28=) | `75.18% <100.00%> (ø)` | |
| [pkg/meta/tkv\_fdb.go](https://codecov.io/gh/juicedata/juicefs/pull/3132/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=juicedata#diff-cGtnL21ldGEvdGt2X2ZkYi5nbw==) | `83.82% <100.00%> (ø)` | |
| [pkg/meta/tkv\_lock.go](https://codecov.io/gh/juicedata/juicefs/pull/3132/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=juicedata#diff-cGtnL21ldGEvdGt2X2xvY2suZ28=) | `83.24% <100.00%> (ø)` | |
| [pkg/meta/tkv\_mem.go](https://codecov.io/gh/juicedata/juicefs/pull/3132/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=juicedata#diff-cGtnL21ldGEvdGt2X21lbS5nbw==) | `86.30% <100.00%> (ø)` | |
| [pkg/meta/tkv\_prefix.go](https://codecov.io/gh/juicedata/juicefs/pull/3132/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=juicedata#diff-cGtnL21ldGEvdGt2X3ByZWZpeC5nbw==) | `92.40% <100.00%> (ø)` | |
| [pkg/meta/tkv\_tikv.go](https://codecov.io/gh/juicedata/juicefs/pull/3132/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=juicedata#diff-cGtnL21ldGEvdGt2X3Rpa3YuZ28=) | `64.86% <100.00%> (ø)` | |
| [pkg/vfs/writer.go](https://codecov.io/gh/juicedata/juicefs/pull/3132/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=juicedata#diff-cGtnL3Zmcy93cml0ZXIuZ28=) | `77.48% <0.00%> (-1.17%)` | :arrow_down: |
| [pkg/meta/sql.go](https://codecov.io/gh/juicedata/juicefs/pull/3132/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=juicedata#diff-cGtnL21ldGEvc3FsLmdv) | `59.28% <0.00%> (-0.08%)` | :arrow_down: |
| ... and [3 more](https://codecov.io/gh/juicedata/juicefs/pull/3132/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=juicedata) | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=juicedata). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=juicedata)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/juicedata/juicefs/pull/3132?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=juicedata).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=juicedata).

github-actions[bot]: <!-- add-pr-comment:add-pr-comment -->

*Mutate Test Report*
 

Usage: https://github.com/juicedata/juicefs/blob/main/.github/scripts/mutate/how_to_use_mutate_test.md

Patch:
diff --git a/docs/en/security/encrypt.md b/docs/en/security/encrypt.md
index 44389ec8..112d14e5 100644
--- a/docs/en/security/encrypt.md
+++ b/docs/en/security/encrypt.md
@@ -12,7 +12,7 @@ JuiceFS provides data encrytion from two aspects:
 
 Running JuiceFS generally involves the network connection between database and object storage, which is determined by the architecture of JuiceFS. As long as the servers support encryption connections, JuiceFS can be accessed through the encrypted channel.
 
-### Connect to objetc storage via HTTPS
+### Connect to object storage via HTTPS
 
 Public cloud object storage generally supports both HTTP and HTTPS. If no scheme is specified, JuiceFS uses HTTPS by default. For example, the client will identify the bucket in following command as `https://myjfs.s3.ap-southeast-1.amazonaws.com`.
 
diff --git a/pkg/meta/tkv.go b/pkg/meta/tkv.go
index 081827b0..2f8d95a5 100644
--- a/pkg/meta/tkv.go
+++ b/pkg/meta/tkv.go
@@ -38,29 +38,38 @@ import (
 	"github.com/juicedata/juicefs/pkg/utils"
 )
 
-type kvTxn interface {
+type kvtxn interface {
 	get(key []byte) []byte
 	gets(keys ...[]byte) [][]byte
-	scanRange(begin, end []byte) map[string][]byte
-	scanKeys(prefix []byte) [][]byte
-	scanKeysRange(begin, end []byte, limit int, filter func(k []byte) bool) [][]byte
-	scanValues(prefix []byte, limit int, filter func(k, v []byte) bool) map[string][]byte
+	// scan stops when handler returns false; begin and end must not be nil
+	scan(begin, end []byte, keysOnly bool, handler func(k, v []byte) bool)
 	exist(prefix []byte) bool
 	set(key, value []byte)
 	append(key []byte, value []byte) []byte
 	incrBy(key []byte, value int64) int64
-	dels(keys ...[]byte)
+	delete(key []byte)
 }
 
 type tkvClient interface {
 	name() string
-	txn(f func(kvTxn) error) error
+	txn(f func(*kvTxn) error) error
 	scan(prefix []byte, handler func(key, value []byte)) error
 	reset(prefix []byte) error
 	close() error
 	shouldRetry(err error) bool
 }
 
+type kvTxn struct {
+	kvtxn
+}
+
+func (tx *kvTxn) deleteKeys(prefix []byte) {
+	tx.scan(prefix, nextKey(prefix), true, func(k, v []byte) bool {
+		tx.delete(k)
+		return true
+	})
+}
+
 type kvMeta struct {
 	*baseMeta
 	client tkvClient
@@ -302,7 +311,7 @@ func (m *kvMeta) parseEntry(buf []byte) (uint8, Ino) {
 
 func (m *kvMeta) get(key []byte) ([]byte, error) {
 	var value []byte
-	err := m.client.txn(func(tx kvTxn) error {
+	err := m.client.txn(func(tx *kvTxn) error {
 		value = tx.get(key)
 		return nil
 	})
@@ -311,17 +320,30 @@ func (m *kvMeta) get(key []byte) ([]byte, error) {
 
 func (m *kvMeta) scanKeys(prefix []byte) ([][]byte, error) {
 	var keys [][]byte
-	err := m.client.txn(func(tx kvTxn) error {
-		keys = tx.scanKeys(prefix)
+	err := m.client.txn(func(tx *kvTxn) error {
+		tx.scan(prefix, nextKey(prefix), true, func(k, v []byte) bool {
+			keys = append(keys, k)
+			return true
+		})
 		return nil
 	})
 	return keys, err
 }
 
 func (m *kvMeta) scanValues(prefix []byte, limit int, filter func(k, v []byte) bool) (map[string][]byte, error) {
-	var values map[string][]byte
-	err := m.client.txn(func(tx kvTxn) error {
-		values = tx.scanValues(prefix, limit, filter)
+	if limit == 0 {
+		return nil, nil
+	}
+	values := make(map[string][]byte)
+	err := m.client.txn(func(tx *kvTxn) error {
+		var c int
+		tx.scan(prefix, nextKey(prefix), false, func(k, v []byte) bool {
+			if filter == nil || filter(k, v) {
+				values[string(k)] = v
+				c++
+			}
+			return limit < 0 || c < limit
+		})
 		return nil
 	})
 	return values, err
@@ -360,7 +382,7 @@ func (m *kvMeta) Init(format *Format, force bool) error {
 		Length: 4 << 10,
 		Parent: 1,
 	}
-	return m.txn(func(tx kvTxn) error {
+	return m.txn(func(tx *kvTxn) error {
 		if format.TrashDays > 0 {
 			buf := tx.get(m.inodeKey(TrashInode))
 			if buf == nil {
@@ -411,14 +433,14 @@ func (m *kvMeta) doCleanStaleSession(sid uint64) error {
 			ls := unmarshalFlock(v)
 			for o := range ls {
 				if o.sid == sid {
-					if err = m.txn(func(tx kvTxn) error {
+					if err = m.txn(func(tx *kvTxn) error {
 						v := tx.get([]byte(k))
 						ls := unmarshalFlock(v)
 						delete(ls, o)
 						if len(ls) > 0 {
 							tx.set([]byte(k), marshalFlock(ls))
 						} else {
-							tx.dels([]byte(k))
+							tx.delete([]byte(k))
 						}
 						return nil
 					}); err != nil {
@@ -438,14 +460,14 @@ func (m *kvMeta) doCleanStaleSession(sid uint64) error {
 			ls := unmarshalPlock(v)
 			for o := range ls {
 				if o.sid == sid {
-					if err = m.txn(func(tx kvTxn) error {
+					if err = m.txn(func(tx *kvTxn) error {
 						v := tx.get([]byte(k))
 						ls := unmarshalPlock(v)
 						delete(ls, o)
 						if len(ls) > 0 {
 							tx.set([]byte(k), marshalPlock(ls))
 						} else {
-							tx.dels([]byte(k))
+							tx.delete([]byte(k))
 						}
 						return nil
 					}); err != nil {
@@ -625,7 +647,7 @@ func (m *kvMeta) shouldRetry(err error) bool {
 	return m.client.shouldRetry(err)
 }
 
-func (m *kvMeta) txn(f func(tx kvTxn) error, inodes ...Ino) error {
+func (m *kvMeta) txn(f func(tx *kvTxn) error, inodes ...Ino) error {
 	if m.conf.ReadOnly {
 		return syscall.EROFS
 	}
@@ -657,7 +679,7 @@ func (m *kvMeta) txn(f func(tx kvTxn) error, inodes ...Ino) error {
 }
 
 func (m *kvMeta) setValue(key, value []byte) error {
-	return m.txn(func(tx kvTxn) error {
+	return m.txn(func(tx *kvTxn) error {
 		tx.set(key, value)
 		return nil
 	})
@@ -671,7 +693,7 @@ func (m *kvMeta) getCounter(name string) (int64, error) {
 func (m *kvMeta) incrCounter(name string, value int64) (int64, error) {
 	var new int64
 	key := m.counterKey(name)
-	err := m.txn(func(tx kvTxn) error {
+	err := m.txn(func(tx *kvTxn) error {
 		new = tx.incrBy(key, value)
 		return nil
 	})
@@ -681,7 +703,7 @@ func (m *kvMeta) incrCounter(name string, value int64) (int64, error) {
 func (m *kvMeta) setIfSmall(name string, value, diff int64) (bool, error) {
 	var changed bool
 	key := m.counterKey(name)
-	err := m.txn(func(tx kvTxn) error {
+	err := m.txn(func(tx *kvTxn) error {
 		changed = false
 		if m.parseInt64(tx.get(key)) > value-diff {
 			return nil
@@ -699,8 +721,10 @@ func (m *kvMeta) deleteKeys(keys ...[]byte) error {
 	if len(keys) == 0 {
 		return nil
 	}
-	return m.txn(func(tx kvTxn) error {
-		tx.dels(keys...)
+	return m.txn(func(tx *kvTxn) error {
+		for _, key := range keys {
+			tx.delete(key)
+		}
 		return nil
 	})
 }
@@ -739,7 +763,7 @@ func (m *kvMeta) SetAttr(ctx Context, inode Ino, set uint16, sugidclearmode uint
 	defer m.timeit(time.Now())
 	inode = m.checkRoot(inode)
 	defer func() { m.of.InvalidateChunk(inode, 0xFFFFFFFE) }()
-	return errno(m.txn(func(tx kvTxn) error {
+	return errno(m.txn(func(tx *kvTxn) error {
 		var cur Attr
 		a := tx.get(m.inodeKey(inode))
 		if a == nil {
@@ -819,7 +843,7 @@ func (m *kvMeta) Truncate(ctx Context, inode Ino, flags uint8, length uint64, at
 	}
 	defer func() { m.of.InvalidateChunk(inode, 0xFFFFFFFF) }()
 	var newSpace int64
-	err := m.txn(func(tx kvTxn) error {
+	err := m.txn(func(tx *kvTxn) error {
 		newSpace = 0
 		var t Attr
 		a := tx.get(m.inodeKey(inode))
@@ -845,11 +869,12 @@ func (m *kvMeta) Truncate(ctx Context, inode Ino, flags uint8, length uint64, at
 			right, left = left, right
 		}
 		if right/ChunkSize-left/ChunkSize > 1 {
-			zeroChunks := tx.scanRange(m.chunkKey(inode, uint32(left/ChunkSize)+1), m.chunkKey(inode, uint32(right/ChunkSize)))
 			buf := marshalSlice(0, 0, 0, 0, ChunkSize)
-			for key, value := range zeroChunks {
-				tx.set([]byte(key), append(value, buf...))
-			}
+			tx.scan(m.chunkKey(inode, uint32(left/ChunkSize)+1), m.chunkKey(inode, uint32(right/ChunkSize)),
+				false, func(k, v []byte) bool {
+					tx.set(k, append(v, buf...))
+					return true
+				})
 		}
 		l := uint32(right - left)
 		if right > (left/ChunkSize+1)*ChunkSize {
@@ -901,7 +926,7 @@ func (m *kvMeta) Fallocate(ctx Context, inode Ino, mode uint8, off uint64, size
 	}
 	defer func() { m.of.InvalidateChunk(inode, 0xFFFFFFFF) }()
 	var newSpace int64
-	err := m.txn(func(tx kvTxn) error {
+	err := m.txn(func(tx *kvTxn) error {
 		newSpace = 0
 		var t Attr
 		a := tx.get(m.inodeKey(inode))
@@ -1007,7 +1032,7 @@ func (m *kvMeta) doMknod(ctx Context, parent Ino, name string, _type uint8, mode
 		*inode = ino
 	}
 
-	err = m.txn(func(tx kvTxn) error {
+	err = m.txn(func(tx *kvTxn) error {
 		var pattr Attr
 		a := tx.get(m.inodeKey(parent))
 		if a == nil {
@@ -1100,7 +1125,7 @@ func (m *kvMeta) doUnlink(ctx Context, parent Ino, name string) syscall.Errno {
 	var attr Attr
 	var opened bool
 	var newSpace, newInode int64
-	err := m.txn(func(tx kvTxn) error {
+	err := m.txn(func(tx *kvTxn) error {
 		opened = false
 		attr = Attr{}
 		newSpace, newInode = 0, 0
@@ -1166,7 +1191,7 @@ func (m *kvMeta) doUnlink(ctx Context, parent Ino, name string) syscall.Errno {
 			updateParent = true
 		}
 
-		tx.dels(m.entryKey(parent, name))
+		tx.delete(m.entryKey(parent, name))
 		if updateParent {
 			tx.set(m.inodeKey(parent), m.marshal(&pattr))
 		}
@@ -1189,19 +1214,19 @@ func (m *kvMeta) doUnlink(ctx Context, parent Ino, name string) syscall.Errno {
 					tx.set(m.sustainedKey(m.sid, inode), []byte{1})
 				} else {
 					tx.set(m.delfileKey(inode, attr.Length), m.packInt64(now.Unix()))
-					tx.dels(m.inodeKey(inode))
+					tx.delete(m.inodeKey(inode))
 					newSpace, newInode = -align4K(attr.Length), -1
 				}
 			case TypeSymlink:
-				tx.dels(m.symKey(inode))
+				tx.delete(m.symKey(inode))
 				fallthrough
 			default:
-				tx.dels(m.inodeKey(inode))
+				tx.delete(m.inodeKey(inode))
 				newSpace, newInode = -align4K(0), -1
 			}
-			tx.dels(tx.scanKeys(m.xattrKey(inode, ""))...)
+			tx.deleteKeys(m.xattrKey(inode, ""))
 			if attr.Parent == 0 {
-				tx.dels(tx.scanKeys(m.fmtKey("A", inode, "P"))...)
+				tx.deleteKeys(m.fmtKey("A", inode, "P"))
 			}
 		}
 		return nil
@@ -1220,7 +1245,7 @@ func (m *kvMeta) doRmdir(ctx Context, parent Ino, name string) syscall.Errno {
 	if st := m.checkTrash(parent, &trash); st != 0 {
 		return st
 	}
-	err := m.txn(func(tx kvTxn) error {
+	err := m.txn(func(tx *kvTxn) error {
 		buf := tx.get(m.entryKey(parent, name))
 		if buf == nil && m.conf.CaseInsensi {
 			if e := m.resolveCase(ctx, parent, name); e != nil {
@@ -1275,13 +1300,13 @@ func (m *kvMeta) doRmdir(ctx Context, parent Ino, name string) syscall.Errno {
 		if !isTrash(parent) {
 			tx.set(m.inodeKey(parent), m.marshal(&pattr))
 		}
-		tx.dels(m.entryKey(parent, name))
+		tx.delete(m.entryKey(parent, name))
 		if trash > 0 {
 			tx.set(m.inodeKey(inode), m.marshal(&attr))
 			tx.set(m.entryKey(trash, m.trashEntry(parent, inode, name)), buf)
 		} else {
-			tx.dels(m.inodeKey(inode))
-			tx.dels(tx.scanKeys(m.xattrKey(inode, ""))...)
+			tx.delete(m.inodeKey(inode))
+			tx.deleteKeys(m.xattrKey(inode, ""))
 		}
 		return nil
 	}, parent)
@@ -1302,7 +1327,7 @@ func (m *kvMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentDst
 	var dtyp uint8
 	var tattr Attr
 	var newSpace, newInode int64
-	err := m.txn(func(tx kvTxn) error {
+	err := m.txn(func(tx *kvTxn) error {
 		opened = false
 		dino, dtyp = 0, 0
 		tattr = Attr{}
@@ -1453,7 +1478,7 @@ func (m *kvMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentDst
 				tx.incrBy(m.parentKey(dino, parentDst), -1)
 			}
 		} else {
-			tx.dels(m.entryKey(parentSrc, nameSrc))
+			tx.delete(m.entryKey(parentSrc, nameSrc))
 			if dino > 0 {
 				if trash > 0 {
 					tx.set(m.inodeKey(dino), m.marshal(&tattr))
@@ -1474,19 +1499,19 @@ func (m *kvMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentDst
 							tx.set(m.sustainedKey(m.sid, dino), []byte{1})
 						} else {
 							tx.set(m.delfileKey(dino, tattr.Length), m.packInt64(now.Unix()))
-							tx.dels(m.inodeKey(dino))
+							tx.delete(m.inodeKey(dino))
 							newSpace, newInode = -align4K(tattr.Length), -1
 						}
 					} else {
 						if dtyp == TypeSymlink {
-							tx.dels(m.symKey(dino))
+							tx.delete(m.symKey(dino))
 						}
-						tx.dels(m.inodeKey(dino))
+						tx.delete(m.inodeKey(dino))
 						newSpace, newInode = -align4K(0), -1
 					}
-					tx.dels(tx.scanKeys(m.xattrKey(dino, ""))...)
+					tx.deleteKeys(m.xattrKey(dino, ""))
 					if tattr.Parent == 0 {
-						tx.dels(tx.scanKeys(m.fmtKey("A", dino, "P"))...)
+						tx.deleteKeys(m.fmtKey("A", dino, "P"))
 					}
 				}
 			}
@@ -1517,7 +1542,7 @@ func (m *kvMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentDst
 }
 
 func (m *kvMeta) doLink(ctx Context, inode, parent Ino, name string, attr *Attr) syscall.Errno {
-	return errno(m.txn(func(tx kvTxn) error {
+	return errno(m.txn(func(tx *kvTxn) error {
 		rs := tx.gets(m.inodeKey(parent), m.inodeKey(inode))
 		if rs[0] == nil || rs[1] == nil {
 			return syscall.ENOENT
@@ -1599,7 +1624,7 @@ func (m *kvMeta) doReaddir(ctx Context, inode Ino, plus uint8, entries *[]*Entry
 				keys[i] = m.inodeKey(e.Inode)
 			}
 			var rs [][]byte
-			err := m.client.txn(func(tx kvTxn) error {
+			err := m.client.txn(func(tx *kvTxn) error {
 				rs = tx.gets(keys...)
 				return nil
 			})
@@ -1652,7 +1677,7 @@ func (m *kvMeta) doReaddir(ctx Context, inode Ino, plus uint8, entries *[]*Entry
 func (m *kvMeta) doDeleteSustainedInode(sid uint64, inode Ino) error {
 	var attr Attr
 	var newSpace int64
-	err := m.txn(func(tx kvTxn) error {
+	err := m.txn(func(tx *kvTxn) error {
 		newSpace = 0
 		a := tx.get(m.inodeKey(inode))
 		if a == nil {
@@ -1660,8 +1685,8 @@ func (m *kvMeta) doDeleteSustainedInode(sid uint64, inode Ino) error {
 		}
 		m.parseAttr(a, &attr)
 		tx.set(m.delfileKey(inode, attr.Length), m.packInt64(time.Now().Unix()))
-		tx.dels(m.inodeKey(inode))
-		tx.dels(m.sustainedKey(sid, inode))
+		tx.delete(m.inodeKey(inode))
+		tx.delete(m.sustainedKey(sid, inode))
 		newSpace = -align4K(attr.Length)
 		return nil
 	})
@@ -1709,7 +1734,7 @@ func (m *kvMeta) Write(ctx Context, inode Ino, indx uint32, off uint32, slice Sl
 	defer func() { m.of.InvalidateChunk(inode, indx) }()
 	var newSpace int64
 	var needCompact bool
-	err := m.txn(func(tx kvTxn) error {
+	err := m.txn(func(tx *kvTxn) error {
 		newSpace = 0
 		var attr Attr
 		a := tx.get(m.inodeKey(inode))
@@ -1756,7 +1781,7 @@ func (m *kvMeta) CopyFileRange(ctx Context, fin Ino, offIn uint64, fout Ino, off
 		defer f.Unlock()
 	}
 	defer func() { m.of.InvalidateChunk(fout, 0xFFFFFFFF) }()
-	err := m.txn(func(tx kvTxn) error {
+	err := m.txn(func(tx *kvTxn) error {
 		newSpace = 0
 		rs := tx.gets(m.inodeKey(fin), m.inodeKey(fout))
 		if rs[0] == nil || rs[1] == nil {
@@ -1798,7 +1823,12 @@ func (m *kvMeta) CopyFileRange(ctx Context, fin Ino, offIn uint64, fout Ino, off
 		attr.Ctime = now.Unix()
 		attr.Ctimensec = uint32(now.Nanosecond())
 
-		vals := tx.scanRange(m.chunkKey(fin, uint32(offIn/ChunkSize)), m.chunkKey(fin, uint32(offIn+size/ChunkSize)+1))
+		vals := make(map[string][]byte)
+		tx.scan(m.chunkKey(fin, uint32(offIn/ChunkSize)), m.chunkKey(fin, uint32(offIn+size/ChunkSize)+1),
+			false, func(k, v []byte) bool {
+				vals[string(k)] = v
+				return true
+			})
 		chunks := make(map[uint32][]*slice)
 		for indx := uint32(offIn / ChunkSize); indx <= uint32((offIn+size)/ChunkSize); indx++ {
 			if v, ok := vals[string(m.chunkKey(fin, indx))]; ok {
@@ -1918,14 +1948,14 @@ func (m *kvMeta) doCleanupSlices() {
 func (m *kvMeta) deleteChunk(inode Ino, indx uint32) error {
 	key := m.chunkKey(inode, indx)
 	var todel []*slice
-	err := m.txn(func(tx kvTxn) error {
+	err := m.txn(func(tx *kvTxn) error {
 		todel = todel[:0]
 		buf := tx.get(key)
 		slices := readSliceBuf(buf)
 		if slices == nil {
 			logger.Errorf("Corrupt value for inode %d chunk index %d, use `gc` to clean up leaked slices", inode, indx)
 		}
-		tx.dels(key)
+		tx.delete(key)
 		for _, s := range slices {
 			if s.id > 0 && tx.incrBy(m.sliceKey(s.id, s.size), -1) < 0 {
 				todel = append(todel, s)
@@ -1943,12 +1973,12 @@ func (m *kvMeta) deleteChunk(inode Ino, indx uint32) error {
 }
 
 func (r *kvMeta) cleanupZeroRef(id uint64, size uint32) {
-	_ = r.txn(func(tx kvTxn) error {
+	_ = r.txn(func(tx *kvTxn) error {
 		v := tx.incrBy(r.sliceKey(id, size), 0)
 		if v != 0 {
 			return syscall.EINVAL
 		}
-		tx.dels(r.sliceKey(id, size))
+		tx.delete(r.sliceKey(id, size))
 		return nil
 	})
 }
@@ -1978,10 +2008,17 @@ func (m *kvMeta) doCleanupDelayedSlices(edge int64) (int, error) {
 	var keys [][]byte
 	var batch int = 1e5
 	for {
-		if err := m.client.txn(func(tx kvTxn) error {
-			keys = tx.scanKeysRange(m.delSliceKey(0, 0), m.delSliceKey(edge, 0), batch, func(k []byte) bool {
-				return len(k) == 1+8+8 // delayed slices: Lttttttttcccccccc
-			})
+		if err := m.client.txn(func(tx *kvTxn) error {
+			keys = keys[:0]
+			var c int
+			tx.scan(m.delSliceKey(0, 0), m.delSliceKey(edge, 0),
+				true, func(k, v []byte) bool {
+					if len(k) == 1+8+8 { // delayed slices: Lttttttttcccccccc
+						keys = append(keys, k)
+						c++
+					}
+					return c < batch
+				})
 			return nil
 		}); err != nil {
 			logger.Warnf("Scan delayed slices: %s", err)
@@ -1989,7 +2026,7 @@ func (m *kvMeta) doCleanupDelayedSlices(edge int64) (int, error) {
 		}
 
 		for _, key := range keys {
-			if err := m.txn(func(tx kvTxn) error {
+			if err := m.txn(func(tx *kvTxn) error {
 				ss, rs = ss[:0], rs[:0]
 				buf := tx.get(key)
 				if len(buf) == 0 {
@@ -2002,7 +2039,7 @@ func (m *kvMeta) doCleanupDelayedSlices(edge int64) (int, error) {
 				for _, s := range ss {
 					rs = append(rs, tx.incrBy(m.sliceKey(s.Id, s.Size), -1))
 				}
-				tx.dels(key)
+				tx.delete(key)
 				return nil
 			}); err != nil {
 				logger.Warnf("Cleanup delayed slices %s: %s", key, err)
@@ -2082,7 +2119,7 @@ func (m *kvMeta) compactChunk(inode Ino, indx uint32, force bool) {
 			}
 		}
 	}
-	err = m.txn(func(tx kvTxn) error {
+	err = m.txn(func(tx *kvTxn) error {
 		buf2 := tx.get(m.chunkKey(inode, indx))
 		if len(buf2) < len(buf) || !bytes.Equal(buf, buf2[:len(buf)]) {
 			logger.Infof("chunk %d:%d was changed %d -> %d", inode, indx, len(buf), len(buf2))
@@ -2129,7 +2166,7 @@ func (m *kvMeta) compactChunk(inode Ino, indx uint32, force bool) {
 		if !trash {
 			var refs int64
 			for _, s := range ss {
-				if s.id > 0 && m.client.txn(func(tx kvTxn) error {
+				if s.id > 0 && m.client.txn(func(tx *kvTxn) error {
 					refs = tx.incrBy(m.sliceKey(s.id, s.size), 0)
 					return nil
 				}) == nil && refs < 0 {
@@ -2230,7 +2267,7 @@ func (m *kvMeta) scanDeletedSlices(ctx Context, scan deletedSliceScan) error {
 			continue
 		}
 		var clean bool
-		err = m.txn(func(tx kvTxn) error {
+		err = m.txn(func(tx *kvTxn) error {
 			ss := ss[:0]
 			rs := rs[:0]
 			v := tx.get(key)
@@ -2248,7 +2285,7 @@ func (m *kvMeta) scanDeletedSlices(ctx Context, scan deletedSliceScan) error {
 				for _, s := range ss {
 					rs = append(rs, tx.incrBy(m.sliceKey(s.Id, s.Size), -1))
 				}
-				tx.dels(key)
+				tx.delete(key)
 			}
 			return nil
 		})
@@ -2299,14 +2336,15 @@ func (m *kvMeta) scanDeletedFiles(ctx Context, scan deletedFileScan) error {
 }
 
 func (m *kvMeta) doRepair(ctx Context, inode Ino, attr *Attr) syscall.Errno {
-	return errno(m.txn(func(tx kvTxn) error {
+	prefix := m.entryKey(inode, "")
+	return errno(m.txn(func(tx *kvTxn) error {
 		attr.Nlink = 2
-		_ = tx.scanValues(m.entryKey(inode, ""), -1, func(k, v []byte) bool {
+		tx.scan(prefix, nextKey(prefix), false, func(k, v []byte) bool {
 			typ, _ := m.parseEntry(v)
 			if typ == TypeDirectory {
 				attr.Nlink++
 			}
-			return false
+			return true
 		})
 		tx.set(m.inodeKey(inode), m.marshal(attr))
 		return nil
@@ -2345,7 +2383,7 @@ func (m *kvMeta) ListXattr(ctx Context, inode Ino, names *[]byte) syscall.Errno
 
 func (m *kvMeta) doSetXattr(ctx Context, inode Ino, name string, value []byte, flags uint32) syscall.Errno {
 	key := m.xattrKey(inode, name)
-	return errno(m.txn(func(tx kvTxn) error {
+	return errno(m.txn(func(tx *kvTxn) error {
 		switch flags {
 		case XattrCreate:
 			v := tx.get(key)
@@ -2365,12 +2403,12 @@ func (m *kvMeta) doSetXattr(ctx Context, inode Ino, name string, value []byte, f
 
 func (m *kvMeta) doRemoveXattr(ctx Context, inode Ino, name string) syscall.Errno {
 	key := m.xattrKey(inode, name)
-	return errno(m.txn(func(tx kvTxn) error {
+	return errno(m.txn(func(tx *kvTxn) error {
 		value := tx.get(key)
 		if value == nil {
 			return ENOATTR
 		}
-		tx.dels(key)
+		tx.delete(key)
 		return nil
 	}))
 }
@@ -2379,7 +2417,7 @@ func (m *kvMeta) dumpEntry(inode Ino, e *DumpedEntry) error {
 	if m.snap != nil {
 		return nil
 	}
-	return m.client.txn(func(tx kvTxn) error {
+	return m.client.txn(func(tx *kvTxn) error {
 		a := tx.get(m.inodeKey(inode))
 		if a == nil {
 			logger.Warnf("inode %d not found", inode)
@@ -2393,19 +2431,24 @@ func (m *kvMeta) dumpEntry(inode Ino, e *DumpedEntry) error {
 		dumpAttr(attr, e.Attr)
 		e.Attr.Inode = inode
 
-		vals := tx.scanValues(m.xattrKey(inode, ""), -1, nil)
-		if len(vals) > 0 {
-			xattrs := make([]*DumpedXattr, 0, len(vals))
-			for k, v := range vals {
-				xattrs = append(xattrs, &DumpedXattr{k[10:], string(v)}) // "A" + inode + "X"
-			}
+		var xattrs []*DumpedXattr
+		tx.scan(m.xattrKey(inode, ""), nextKey(m.xattrKey(inode, "")), false, func(k, v []byte) bool {
+			xattrs = append(xattrs, &DumpedXattr{string(k[10:]), string(v)}) // "A" + inode + "X"
+			return true
+		})
+		if len(xattrs) > 0 {
 			sort.Slice(xattrs, func(i, j int) bool { return xattrs[i].Name < xattrs[j].Name })
 			e.Xattrs = xattrs
 		}
 
 		if attr.Typ == TypeFile {
 			e.Chunks = e.Chunks[:0]
-			vals := tx.scanRange(m.chunkKey(inode, 0), m.chunkKey(inode, uint32(attr.Length/ChunkSize)+1))
+			vals := make(map[string][]byte)
+			tx.scan(m.chunkKey(inode, 0), m.chunkKey(inode, uint32(attr.Length/ChunkSize)+1),
+				false, func(k, v []byte) bool {
+					vals[string(k)] = v
+					return true
+				})
 			for indx := uint32(0); uint64(indx)*ChunkSize < attr.Length; indx++ {
 				v, ok := vals[string(m.chunkKey(inode, indx))]
 				if !ok {
@@ -2619,7 +2662,7 @@ func (m *kvMeta) DumpMeta(w io.Writer, root Ino, keepSecret bool) (err error) {
 	tree.Name = "FSTree"
 
 	var rs [][]byte
-	err = m.txn(func(tx kvTxn) error {
+	err = m.txn(func(tx *kvTxn) error {
 		rs = tx.gets(m.counterKey(usedSpace),
 			m.counterKey(totalInodes),
 			m.counterKey("nextInode"),
@@ -2753,7 +2796,7 @@ func (m *kvMeta) loadEntry(e *DumpedEntry, kv chan *pair) {
 
 func (m *kvMeta) LoadMeta(r io.Reader) error {
 	var exist bool
-	err := m.txn(func(tx kvTxn) error {
+	err := m.txn(func(tx *kvTxn) error {
 		exist = tx.exist(m.fmtKey())
 		return nil
 	})
@@ -2778,7 +2821,7 @@ func (m *kvMeta) LoadMeta(r io.Reader) error {
 			for p := range kv {
 				buffer = append(buffer, p)
 				if len(buffer) >= batch {
-					err := m.txn(func(tx kvTxn) error {
+					err := m.txn(func(tx *kvTxn) error {
 						for _, p := range buffer {
 							tx.set(p.key, p.value)
 						}
@@ -2791,7 +2834,7 @@ func (m *kvMeta) LoadMeta(r io.Reader) error {
 				}
 			}
 			if len(buffer) > 0 {
-				err := m.txn(func(tx kvTxn) error {
+				err := m.txn(func(tx *kvTxn) error {
 					for _, p := range buffer {
 						tx.set(p.key, p.value)
 					}
@@ -2829,7 +2872,7 @@ func (m *kvMeta) LoadMeta(r io.Reader) error {
 
 	// update nlinks and parents for hardlinks
 	st := make(map[Ino]int64)
-	return m.txn(func(tx kvTxn) error {
+	return m.txn(func(tx *kvTxn) error {
 		for i, ps := range parents {
 			if len(ps) > 1 {
 				a := tx.get(m.inodeKey(i))
diff --git a/pkg/meta/tkv_badger.go b/pkg/meta/tkv_badger.go
index bab49d14..5fd62147 100644
--- a/pkg/meta/tkv_badger.go
+++ b/pkg/meta/tkv_badger.go
@@ -55,98 +55,36 @@ func (tx *badgerTxn) gets(keys ...[]byte) [][]byte {
 	return values
 }
 
-func (tx *badgerTxn) scanRange(begin, end []byte) map[string][]byte {
-	it := tx.t.NewIterator(badger.IteratorOptions{
-		PrefetchValues: true,
+func (tx *badgerTxn) scan(begin, end []byte, keysOnly bool, handler func(k, v []byte) bool) {
+	var prefix bool
+	var options = badger.IteratorOptions{
+		PrefetchValues: !keysOnly,
 		PrefetchSize:   1024,
-	})
-	defer it.Close()
-	var ret = make(map[string][]byte)
-	for it.Seek(begin); it.Valid(); it.Next() {
-		item := it.Item()
-		key := item.Key()
-		if bytes.Compare(key, end) >= 0 {
-			break
-		}
-		var value []byte
-		value, err := item.ValueCopy(nil)
-		if err != nil {
-			panic(err)
-		}
-		ret[string(key)] = value
 	}
-	return ret
-}
-
-func (tx *badgerTxn) scanKeys(prefix []byte) [][]byte {
-	it := tx.t.NewIterator(badger.IteratorOptions{
-		PrefetchValues: false,
-		PrefetchSize:   1024,
-		Prefix:         prefix,
-	})
-	defer it.Close()
-	var ret [][]byte
-	for it.Rewind(); it.Valid(); it.Next() {
-		ret = append(ret, it.Item().KeyCopy(nil))
+	if bytes.Equal(nextKey(begin), end) {
+		prefix = true
+		options.Prefix = begin
 	}
-	return ret
-}
-
-func (tx *badgerTxn) scanKeysRange(begin, end []byte, limit int, filter func(k []byte) bool) [][]byte {
-	if limit == 0 {
-		return nil
+	it := tx.t.NewIterator(options)
+	if prefix {
+		it.Rewind()
+	} else {
+		it.Seek(begin)
 	}
-	it := tx.t.NewIterator(badger.IteratorOptions{
-		PrefetchValues: true,
-		PrefetchSize:   1024,
-	})
 	defer it.Close()
-	var ret [][]byte
-	for it.Seek(begin); it.Valid(); it.Next() {
-		key := it.Item().KeyCopy(nil)
-		if bytes.Compare(key, end) >= 0 {
+	for ; it.Valid(); it.Next() {
+		item := it.Item()
+		if !prefix && bytes.Compare(item.Key(), end) >= 0 {
 			break
 		}
-		if filter == nil || filter(key) {
-			ret = append(ret, key)
-			if limit > 0 {
-				if limit--; limit == 0 {
-					break
-				}
-			}
-		}
-	}
-	return ret
-}
-
-func (tx *badgerTxn) scanValues(prefix []byte, limit int, filter func(k, v []byte) bool) map[string][]byte {
-	if limit == 0 {
-		return nil
-	}
-
-	it := tx.t.NewIterator(badger.IteratorOptions{
-		PrefetchValues: true,
-		PrefetchSize:   1024,
-		Prefix:         prefix,
-	})
-	defer it.Close()
-	var ret = make(map[string][]byte)
-	for it.Rewind(); it.Valid(); it.Next() {
-		item := it.Item()
 		value, err := item.ValueCopy(nil)
 		if err != nil {
 			panic(err)
 		}
-		if filter == nil || filter(item.Key(), value) {
-			ret[string(item.Key())] = value
-			if limit > 0 {
-				if limit--; limit == 0 {
-					break
-				}
-			}
+		if !handler(item.KeyCopy(nil), value) {
+			break
 		}
 	}
-	return ret
 }
 
 func (tx *badgerTxn) exist(prefix []byte) bool {
@@ -190,11 +128,9 @@ func (tx *badgerTxn) incrBy(key []byte, value int64) int64 {
 	return newCounter
 }
 
-func (tx *badgerTxn) dels(keys ...[]byte) {
-	for _, key := range keys {
-		if err := tx.t.Delete(key); err != nil {
-			panic(err)
-		}
+func (tx *badgerTxn) delete(key []byte) {
+	if err := tx.t.Delete(key); err != nil {
+		panic(err)
 	}
 }
 
@@ -211,9 +147,9 @@ func (c *badgerClient) shouldRetry(err error) bool {
 	return err == badger.ErrConflict
 }
 
-func (c *badgerClient) txn(f func(kvTxn) error) (err error) {
-	tx := c.client.NewTransaction(true)
-	defer tx.Discard()
+func (c *badgerClient) txn(f func(*kvTxn) error) (err error) {
+	t := c.client.NewTransaction(true)
+	defer t.Discard()
 	defer func() {
 		if r := recover(); r != nil {
 			fe, ok := r.(error)
@@ -224,13 +160,13 @@ func (c *badgerClient) txn(f func(kvTxn) error) (err error) {
 			}
 		}
 	}()
-	txn := &badgerTxn{tx, c.client}
-	err = f(txn)
+	tx := &badgerTxn{t, c.client}
+	err = f(&kvTxn{tx})
 	if err != nil {
 		return err
 	}
 	// tx could be committed
-	return txn.t.Commit()
+	return tx.t.Commit()
 }
 
 func (c *badgerClient) scan(prefix []byte, handler func(key []byte, value []byte)) error {
diff --git a/pkg/meta/tkv_etcd.go b/pkg/meta/tkv_etcd.go
index 0a24b655..75d39331 100644
--- a/pkg/meta/tkv_etcd.go
+++ b/pkg/meta/tkv_etcd.go
@@ -114,75 +114,21 @@ func (tx *etcdTxn) gets(keys ...[]byte) [][]byte {
 	return values
 }
 
-func (tx *etcdTxn) scanRange(begin_, end_ []byte) map[string][]byte {
-	resp, err := tx.kv.Get(tx.ctx, string(begin_), etcd.WithRange(string(end_)))
-	if err != nil {
-		panic(fmt.Errorf("get range [%v-%v): %s", string(begin_), string(end_), err))
-	}
-	ret := make(map[string][]byte)
-	for _, kv := range resp.Kvs {
-		k := string(kv.Key)
-		tx.observed[k] = kv.ModRevision
-		ret[k] = kv.Value
+func (tx *etcdTxn) scan(begin, end []byte, keysOnly bool, handler func(k, v []byte) bool) {
+	opts := []etcd.OpOption{etcd.WithRange(string(end))}
+	if keysOnly {
+		opts = append(opts, etcd.WithKeysOnly())
 	}
-	return ret
-}
-
-func (tx *etcdTxn) scanKeys(prefix []byte) [][]byte {
-	resp, err := tx.kv.Get(tx.ctx, string(prefix), etcd.WithPrefix(), etcd.WithKeysOnly())
+	resp, err := tx.kv.Get(tx.ctx, string(begin), opts...)
 	if err != nil {
-		panic(fmt.Errorf("get prefix %v with keys only: %s", string(prefix), err))
+		panic(fmt.Errorf("get range [%v-%v): %s", string(begin), string(end), err))
 	}
-	var keys [][]byte
 	for _, kv := range resp.Kvs {
 		tx.observed[string(kv.Key)] = kv.ModRevision
-		keys = append(keys, kv.Key)
-	}
-	return keys
-}
-
-func (tx *etcdTxn) scanKeysRange(begin_, end_ []byte, limit int, filter func(k []byte) bool) [][]byte {
-	if limit == 0 {
-		return nil
-	}
-	resp, err := tx.kv.Get(tx.ctx, string(begin_), etcd.WithRange(string(end_)), etcd.WithKeysOnly())
-	if err != nil {
-		panic(fmt.Errorf("get range [%v-%v): %s", string(begin_), string(end_), err))
-	}
-	var keys [][]byte
-	for _, kv := range resp.Kvs {
-		if filter == nil || filter(kv.Key) {
-			k := string(kv.Key)
-			tx.observed[k] = kv.ModRevision
-			keys = append(keys, kv.Key)
-			if limit > 0 && len(keys) >= limit {
-				break
-			}
-		}
-	}
-	return keys
-}
-
-func (tx *etcdTxn) scanValues(prefix []byte, limit int, filter func(k, v []byte) bool) map[string][]byte {
-	if limit == 0 {
-		return nil
-	}
-	resp, err := tx.kv.Get(tx.ctx, string(prefix), etcd.WithPrefix())
-	if err != nil {
-		panic(fmt.Errorf("get prefix %s: %s", string(prefix), err))
-	}
-	ret := make(map[string][]byte)
-	for _, kv := range resp.Kvs {
-		if filter == nil || filter(kv.Key, kv.Value) {
-			k := string(kv.Key)
-			tx.observed[k] = kv.ModRevision
-			ret[k] = kv.Value
-			if limit > 0 && len(ret) >= limit {
-				break
-			}
+		if !handler(kv.Key, kv.Value) {
+			break
 		}
 	}
-	return ret
 }
 
 func (tx *etcdTxn) exist(prefix []byte) bool {
@@ -221,10 +167,8 @@ func (tx *etcdTxn) incrBy(key []byte, value int64) int64 {
 	return new
 }
 
-func (tx *etcdTxn) dels(keys ...[]byte) {
-	for _, key := range keys {
-		tx.buffer[string(key)] = nil
-	}
+func (tx *etcdTxn) delete(key []byte) {
+	tx.buffer[string(key)] = nil
 }
 
 func (tx *etcdTxn) commmit() error {
@@ -269,7 +213,7 @@ func (c *etcdClient) shouldRetry(err error) bool {
 	return errors.Is(err, conflicted)
 }
 
-func (c *etcdClient) txn(f func(kvTxn) error) (err error) {
+func (c *etcdClient) txn(f func(*kvTxn) error) (err error) {
 	ctx := context.Background()
 	tx := &etcdTxn{
 		ctx,
@@ -287,7 +231,7 @@ func (c *etcdClient) txn(f func(kvTxn) error) (err error) {
 			}
 		}
 	}()
-	err = f(tx)
+	err = f(&kvTxn{tx})
 	if err != nil {
 		return err
 	}
diff --git a/pkg/meta/tkv_fdb.go b/pkg/meta/tkv_fdb.go
index 4ad05609..7e2f5cae 100644
--- a/pkg/meta/tkv_fdb.go
+++ b/pkg/meta/tkv_fdb.go
@@ -59,9 +59,9 @@ func (c *fdbClient) name() string {
 	return "fdb"
 }
 
-func (c *fdbClient) txn(f func(kvTxn) error) error {
+func (c *fdbClient) txn(f func(*kvTxn) error) error {
 	_, err := c.client.Transact(func(t fdb.Transaction) (interface{}, error) {
-		e := f(&fdbTxn{t})
+		e := f(&kvTxn{&fdbTxn{t}})
 		return nil, e
 	})
 	return err
@@ -134,69 +134,22 @@ func (tx *fdbTxn) gets(keys ...[]byte) [][]byte {
 	return ret
 }
 
-func (tx *fdbTxn) range0(begin, end []byte) *fdb.RangeIterator {
-	return tx.GetRange(
-		fdb.KeyRange{Begin: fdb.Key(begin), End: fdb.Key(end)},
-		fdb.RangeOptions{Mode: fdb.StreamingModeWantAll},
-	).Iterator()
-}
-
-func (tx *fdbTxn) scanRange(begin, end []byte) map[string][]byte {
-	ret := make(map[string][]byte)
-	iter := tx.range0(begin, end)
-	for iter.Advance() {
-		r := iter.MustGet()
-		ret[string(r.Key)] = r.Value
-	}
-	return ret
-}
-
-func (tx *fdbTxn) scanKeys(prefix []byte) [][]byte {
-	ret := make([][]byte, 0)
-	iter := tx.range0(prefix, nextKey(prefix))
-	for iter.Advance() {
-		ret = append(ret, iter.MustGet().Key)
-	}
-	return ret
-}
-
-func (tx *fdbTxn) scanKeysRange(begin, end []byte, limit int, filter func(k []byte) bool) [][]byte {
-	ret := make([][]byte, 0)
-	iter := tx.range0(begin, end)
-	for iter.Advance() {
-		r := iter.MustGet()
-		if filter == nil || filter(r.Key) {
-			ret = append(ret, r.Key)
-			if limit > 0 {
-				if limit--; limit == 0 {
-					break
-				}
-			}
+func (tx *fdbTxn) scan(begin, end []byte, keysOnly bool, handler func(k, v []byte) bool) {
+	it := tx.GetRange(fdb.KeyRange{Begin: fdb.Key(begin), End: fdb.Key(end)},
+		fdb.RangeOptions{Mode: fdb.StreamingModeWantAll}).Iterator()
+	for it.Advance() {
+		kv := it.MustGet()
+		if !handler(kv.Key, kv.Value) {
+			break
 		}
 	}
-	return ret
-}
-
-func (tx *fdbTxn) scanValues(prefix []byte, limit int, filter func(k, v []byte) bool) map[string][]byte {
-	ret := make(map[string][]byte)
-	iter := tx.range0(prefix, nextKey(prefix))
-	for iter.Advance() {
-		r := iter.MustGet()
-		if filter == nil || filter(r.Key, r.Value) {
-			ret[string(r.Key)] = r.Value
-			if limit > 0 {
-				if limit--; limit == 0 {
-					break
-				}
-			}
-		}
-	}
-	return ret
 }
 
 func (tx *fdbTxn) exist(prefix []byte) bool {
-	iter := tx.range0(prefix, nextKey(prefix))
-	return iter.Advance()
+	return tx.GetRange(
+		fdb.KeyRange{Begin: fdb.Key(prefix), End: fdb.Key(nextKey(prefix))},
+		fdb.RangeOptions{Mode: fdb.StreamingModeWantAll},
+	).Iterator().Advance()
 }
 
 func (tx *fdbTxn) set(key, value []byte) {
@@ -213,8 +166,6 @@ func (tx *fdbTxn) incrBy(key []byte, value int64) int64 {
 	return parseCounter(tx.Get(fdb.Key(key)).MustGet())
 }
 
-func (tx *fdbTxn) dels(keys ...[]byte) {
-	for _, key := range keys {
-		tx.Clear(fdb.Key(key))
-	}
+func (tx *fdbTxn) delete(key []byte) {
+	tx.Clear(fdb.Key(key))
 }
diff --git a/pkg/meta/tkv_lock.go b/pkg/meta/tkv_lock.go
index d3376b16..7d6cffa2 100644
--- a/pkg/meta/tkv_lock.go
+++ b/pkg/meta/tkv_lock.go
@@ -56,7 +56,7 @@ func (m *kvMeta) Flock(ctx Context, inode Ino, owner uint64, ltype uint32, block
 	var err error
 	lkey := lockOwner{m.sid, owner}
 	for {
-		err = m.txn(func(tx kvTxn) error {
+		err = m.txn(func(tx *kvTxn) error {
 			v := tx.get(ikey)
 			ls := unmarshalFlock(v)
 			switch ltype {
@@ -79,7 +79,7 @@ func (m *kvMeta) Flock(ctx Context, inode Ino, owner uint64, ltype uint32, block
 				return syscall.EINVAL
 			}
 			if len(ls) == 0 {
-				tx.dels(ikey)
+				tx.delete(ikey)
 			} else {
 				tx.set(ikey, marshalFlock(ls))
 			}
@@ -171,7 +171,7 @@ func (m *kvMeta) Setlk(ctx Context, inode Ino, owner uint64, block bool, ltype u
 	lock := plockRecord{ltype, pid, start, end}
 	lkey := lockOwner{m.sid, owner}
 	for {
-		err = m.txn(func(tx kvTxn) error {
+		err = m.txn(func(tx *kvTxn) error {
 			owners := unmarshalPlock(tx.get(ikey))
 			if ltype == F_UNLCK {
 				records := owners[lkey]
@@ -201,7 +201,7 @@ func (m *kvMeta) Setlk(ctx Context, inode Ino, owner uint64, block bool, ltype u
 				owners[lkey] = dumpLocks(ls)
 			}
 			if len(owners) == 0 {
-				tx.dels(ikey)
+				tx.delete(ikey)
 			} else {
 				tx.set(ikey, marshalPlock(owners))
 			}
@@ -229,7 +229,7 @@ func (m *kvMeta) ListLocks(ctx context.Context, inode Ino) ([]PLockItem, []FLock
 
 	var flocks []FLockItem
 	var plocks []PLockItem
-	err := m.txn(func(tx kvTxn) error {
+	err := m.txn(func(tx *kvTxn) error {
 		fv := tx.get(fKey)
 		fs := unmarshalFlock(fv)
 		for k, t := range fs {
diff --git a/pkg/meta/tkv_mem.go b/pkg/meta/tkv_mem.go
index 3d08238d..81b7fb20 100644
--- a/pkg/meta/tkv_mem.go
+++ b/pkg/meta/tkv_mem.go
@@ -17,6 +17,7 @@
 package meta
 
 import (
+	"bytes"
 	"encoding/json"
 	"fmt"
 	"io/ioutil"
@@ -77,22 +78,18 @@ func (tx *memTxn) gets(keys ...[]byte) [][]byte {
 	return values
 }
 
-func (tx *memTxn) scanRange(begin_, end_ []byte) map[string][]byte {
+func (tx *memTxn) scan(begin, end []byte, keysOnly bool, handler func(k, v []byte) bool) {
 	tx.store.Lock()
 	defer tx.store.Unlock()
-	begin := string(begin_)
-	end := string(end_)
-	ret := make(map[string][]byte)
-	tx.store.items.AscendGreaterOrEqual(&kvItem{key: begin}, func(i btree.Item) bool {
+	tx.store.items.AscendGreaterOrEqual(&kvItem{key: string(begin)}, func(i btree.Item) bool {
 		it := i.(*kvItem)
-		if end == "" || it.key < end {
-			tx.observed[it.key] = it.ver
-			ret[it.key] = it.value
-			return true
+		key := []byte(it.key)
+		if bytes.Compare(key, end) >= 0 {
+			return false
 		}
-		return false
+		tx.observed[it.key] = it.ver
+		return handler(key, it.value)
 	})
-	return ret
 }
 
 func nextKey(key []byte) []byte {
@@ -115,75 +112,19 @@ func nextKey(key []byte) []byte {
 	return next
 }
 
-func (tx *memTxn) scanKeys(prefix_ []byte) [][]byte {
-	var keys [][]byte
+func (tx *memTxn) exist(prefix []byte) bool {
+	var ret bool
 	tx.store.Lock()
 	defer tx.store.Unlock()
-	prefix := string(prefix_)
-	tx.store.items.AscendGreaterOrEqual(&kvItem{key: prefix}, func(i btree.Item) bool {
+	tx.store.items.AscendGreaterOrEqual(&kvItem{key: string(prefix)}, func(i btree.Item) bool {
 		it := i.(*kvItem)
-		if strings.HasPrefix(it.key, prefix) {
+		if strings.HasPrefix(it.key, string(prefix)) {
 			tx.observed[it.key] = it.ver
-			keys = append(keys, []byte(it.key))
-			return true
-		}
-		return false
-	})
-	return keys
-}
-
-func (tx *memTxn) scanKeysRange(begin_, end_ []byte, limit int, filter func(k []byte) bool) [][]byte {
-	if limit == 0 {
-		return nil
-	}
-	tx.store.Lock()
-	defer tx.store.Unlock()
-	begin := string(begin_)
-	end := string(end_)
-	var keys [][]byte
-	tx.store.items.AscendGreaterOrEqual(&kvItem{key: begin}, func(i btree.Item) bool {
-		it := i.(*kvItem)
-		if end == "" || it.key < end {
-			if filter == nil || filter([]byte(it.key)) {
-				tx.observed[it.key] = it.ver
-				keys = append(keys, []byte(it.key))
-				if limit > 0 {
-					if limit--; limit == 0 {
-						return false
-					}
-				}
-			}
-			return true
+			ret = true
 		}
 		return false
 	})
-	return keys
-}
-
-func (tx *memTxn) scanValues(prefix []byte, limit int, filter func(k, v []byte) bool) map[string][]byte {
-	if limit == 0 {
-		return nil
-	}
-
-	res := tx.scanRange(prefix, nextKey(prefix))
-	for k, v := range res {
-		if filter != nil && !filter([]byte(k), v) {
-			delete(res, k)
-		}
-	}
-	if n := len(res) - limit; limit > 0 && n > 0 {
-		for k := range res {
-			delete(res, k)
-			if n--; n == 0 {
-				break
-			}
-		}
-	}
-	return res
-}
-
-func (tx *memTxn) exist(prefix []byte) bool {
-	return len(tx.scanKeys(prefix)) > 0
+	return ret
 }
 
 func (tx *memTxn) set(key, value []byte) {
@@ -206,10 +147,8 @@ func (tx *memTxn) incrBy(key []byte, value int64) int64 {
 	return new
 }
 
-func (tx *memTxn) dels(keys ...[]byte) {
-	for _, key := range keys {
-		tx.buffer[string(key)] = nil
-	}
+func (tx *memTxn) delete(key []byte) {
+	tx.buffer[string(key)] = nil
 }
 
 type kvItem struct {
@@ -260,13 +199,13 @@ func (c *memKV) set(key string, value []byte) {
 	}
 }
 
-func (c *memKV) txn(f func(kvTxn) error) error {
+func (c *memKV) txn(f func(*kvTxn) error) error {
 	tx := &memTxn{
 		store:    c,
 		observed: make(map[string]int),
 		buffer:   make(map[string][]byte),
 	}
-	if err := f(tx); err != nil {
+	if err := f(&kvTxn{tx}); err != nil {
 		return err
 	}
 
@@ -321,9 +260,9 @@ func (c *memKV) reset(prefix []byte) error {
 		c.Unlock()
 		return nil
 	}
-	return c.txn(func(kt kvTxn) error {
+	return c.txn(func(kt *kvTxn) error {
 		return c.scan(prefix, func(key, value []byte) {
-			kt.dels(key)
+			kt.delete(key)
 		})
 	})
 }
diff --git a/pkg/meta/tkv_prefix.go b/pkg/meta/tkv_prefix.go
index ee0560b2..06508c50 100644
--- a/pkg/meta/tkv_prefix.go
+++ b/pkg/meta/tkv_prefix.go
@@ -19,7 +19,7 @@ package meta
 import "fmt"
 
 type prefixTxn struct {
-	kvTxn
+	*kvTxn
 	prefix []byte
 }
 
@@ -45,48 +45,10 @@ func (tx *prefixTxn) gets(keys ...[]byte) [][]byte {
 	return tx.kvTxn.gets(keys...)
 }
 
-func (tx *prefixTxn) scanRange(begin_, end_ []byte) map[string][]byte {
-	r := tx.kvTxn.scanRange(tx.realKey(begin_), tx.realKey(end_))
-	m := make(map[string][]byte, len(r))
-	for k, v := range r {
-		m[k[len(tx.prefix):]] = v
-	}
-	return m
-}
-
-func (tx *prefixTxn) scanKeys(prefix []byte) [][]byte {
-	keys := tx.kvTxn.scanKeys(tx.realKey(prefix))
-	for i, k := range keys {
-		keys[i] = tx.origKey(k)
-	}
-	return keys
-}
-
-func (tx *prefixTxn) scanKeysRange(begin, end []byte, limit int, filter func(k []byte) bool) [][]byte {
-	keys := tx.kvTxn.scanKeysRange(tx.realKey(begin), tx.realKey(end), limit, func(k []byte) bool {
-		if filter == nil {
-			return true
-		}
-		return filter(tx.origKey(k))
-	})
-	for i, k := range keys {
-		keys[i] = tx.origKey(k)
-	}
-	return keys
-}
-
-func (tx *prefixTxn) scanValues(prefix []byte, limit int, filter func(k, v []byte) bool) map[string][]byte {
-	r := tx.kvTxn.scanValues(tx.realKey(prefix), limit, func(k, v []byte) bool {
-		if filter == nil {
-			return true
-		}
-		return filter(tx.origKey(k), v)
+func (tx *prefixTxn) scan(begin, end []byte, keysOnly bool, handler func(k, v []byte) bool) {
+	tx.kvTxn.scan(tx.realKey(begin), tx.realKey(end), keysOnly, func(k, v []byte) bool {
+		return handler(tx.origKey(k), v)
 	})
-	m := make(map[string][]byte, len(r))
-	for k, v := range r {
-		m[k[len(tx.prefix):]] = v
-	}
-	return m
 }
 
 func (tx *prefixTxn) exist(prefix []byte) bool {
@@ -105,11 +67,8 @@ func (tx *prefixTxn) incrBy(key []byte, value int64) int64 {
 	return tx.kvTxn.incrBy(tx.realKey(key), value)
 }
 
-func (tx *prefixTxn) dels(keys ...[]byte) {
-	for i, key := range keys {
-		keys[i] = tx.realKey(key)
-	}
-	tx.kvTxn.dels(keys...)
+func (tx *prefixTxn) delete(key []byte) {
+	tx.kvTxn.delete(tx.realKey(key))
 }
 
 type prefixClient struct {
@@ -117,9 +76,9 @@ type prefixClient struct {
 	prefix []byte
 }
 
-func (c *prefixClient) txn(f func(kvTxn) error) error {
-	return c.tkvClient.txn(func(tx kvTxn) error {
-		return f(&prefixTxn{tx, c.prefix})
+func (c *prefixClient) txn(f func(*kvTxn) error) error {
+	return c.tkvClient.txn(func(tx *kvTxn) error {
+		return f(&kvTxn{&prefixTxn{tx, c.prefix}})
 	})
 }
 
diff --git a/pkg/meta/tkv_tikv.go b/pkg/meta/tkv_tikv.go
index a453e487..a0d5c721 100644
--- a/pkg/meta/tkv_tikv.go
+++ b/pkg/meta/tkv_tikv.go
@@ -106,84 +106,17 @@ func (tx *tikvTxn) gets(keys ...[]byte) [][]byte {
 	return values
 }
 
-func (tx *tikvTxn) scanRange0(begin, end []byte, limit int, filter func(k, v []byte) bool) map[string][]byte {
-	if limit == 0 {
-		return nil
-	}
-
+func (tx *tikvTxn) scan(begin, end []byte, keysOnly bool, handler func(k, v []byte) bool) {
 	it, err := tx.Iter(begin, end)
 	if err != nil {
 		panic(err)
 	}
 	defer it.Close()
-	var ret = make(map[string][]byte)
-	for it.Valid() {
-		key := it.Key()
-		value := it.Value()
-		if filter == nil || filter(key, value) {
-			ret[string(key)] = value
-			if limit > 0 {
-				if limit--; limit == 0 {
-					break
-				}
-			}
-		}
+	for it.Valid() && handler(it.Key(), it.Value()) {
 		if err = it.Next(); err != nil {
 			panic(err)
 		}
 	}
-	return ret
-}
-
-func (tx *tikvTxn) scanRange(begin, end []byte) map[string][]byte {
-	return tx.scanRange0(begin, end, -1, nil)
-}
-
-func (tx *tikvTxn) scanKeys(prefix []byte) [][]byte {
-	it, err := tx.Iter(prefix, nextKey(prefix))
-	if err != nil {
-		panic(err)
-	}
-	defer it.Close()
-	var ret [][]byte
-	for it.Valid() {
-		ret = append(ret, it.Key())
-		if err = it.Next(); err != nil {
-			panic(err)
-		}
-	}
-	return ret
-}
-
-func (tx *tikvTxn) scanKeysRange(begin, end []byte, limit int, filter func(k []byte) bool) [][]byte {
-	if limit == 0 {
-		return nil
-	}
-	it, err := tx.Iter(begin, end)
-	if err != nil {
-		panic(err)
-	}
-	defer it.Close()
-	var ret [][]byte
-	for it.Valid() {
-		key := it.Key()
-		if filter == nil || filter(key) {
-			ret = append(ret, it.Key())
-			if limit > 0 {
-				if limit--; limit == 0 {
-					break
-				}
-			}
-		}
-		if err = it.Next(); err != nil {
-			panic(err)
-		}
-	}
-	return ret
-}
-
-func (tx *tikvTxn) scanValues(prefix []byte, limit int, filter func(k, v []byte) bool) map[string][]byte {
-	return tx.scanRange0(prefix, nextKey(prefix), limit, filter)
 }
 
 func (tx *tikvTxn) exist(prefix []byte) bool {
@@ -217,11 +150,9 @@ func (tx *tikvTxn) incrBy(key []byte, value int64) int64 {
 	return new
 }
 
-func (tx *tikvTxn) dels(keys ...[]byte) {
-	for _, key := range keys {
-		if err := tx.Delete(key); err != nil {
-			panic(err)
-		}
+func (tx *tikvTxn) delete(key []byte) {
+	if err := tx.Delete(key); err != nil {
+		panic(err)
 	}
 }
 
@@ -237,7 +168,7 @@ func (c *tikvClient) shouldRetry(err error) bool {
 	return strings.Contains(err.Error(), "write conflict") || strings.Contains(err.Error(), "TxnLockNotFound")
 }
 
-func (c *tikvClient) txn(f func(kvTxn) error) (err error) {
+func (c *tikvClient) txn(f func(*kvTxn) error) (err error) {
 	tx, err := c.client.Begin()
 	if err != nil {
 		return err
@@ -252,7 +183,7 @@ func (c *tikvClient) txn(f func(kvTxn) error) (err error) {
 			}
 		}
 	}()
-	if err = f(&tikvTxn{tx}); err != nil {
+	if err = f(&kvTxn{&tikvTxn{tx}}); err != nil {
 		return err
 	}
 	if !tx.IsReadOnly() {


Test patch:
diff --git a/pkg/meta/base_test.go b/pkg/meta/base_test.go
index bc60b46b..edaacf2f 100644
--- a/pkg/meta/base_test.go
+++ b/pkg/meta/base_test.go
@@ -1707,7 +1707,7 @@ func setAttr(t *testing.T, m Meta, inode Ino, attr *Attr) {
 			return err
 		})
 	case *kvMeta:
-		err = m.txn(func(tx kvTxn) error {
+		err = m.txn(func(tx *kvTxn) error {
 			tx.set(m.inodeKey(inode), m.marshal(attr))
 			return nil
 		})
diff --git a/pkg/meta/tkv_test.go b/pkg/meta/tkv_test.go
index fd6532d8..ebf09a0f 100644
--- a/pkg/meta/tkv_test.go
+++ b/pkg/meta/tkv_test.go
@@ -20,7 +20,9 @@ package meta
 
 import (
 	"bytes"
+	"fmt"
 	"os"
+	"sort"
 	"testing"
 )
 
@@ -58,8 +60,8 @@ func TestEtcdClient(t *testing.T) {
 }
 
 func testTKV(t *testing.T, c tkvClient) {
-	txn := func(f func(kt kvTxn)) {
-		if err := c.txn(func(kt kvTxn) error {
+	txn := func(f func(kt *kvTxn)) {
+		if err := c.txn(func(kt *kvTxn) error {
 			f(kt)
 			return nil
 		}); err != nil {
@@ -72,28 +74,28 @@ func testTKV(t *testing.T, c tkvClient) {
 		t.Fatalf("reset: %s", err)
 	}
 	var hasKey bool
-	txn(func(kt kvTxn) { hasKey = kt.exist(nil) })
+	txn(func(kt *kvTxn) { hasKey = kt.exist(nil) })
 	if hasKey {
 		t.Fatalf("has key after reset")
 	}
 	k := []byte("k")
 	v := []byte("value")
 
-	txn(func(kt kvTxn) {
+	txn(func(kt *kvTxn) {
 		kt.set(k, v)
 		kt.append(k, v)
 	})
 	var r []byte
-	txn(func(kt kvTxn) { r = kt.get(k) })
+	txn(func(kt *kvTxn) { r = kt.get(k) })
 	if !bytes.Equal(r, []byte("valuevalue")) {
 		t.Fatalf("expect 'valuevalue', but got %v", string(r))
 	}
-	txn(func(kt kvTxn) {
+	txn(func(kt *kvTxn) {
 		kt.set([]byte("k2"), v)
 		kt.set([]byte("v"), k)
 	})
 	var ks [][]byte
-	txn(func(kt kvTxn) { ks = kt.gets([]byte("k1"), []byte("k2")) })
+	txn(func(kt *kvTxn) { ks = kt.gets([]byte("k1"), []byte("k2")) })
 	if ks[0] != nil || string(ks[1]) != "value" {
 		t.Fatalf("gets k1,k2: %+v != %+v", ks, [][]byte{nil, []byte("value")})
 	}
@@ -105,68 +107,108 @@ func testTKV(t *testing.T, c tkvClient) {
 	if len(keys) != 2 || string(keys[0]) != "k" || string(keys[1]) != "k2" {
 		t.Fatalf("keys: %+v", keys)
 	}
-	txn(func(kt kvTxn) {
-		keys = kt.scanKeysRange([]byte("a"), []byte("z"), -1, func(k []byte) bool { return len(k) == 1 })
+	keys = keys[:0]
+	txn(func(kt *kvTxn) {
+		kt.scan([]byte("a"), []byte("z"), true, func(k, v []byte) bool {
+			if len(k) == 1 {
+				keys = append(keys, k)
+			}
+			return true
+		})
 	})
 	if len(keys) != 2 || string(keys[0]) != "k" || string(keys[1]) != "v" {
 		t.Fatalf("keys: %+v", keys)
 	}
-	txn(func(kt kvTxn) {
-		keys = kt.scanKeysRange([]byte("k"), []byte("l"), -1, nil)
+	keys = keys[:0]
+	txn(func(kt *kvTxn) {
+		kt.scan([]byte("k"), []byte("l"), true, func(k, v []byte) bool {
+			keys = append(keys, k)
+			return true
+		})
 	})
 	if len(keys) != 2 || string(keys[0]) != "k" || string(keys[1]) != "k2" {
 		t.Fatalf("keys: %+v", keys)
 	}
-	txn(func(kt kvTxn) { keys = kt.scanKeys(nil) })
+	keys = keys[:0]
+	txn(func(kt *kvTxn) {
+		kt.scan([]byte("a"), []byte("z"), true, func(k, v []byte) bool {
+			keys = append(keys, k)
+			return true
+		})
+	})
 	if len(keys) != 3 || string(keys[0]) != "k" || string(keys[1]) != "k2" || string(keys[2]) != "v" {
 		t.Fatalf("keys: %+v", keys)
 	}
-	var values map[string][]byte
-	txn(func(kt kvTxn) { values = kt.scanValues([]byte("k"), -1, func(k, v []byte) bool { return len(v) == 5 }) })
+	values := make(map[string][]byte)
+	txn(func(kt *kvTxn) {
+		kt.scan([]byte("k"), nextKey([]byte("k")), false, func(k, v []byte) bool {
+			if len(v) == 5 {
+				values[string(k)] = v
+			}
+			return true
+		})
+	})
 	if len(values) != 1 || string(values["k2"]) != "value" {
 		t.Fatalf("scan values: %+v", values)
 	}
-	txn(func(kt kvTxn) { values = kt.scanRange([]byte("k2"), []byte("v")) })
+	values = make(map[string][]byte)
+	txn(func(kt *kvTxn) {
+		kt.scan([]byte("k2"), []byte("v"),
+			false, func(k, v []byte) bool {
+				values[string(k)] = v
+				return true
+			})
+	})
 	if len(values) != 1 || string(values["k2"]) != "value" {
 		t.Fatalf("scanRange: %+v", values)
 	}
 
 	// exists
-	txn(func(kt kvTxn) { hasKey = kt.exist([]byte("k")) })
+	txn(func(kt *kvTxn) { hasKey = kt.exist([]byte("k")) })
 	if !hasKey {
 		t.Fatalf("has key k*")
 	}
-	txn(func(kt kvTxn) { kt.dels(keys...) })
-	txn(func(kt kvTxn) { r = kt.get(k) })
+	txn(func(kt *kvTxn) {
+		for _, key := range keys {
+			kt.delete(key)
+		}
+	})
+	txn(func(kt *kvTxn) { r = kt.get(k) })
 	if r != nil {
 		t.Fatalf("expect nil, but got %v", string(r))
 	}
-	txn(func(kt kvTxn) { keys = kt.scanKeys(nil) })
+	keys = keys[:0]
+	txn(func(kt *kvTxn) {
+		kt.scan([]byte("a"), []byte("z"), true, func(k, v []byte) bool {
+			keys = append(keys, k)
+			return true
+		})
+	})
 	if len(keys) != 0 {
 		t.Fatalf("no keys: %+v", keys)
 	}
-	txn(func(kt kvTxn) { hasKey = kt.exist(nil) })
+	txn(func(kt *kvTxn) { hasKey = kt.exist(nil) })
 	if hasKey {
 		t.Fatalf("has not keys")
 	}
 
 	// counters
 	var count int64
-	c.txn(func(tx kvTxn) error {
+	c.txn(func(tx *kvTxn) error {
 		count = tx.incrBy([]byte("counter"), -1)
 		return nil
 	})
 	if count != -1 {
 		t.Fatalf("counter should be -1, but got %d", count)
 	}
-	c.txn(func(tx kvTxn) error {
+	c.txn(func(tx *kvTxn) error {
 		count = tx.incrBy([]byte("counter"), 0)
 		return nil
 	})
 	if count != -1 {
 		t.Fatalf("counter should be -1, but got %d", count)
 	}
-	c.txn(func(tx kvTxn) error {
+	c.txn(func(tx *kvTxn) error {
 		count = tx.incrBy([]byte("counter"), 2)
 		return nil
 	})
@@ -176,16 +218,42 @@ func testTKV(t *testing.T, c tkvClient) {
 
 	// key with zeros
 	k = []byte("k\x001")
-	txn(func(kt kvTxn) {
+	txn(func(kt *kvTxn) {
 		kt.set(k, v)
 	})
 	var v2 []byte
-	txn(func(kt kvTxn) {
+	txn(func(kt *kvTxn) {
 		v2 = kt.get(k)
 	})
 	if !bytes.Equal(v2, v) {
 		t.Fatalf("expect %v but got %v", v, v2)
 	}
+
+	// scan many key-value pairs
+	keys = make([][]byte, 0, 100000)
+	for i := 0; i < 1000; i++ {
+		txn(func(kt *kvTxn) {
+			for j := 0; j < 100; j++ {
+				k := []byte(fmt.Sprintf("Key_%d_%d", i, j))
+				v := []byte(fmt.Sprintf("Value_%d_%d", i, j))
+				kt.set(k, v)
+				keys = append(keys, k)
+			}
+		})
+	}
+	kvs := make([][]byte, 0, 200000)
+	txn(func(kt *kvTxn) {
+		kt.scan([]byte("A"), []byte("Z"), false, func(k, v []byte) bool {
+			kvs = append(kvs, k, v)
+			return true
+		})
+	})
+	sort.Slice(keys, func(i, j int) bool { return bytes.Compare(keys[i], keys[j]) < 0 })
+	for i, k := range keys {
+		if !bytes.Equal(k, kvs[i*2]) || !bytes.Equal([]byte(fmt.Sprintf("Value%s", k[3:])), kvs[i*2+1]) {
+			t.Fatalf("expect %s but got %s, %s", k, keys[i*2], keys[i*2+1])
+		}
+	}
 }
 
 func TestBadgerKV(t *testing.T) {