perf: support batch delete keys without scan confirm (#283)

This commit is contained in:
Lykin 2024-06-17 18:29:56 +08:00
parent 1bcde26e35
commit 86f42fcc10
12 changed files with 198 additions and 17 deletions

View File

@ -2292,6 +2292,84 @@ func (b *browserService) DeleteKeys(server string, db int, ks []any, serialNo st
return
}
// DeleteKeysByPattern delete keys by pattern
func (b *browserService) DeleteKeysByPattern(server string, db int, pattern string) (resp types.JSResp) {
conf := Connection().getConnection(server)
if conf == nil {
resp.Msg = fmt.Sprintf("no connection profile named: %s", server)
return
}
var client redis.UniversalClient
var err error
var connConfig = conf.ConnectionConfig
connConfig.LastDB = db
if client, err = b.createRedisClient(connConfig); err != nil {
resp.Msg = err.Error()
return
}
ctx, cancelFunc := context.WithCancel(b.ctx)
defer client.Close()
defer cancelFunc()
var ks []any
ks, _, err = b.scanKeys(ctx, client, pattern, "", 0, 0)
if err != nil {
resp.Msg = err.Error()
return
}
total := len(ks)
var canceled bool
var deletedKeys = make([]any, 0, total)
var mutex sync.Mutex
del := func(ctx context.Context, cli redis.UniversalClient) error {
const batchSize = 1000
for i := 0; i < total; i += batchSize {
pipe := cli.Pipeline()
for j := 0; j < batchSize; j++ {
if i+j < total {
pipe.Del(ctx, strutil.DecodeRedisKey(ks[i+j]))
}
}
cmders, delErr := pipe.Exec(ctx)
for j, cmder := range cmders {
if cmder.(*redis.IntCmd).Val() == 1 {
// save deleted key
mutex.Lock()
deletedKeys = append(deletedKeys, ks[i+j])
mutex.Unlock()
}
}
if errors.Is(delErr, context.Canceled) || canceled {
canceled = true
break
}
}
return nil
}
if cluster, ok := client.(*redis.ClusterClient); ok {
// cluster mode
err = cluster.ForEachMaster(ctx, func(ctx context.Context, cli *redis.Client) error {
return del(ctx, cli)
})
} else {
err = del(ctx, client)
}
resp.Success = true
resp.Data = struct {
Canceled bool `json:"canceled"`
Deleted any `json:"deleted"`
Failed int `json:"failed"`
}{
Canceled: canceled,
Deleted: deletedKeys,
Failed: len(ks) - len(deletedKeys),
}
return
}
// ExportKey export keys
func (b *browserService) ExportKey(server string, db int, ks []any, path string, includeExpire bool) (resp types.JSResp) {
// connect a new connection to export keys

View File

@ -1,7 +1,6 @@
<script setup>
import { computed, nextTick, reactive, ref, watchEffect } from 'vue'
import useDialog from 'stores/dialog'
import { useI18n } from 'vue-i18n'
import { isEmpty, map, size } from 'lodash'
import useBrowserStore from 'stores/browser.js'
import { decodeRedisKey } from '@/utils/key_convert.js'
@ -14,6 +13,7 @@ const deleteForm = reactive({
loadingAffected: false,
affectedKeys: [],
async: true,
direct: false,
})
const dialogStore = useDialog()
@ -68,7 +68,6 @@ const keyLines = computed(() => {
return map(deleteForm.affectedKeys, (k) => decodeRedisKey(k))
})
const i18n = useI18n()
const onConfirmDelete = async () => {
try {
deleting.value = true
@ -84,6 +83,21 @@ const onConfirmDelete = async () => {
dialogStore.closeDeleteKeyDialog()
}
const onConfirmDirectDelete = async () => {
try {
deleting.value = true
const { server, db, key } = deleteForm
await nextTick()
browserStore.deleteByPattern(server, db, key).catch((e) => {})
} catch (e) {
$message.error(e.message)
return
} finally {
deleting.value = false
}
dialogStore.closeDeleteKeyDialog()
}
const onClose = () => {
dialogStore.closeDeleteKeyDialog()
}
@ -116,9 +130,9 @@ const onClose = () => {
required>
<n-input v-model:value="deleteForm.key" placeholder="" @input="resetAffected" />
</n-form-item>
<!-- <n-checkbox v-model:checked="deleteForm.async">-->
<!-- {{ $t('dialogue.key.silent') }}-->
<!-- </n-checkbox>-->
<n-checkbox v-model:checked="deleteForm.direct">
{{ $t('dialogue.key.direct_delete') }}
</n-checkbox>
<n-card
v-if="deleteForm.showAffected"
:title="$t('dialogue.key.affected_key') + `(${size(deleteForm.affectedKeys)})`"
@ -140,22 +154,32 @@ const onClose = () => {
<div class="flex-item n-dialog__action">
<n-button :disabled="loading" :focusable="false" @click="onClose">{{ $t('common.cancel') }}</n-button>
<n-button
v-if="!deleteForm.showAffected"
v-if="deleteForm.direct"
:focusable="false"
:loading="loading"
type="primary"
@click="scanAffectedKey">
{{ $t('dialogue.key.show_affected_key') }}
</n-button>
<n-button
v-else
:disabled="isEmpty(deleteForm.affectedKeys)"
:focusable="false"
:loading="loading"
type="primary"
@click="onConfirmDelete">
{{ $t('dialogue.key.confirm_delete_key', { num: size(deleteForm.affectedKeys) }) }}
@click="onConfirmDirectDelete">
{{ $t('dialogue.key.confirm_delete') }}
</n-button>
<template v-else>
<n-button
v-if="!deleteForm.showAffected"
:focusable="false"
:loading="loading"
type="primary"
@click="scanAffectedKey">
{{ $t('dialogue.key.show_affected_key') }}
</n-button>
<n-button
v-else
:disabled="isEmpty(deleteForm.affectedKeys)"
:focusable="false"
:loading="loading"
type="primary"
@click="onConfirmDelete">
{{ $t('dialogue.key.confirm_delete_key', { num: size(deleteForm.affectedKeys) }) }}
</n-button>
</template>
</div>
</template>
</n-modal>

View File

@ -296,6 +296,8 @@
"affected_key": "Affected Keys",
"show_affected_key": "Show Affected Keys",
"confirm_delete_key": "Confirm delete {num} key(s)",
"direct_delete": "Delete match pattern directly",
"confirm_delete": "Confirm Delete",
"async_delete": "Async Execution",
"async_delete_title": "Don't wait for result",
"confirm_flush": "I know what I'm doing!",

View File

@ -296,6 +296,8 @@
"affected_key": "Claves afectadas",
"show_affected_key": "Mostrar claves afectadas",
"confirm_delete_key": "Confirmar eliminar {num} clave(s)",
"direct_delete": "Eliminar el patrón coincidente directamente",
"confirm_delete": "Confirmar eliminación",
"async_delete": "Ejecución asíncrona",
"async_delete_title": "No esperar el resultado",
"confirm_flush": "¡Sé lo que estoy haciendo!",

View File

@ -296,6 +296,8 @@
"affected_key": "Clés affectées",
"show_affected_key": "Afficher les clés affectées",
"confirm_delete_key": "Confirmer la suppression de {num} clé(s)",
"direct_delete": "Supprimer le modèle correspondant directement",
"confirm_delete": "Confirmer la suppression",
"async_delete": "Exécution asynchrone",
"async_delete_title": "Ne pas attendre le résultat",
"confirm_flush": "Je sais ce que je fais !",

View File

@ -296,6 +296,8 @@
"affected_key": "影響を受けるキー",
"show_affected_key": "影響を受けるキーを表示",
"confirm_delete_key": "{num}個のキーを削除することを確認",
"direct_delete": "一致するパターンを直接削除",
"confirm_delete": "削除を確認",
"async_delete": "非同期実行",
"async_delete_title": "結果を待たない",
"confirm_flush": "自分が実行しようとしている操作を理解しています!",

View File

@ -296,6 +296,8 @@
"affected_key": "영향받는 키",
"show_affected_key": "영향받는 키 표시",
"confirm_delete_key": "{num}개의 키를 삭제하시겠습니까?",
"direct_delete": "일치하는 패턴 직접 삭제",
"confirm_delete": "삭제 확인",
"async_delete": "비동기 실행",
"async_delete_title": "결과를 기다리지 않음",
"confirm_flush": "진행 중인 작업을 알고 있습니다!",

View File

@ -296,6 +296,8 @@
"affected_key": "Chaves Afetadas",
"show_affected_key": "Mostrar Chaves Afetadas",
"confirm_delete_key": "Confirmar Exclusão de {num} Chave(s)",
"direct_delete": "Excluir padrão correspondente diretamente",
"confirm_delete": "Confirmar exclusão",
"async_delete": "Execução Assíncrona",
"async_delete_title": "Não esperar pelo resultado da operação",
"confirm_flush": "Eu sei o que estou fazendo!",

View File

@ -296,6 +296,8 @@
"affected_key": "Затронутые ключи",
"show_affected_key": "Показать затронутые ключи",
"confirm_delete_key": "Подтвердить удаление {num} ключ(ей/ей)",
"direct_delete": "Удалить совпадающий шаблон напрямую",
"confirm_delete": "Подтвердить удаление",
"async_delete": "Асинхронное выполнение",
"async_delete_title": "Не ждать результата",
"confirm_flush": "Я знаю, что делаю!",

View File

@ -296,6 +296,8 @@
"affected_key": "受影响的键名",
"show_affected_key": "查看受影响的键名",
"confirm_delete_key": "确认删除{num}个键",
"direct_delete": "直接匹配删除",
"confirm_delete": "确认删除",
"async_delete": "异步执行",
"async_delete_title": "不等待操作结果",
"confirm_flush": "我知道我正在执行的操作!",

View File

@ -296,6 +296,8 @@
"affected_key": "受影響的鍵名",
"show_affected_key": "檢視受影響的鍵名",
"confirm_delete_key": "確認刪除{num}個鍵",
"direct_delete": "直接匹配刪除",
"confirm_delete": "確認刪除",
"async_delete": "異步執行",
"async_delete_title": "不等待操作結果",
"confirm_flush": "我知道我正在執行的操作!",

View File

@ -11,6 +11,7 @@ import {
ConvertValue,
DeleteKey,
DeleteKeys,
DeleteKeysByPattern,
ExportKey,
FlushDB,
GetClientList,
@ -1774,6 +1775,66 @@ const useBrowserStore = defineStore('browser', {
})
},
/**
* delete multiple keys by pattern
* @param server
* @param db
* @param pattern
* @return {Promise<void>}
*/
async deleteByPattern(server, db, pattern) {
const msgRef = $message.loading(i18nGlobal.t('dialogue.delete.deleting'), { duration: 0, closable: true })
let deleted = []
let failCount = 0
let canceled = false
try {
const { success, msg, data } = await DeleteKeysByPattern(server, db, pattern)
if (success) {
canceled = get(data, 'canceled', false)
deleted = get(data, 'deleted', [])
failCount = get(data, 'failed', 0)
} else {
$message.error(msg)
}
} finally {
msgRef.destroy()
// clear checked keys
const tab = useTabStore()
tab.setCheckedKeys(server)
}
// refresh model data
const deletedCount = size(deleted)
if (canceled) {
$message.info(i18nGlobal.t('dialogue.handle_cancel'))
} else if (failCount <= 0) {
// no fail
$message.success(i18nGlobal.t('dialogue.delete.completed', { success: deletedCount, fail: failCount }))
} else if (failCount >= deletedCount) {
// all fail
$message.error(i18nGlobal.t('dialogue.delete.completed', { success: deletedCount, fail: failCount }))
} else {
// some fail
$message.warning(i18nGlobal.t('dialogue.delete.completed', { success: deletedCount, fail: failCount }))
}
// update ui
timeout(100).then(async () => {
/** @type RedisServerState **/
const serverInst = this.servers[server]
if (serverInst != null) {
let start = now()
for (let i = 0; i < deleted.length; i++) {
serverInst.removeKeyNode(deleted[i], false)
if (now() - start > 300) {
await timeout(100)
start = now()
}
}
serverInst.tidyNode('', true)
serverInst.updateDBKeyCount(db, -deletedCount)
}
})
},
/**
* export multiple keys
* @param {string} server