perf: support batch delete keys without scan confirm (#283)
This commit is contained in:
parent
1bcde26e35
commit
86f42fcc10
|
@ -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
|
||||
|
|
|
@ -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)})`"
|
||||
|
@ -139,6 +153,15 @@ const onClose = () => {
|
|||
<template #action>
|
||||
<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.direct"
|
||||
:focusable="false"
|
||||
:loading="loading"
|
||||
type="primary"
|
||||
@click="onConfirmDirectDelete">
|
||||
{{ $t('dialogue.key.confirm_delete') }}
|
||||
</n-button>
|
||||
<template v-else>
|
||||
<n-button
|
||||
v-if="!deleteForm.showAffected"
|
||||
:focusable="false"
|
||||
|
@ -156,6 +179,7 @@ const onClose = () => {
|
|||
@click="onConfirmDelete">
|
||||
{{ $t('dialogue.key.confirm_delete_key', { num: size(deleteForm.affectedKeys) }) }}
|
||||
</n-button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</n-modal>
|
||||
|
|
|
@ -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!",
|
||||
|
|
|
@ -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!",
|
||||
|
|
|
@ -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 !",
|
||||
|
|
|
@ -296,6 +296,8 @@
|
|||
"affected_key": "影響を受けるキー",
|
||||
"show_affected_key": "影響を受けるキーを表示",
|
||||
"confirm_delete_key": "{num}個のキーを削除することを確認",
|
||||
"direct_delete": "一致するパターンを直接削除",
|
||||
"confirm_delete": "削除を確認",
|
||||
"async_delete": "非同期実行",
|
||||
"async_delete_title": "結果を待たない",
|
||||
"confirm_flush": "自分が実行しようとしている操作を理解しています!",
|
||||
|
|
|
@ -296,6 +296,8 @@
|
|||
"affected_key": "영향받는 키",
|
||||
"show_affected_key": "영향받는 키 표시",
|
||||
"confirm_delete_key": "{num}개의 키를 삭제하시겠습니까?",
|
||||
"direct_delete": "일치하는 패턴 직접 삭제",
|
||||
"confirm_delete": "삭제 확인",
|
||||
"async_delete": "비동기 실행",
|
||||
"async_delete_title": "결과를 기다리지 않음",
|
||||
"confirm_flush": "진행 중인 작업을 알고 있습니다!",
|
||||
|
|
|
@ -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!",
|
||||
|
|
|
@ -296,6 +296,8 @@
|
|||
"affected_key": "Затронутые ключи",
|
||||
"show_affected_key": "Показать затронутые ключи",
|
||||
"confirm_delete_key": "Подтвердить удаление {num} ключ(ей/ей)",
|
||||
"direct_delete": "Удалить совпадающий шаблон напрямую",
|
||||
"confirm_delete": "Подтвердить удаление",
|
||||
"async_delete": "Асинхронное выполнение",
|
||||
"async_delete_title": "Не ждать результата",
|
||||
"confirm_flush": "Я знаю, что делаю!",
|
||||
|
|
|
@ -296,6 +296,8 @@
|
|||
"affected_key": "受影响的键名",
|
||||
"show_affected_key": "查看受影响的键名",
|
||||
"confirm_delete_key": "确认删除{num}个键",
|
||||
"direct_delete": "直接匹配删除",
|
||||
"confirm_delete": "确认删除",
|
||||
"async_delete": "异步执行",
|
||||
"async_delete_title": "不等待操作结果",
|
||||
"confirm_flush": "我知道我正在执行的操作!",
|
||||
|
|
|
@ -296,6 +296,8 @@
|
|||
"affected_key": "受影響的鍵名",
|
||||
"show_affected_key": "檢視受影響的鍵名",
|
||||
"confirm_delete_key": "確認刪除{num}個鍵",
|
||||
"direct_delete": "直接匹配刪除",
|
||||
"confirm_delete": "確認刪除",
|
||||
"async_delete": "異步執行",
|
||||
"async_delete_title": "不等待操作結果",
|
||||
"confirm_flush": "我知道我正在執行的操作!",
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue