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
|
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
|
// ExportKey export keys
|
||||||
func (b *browserService) ExportKey(server string, db int, ks []any, path string, includeExpire bool) (resp types.JSResp) {
|
func (b *browserService) ExportKey(server string, db int, ks []any, path string, includeExpire bool) (resp types.JSResp) {
|
||||||
// connect a new connection to export keys
|
// connect a new connection to export keys
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, nextTick, reactive, ref, watchEffect } from 'vue'
|
import { computed, nextTick, reactive, ref, watchEffect } from 'vue'
|
||||||
import useDialog from 'stores/dialog'
|
import useDialog from 'stores/dialog'
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
import { isEmpty, map, size } from 'lodash'
|
import { isEmpty, map, size } from 'lodash'
|
||||||
import useBrowserStore from 'stores/browser.js'
|
import useBrowserStore from 'stores/browser.js'
|
||||||
import { decodeRedisKey } from '@/utils/key_convert.js'
|
import { decodeRedisKey } from '@/utils/key_convert.js'
|
||||||
|
@ -14,6 +13,7 @@ const deleteForm = reactive({
|
||||||
loadingAffected: false,
|
loadingAffected: false,
|
||||||
affectedKeys: [],
|
affectedKeys: [],
|
||||||
async: true,
|
async: true,
|
||||||
|
direct: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const dialogStore = useDialog()
|
const dialogStore = useDialog()
|
||||||
|
@ -68,7 +68,6 @@ const keyLines = computed(() => {
|
||||||
return map(deleteForm.affectedKeys, (k) => decodeRedisKey(k))
|
return map(deleteForm.affectedKeys, (k) => decodeRedisKey(k))
|
||||||
})
|
})
|
||||||
|
|
||||||
const i18n = useI18n()
|
|
||||||
const onConfirmDelete = async () => {
|
const onConfirmDelete = async () => {
|
||||||
try {
|
try {
|
||||||
deleting.value = true
|
deleting.value = true
|
||||||
|
@ -84,6 +83,21 @@ const onConfirmDelete = async () => {
|
||||||
dialogStore.closeDeleteKeyDialog()
|
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 = () => {
|
const onClose = () => {
|
||||||
dialogStore.closeDeleteKeyDialog()
|
dialogStore.closeDeleteKeyDialog()
|
||||||
}
|
}
|
||||||
|
@ -116,9 +130,9 @@ const onClose = () => {
|
||||||
required>
|
required>
|
||||||
<n-input v-model:value="deleteForm.key" placeholder="" @input="resetAffected" />
|
<n-input v-model:value="deleteForm.key" placeholder="" @input="resetAffected" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<!-- <n-checkbox v-model:checked="deleteForm.async">-->
|
<n-checkbox v-model:checked="deleteForm.direct">
|
||||||
<!-- {{ $t('dialogue.key.silent') }}-->
|
{{ $t('dialogue.key.direct_delete') }}
|
||||||
<!-- </n-checkbox>-->
|
</n-checkbox>
|
||||||
<n-card
|
<n-card
|
||||||
v-if="deleteForm.showAffected"
|
v-if="deleteForm.showAffected"
|
||||||
:title="$t('dialogue.key.affected_key') + `(${size(deleteForm.affectedKeys)})`"
|
:title="$t('dialogue.key.affected_key') + `(${size(deleteForm.affectedKeys)})`"
|
||||||
|
@ -139,6 +153,15 @@ const onClose = () => {
|
||||||
<template #action>
|
<template #action>
|
||||||
<div class="flex-item n-dialog__action">
|
<div class="flex-item n-dialog__action">
|
||||||
<n-button :disabled="loading" :focusable="false" @click="onClose">{{ $t('common.cancel') }}</n-button>
|
<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
|
<n-button
|
||||||
v-if="!deleteForm.showAffected"
|
v-if="!deleteForm.showAffected"
|
||||||
:focusable="false"
|
:focusable="false"
|
||||||
|
@ -156,6 +179,7 @@ const onClose = () => {
|
||||||
@click="onConfirmDelete">
|
@click="onConfirmDelete">
|
||||||
{{ $t('dialogue.key.confirm_delete_key', { num: size(deleteForm.affectedKeys) }) }}
|
{{ $t('dialogue.key.confirm_delete_key', { num: size(deleteForm.affectedKeys) }) }}
|
||||||
</n-button>
|
</n-button>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
|
|
|
@ -296,6 +296,8 @@
|
||||||
"affected_key": "Affected Keys",
|
"affected_key": "Affected Keys",
|
||||||
"show_affected_key": "Show Affected Keys",
|
"show_affected_key": "Show Affected Keys",
|
||||||
"confirm_delete_key": "Confirm delete {num} key(s)",
|
"confirm_delete_key": "Confirm delete {num} key(s)",
|
||||||
|
"direct_delete": "Delete match pattern directly",
|
||||||
|
"confirm_delete": "Confirm Delete",
|
||||||
"async_delete": "Async Execution",
|
"async_delete": "Async Execution",
|
||||||
"async_delete_title": "Don't wait for result",
|
"async_delete_title": "Don't wait for result",
|
||||||
"confirm_flush": "I know what I'm doing!",
|
"confirm_flush": "I know what I'm doing!",
|
||||||
|
|
|
@ -296,6 +296,8 @@
|
||||||
"affected_key": "Claves afectadas",
|
"affected_key": "Claves afectadas",
|
||||||
"show_affected_key": "Mostrar claves afectadas",
|
"show_affected_key": "Mostrar claves afectadas",
|
||||||
"confirm_delete_key": "Confirmar eliminar {num} clave(s)",
|
"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": "Ejecución asíncrona",
|
||||||
"async_delete_title": "No esperar el resultado",
|
"async_delete_title": "No esperar el resultado",
|
||||||
"confirm_flush": "¡Sé lo que estoy haciendo!",
|
"confirm_flush": "¡Sé lo que estoy haciendo!",
|
||||||
|
|
|
@ -296,6 +296,8 @@
|
||||||
"affected_key": "Clés affectées",
|
"affected_key": "Clés affectées",
|
||||||
"show_affected_key": "Afficher les clés affectées",
|
"show_affected_key": "Afficher les clés affectées",
|
||||||
"confirm_delete_key": "Confirmer la suppression de {num} clé(s)",
|
"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": "Exécution asynchrone",
|
||||||
"async_delete_title": "Ne pas attendre le résultat",
|
"async_delete_title": "Ne pas attendre le résultat",
|
||||||
"confirm_flush": "Je sais ce que je fais !",
|
"confirm_flush": "Je sais ce que je fais !",
|
||||||
|
|
|
@ -296,6 +296,8 @@
|
||||||
"affected_key": "影響を受けるキー",
|
"affected_key": "影響を受けるキー",
|
||||||
"show_affected_key": "影響を受けるキーを表示",
|
"show_affected_key": "影響を受けるキーを表示",
|
||||||
"confirm_delete_key": "{num}個のキーを削除することを確認",
|
"confirm_delete_key": "{num}個のキーを削除することを確認",
|
||||||
|
"direct_delete": "一致するパターンを直接削除",
|
||||||
|
"confirm_delete": "削除を確認",
|
||||||
"async_delete": "非同期実行",
|
"async_delete": "非同期実行",
|
||||||
"async_delete_title": "結果を待たない",
|
"async_delete_title": "結果を待たない",
|
||||||
"confirm_flush": "自分が実行しようとしている操作を理解しています!",
|
"confirm_flush": "自分が実行しようとしている操作を理解しています!",
|
||||||
|
|
|
@ -296,6 +296,8 @@
|
||||||
"affected_key": "영향받는 키",
|
"affected_key": "영향받는 키",
|
||||||
"show_affected_key": "영향받는 키 표시",
|
"show_affected_key": "영향받는 키 표시",
|
||||||
"confirm_delete_key": "{num}개의 키를 삭제하시겠습니까?",
|
"confirm_delete_key": "{num}개의 키를 삭제하시겠습니까?",
|
||||||
|
"direct_delete": "일치하는 패턴 직접 삭제",
|
||||||
|
"confirm_delete": "삭제 확인",
|
||||||
"async_delete": "비동기 실행",
|
"async_delete": "비동기 실행",
|
||||||
"async_delete_title": "결과를 기다리지 않음",
|
"async_delete_title": "결과를 기다리지 않음",
|
||||||
"confirm_flush": "진행 중인 작업을 알고 있습니다!",
|
"confirm_flush": "진행 중인 작업을 알고 있습니다!",
|
||||||
|
|
|
@ -296,6 +296,8 @@
|
||||||
"affected_key": "Chaves Afetadas",
|
"affected_key": "Chaves Afetadas",
|
||||||
"show_affected_key": "Mostrar Chaves Afetadas",
|
"show_affected_key": "Mostrar Chaves Afetadas",
|
||||||
"confirm_delete_key": "Confirmar Exclusão de {num} Chave(s)",
|
"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": "Execução Assíncrona",
|
||||||
"async_delete_title": "Não esperar pelo resultado da operação",
|
"async_delete_title": "Não esperar pelo resultado da operação",
|
||||||
"confirm_flush": "Eu sei o que estou fazendo!",
|
"confirm_flush": "Eu sei o que estou fazendo!",
|
||||||
|
|
|
@ -296,6 +296,8 @@
|
||||||
"affected_key": "Затронутые ключи",
|
"affected_key": "Затронутые ключи",
|
||||||
"show_affected_key": "Показать затронутые ключи",
|
"show_affected_key": "Показать затронутые ключи",
|
||||||
"confirm_delete_key": "Подтвердить удаление {num} ключ(ей/ей)",
|
"confirm_delete_key": "Подтвердить удаление {num} ключ(ей/ей)",
|
||||||
|
"direct_delete": "Удалить совпадающий шаблон напрямую",
|
||||||
|
"confirm_delete": "Подтвердить удаление",
|
||||||
"async_delete": "Асинхронное выполнение",
|
"async_delete": "Асинхронное выполнение",
|
||||||
"async_delete_title": "Не ждать результата",
|
"async_delete_title": "Не ждать результата",
|
||||||
"confirm_flush": "Я знаю, что делаю!",
|
"confirm_flush": "Я знаю, что делаю!",
|
||||||
|
|
|
@ -296,6 +296,8 @@
|
||||||
"affected_key": "受影响的键名",
|
"affected_key": "受影响的键名",
|
||||||
"show_affected_key": "查看受影响的键名",
|
"show_affected_key": "查看受影响的键名",
|
||||||
"confirm_delete_key": "确认删除{num}个键",
|
"confirm_delete_key": "确认删除{num}个键",
|
||||||
|
"direct_delete": "直接匹配删除",
|
||||||
|
"confirm_delete": "确认删除",
|
||||||
"async_delete": "异步执行",
|
"async_delete": "异步执行",
|
||||||
"async_delete_title": "不等待操作结果",
|
"async_delete_title": "不等待操作结果",
|
||||||
"confirm_flush": "我知道我正在执行的操作!",
|
"confirm_flush": "我知道我正在执行的操作!",
|
||||||
|
|
|
@ -296,6 +296,8 @@
|
||||||
"affected_key": "受影響的鍵名",
|
"affected_key": "受影響的鍵名",
|
||||||
"show_affected_key": "檢視受影響的鍵名",
|
"show_affected_key": "檢視受影響的鍵名",
|
||||||
"confirm_delete_key": "確認刪除{num}個鍵",
|
"confirm_delete_key": "確認刪除{num}個鍵",
|
||||||
|
"direct_delete": "直接匹配刪除",
|
||||||
|
"confirm_delete": "確認刪除",
|
||||||
"async_delete": "異步執行",
|
"async_delete": "異步執行",
|
||||||
"async_delete_title": "不等待操作結果",
|
"async_delete_title": "不等待操作結果",
|
||||||
"confirm_flush": "我知道我正在執行的操作!",
|
"confirm_flush": "我知道我正在執行的操作!",
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
ConvertValue,
|
ConvertValue,
|
||||||
DeleteKey,
|
DeleteKey,
|
||||||
DeleteKeys,
|
DeleteKeys,
|
||||||
|
DeleteKeysByPattern,
|
||||||
ExportKey,
|
ExportKey,
|
||||||
FlushDB,
|
FlushDB,
|
||||||
GetClientList,
|
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
|
* export multiple keys
|
||||||
* @param {string} server
|
* @param {string} server
|
||||||
|
|
Loading…
Reference in New Issue