Süvitsi minek JavaScripti täiustatud ressursside haldusesse. Õppige, kuidas kombineerida tulevast 'using' deklaratsiooni ressursside kogumisega puhtamate, turvalisemate ja suure jõudlusega rakenduste jaoks.
Ressursside haldamise valdamine: JavaScripti 'using' avaldus ja ressursside kogumise strateegia
Suure jõudlusega serveripoolse JavaScripti maailmas, eriti sellistes keskkondades nagu Node.js ja Deno, ei ole tõhus ressursside haldamine lihtsalt hea tava; see on skaleeritavate, vastupidavate ja kulutõhusate rakenduste ehitamise kriitiline komponent. Arendajad maadlevad sageli piiratud, kulukate ressursside, nagu andmebaasiühendused, failikäepidemed, võrgupesad või töötlusniidid, haldamisega. Nende ressursside valesti käitlemine võib põhjustada probleeme: mälulekked, ühenduste ammendumine, süsteemi ebastabiilsus ja jõudluse halvenemine.
Traditsiooniliselt on arendajad ressursside puhastamise tagamiseks tuginenud try...catch...finally
blokile. Kuigi see muster on tõhus, võib see olla mahukas ja vigadele vastuvõtlik. Teisest küljest kasutame jõudluse tagamiseks ressursside kogumist, et vältida pidevat nende varade loomise ja hävitamise kulu. Kuid kuidas me saame elegantselt ühendada garanteeritud puhastuse turvalisuse ressursside taaskasutuse tõhususega? Vastus peitub kahe kontseptsiooni vahelises võimsas sünergias: muster, mis meenutab teistes keeltes leiduvat using
avaldust ja ressursside kogumise tõestatud strateegiat.
See põhjalik juhend uurib, kuidas arhitektuurida kaasaegses JavaScriptis tugevat ressursside haldamise strateegiat. Me süveneme TC39 eelseisvasse ettepanekusse selgesõnalise ressursside haldamise kohta, mis tutvustab märksõnu using
ja await using
, ning demonstreerime, kuidas integreerida see puhas, deklaratiivne süntaks kohandatud ressursside kogumiga, et ehitada rakendusi, mis on nii võimsad kui ka hõlpsasti hooldatavad.
Põhiprobleemi mõistmine: ressursside haldamine JavaScriptis
Enne lahenduse ehitamist on oluline mõista probleemi nüansse. Mis täpselt on "ressursid" selles kontekstis ja miks on nende haldamine erinev lihtsa mälu haldamisest?
Mis on "ressursid"?
Selles arutelus tähendab "ressurss" mis tahes objekti, mis hoiab ühendust välise süsteemiga või nõuab selgesõnalist "sulgemise" või "ühenduse katkestamise" toimingut. Need on sageli arvult piiratud ja arvutuslikult kulukad luua. Levinud näited hõlmavad järgmist:
- Andmebaasiühendused: Andmebaasiga ühenduse loomine hõlmab võrgu käepigistusi, autentimist ja seansi seadistamist, mis kõik kulutavad aega ja CPU tsükleid.
- Failikäepidemed: Operatsioonisüsteemid piiravad failide arvu, mis protsessil saab korraga avatud olla. Lekkinud failikäepidemed võivad takistada rakendusel uute failide avamist.
- Võrgupesad: Ühendused väliste API-de, sõnumijärjekordade või muude mikroteenustega.
- Töötlusniidid või alamprotsessid: Rasked arvutusressursid, mida tuleks hallata kogumis, et vältida protsessi loomise kulu.
Miks prügikoristajast ei piisa
Süsteemiprogrammeerimisega uute arendajate seas on levinud väärarusaam, et JavaScripti prügikoristaja (GC) saab kõigega hakkama. GC on suurepärane mälus hõivatud ruumi tagastamisel objektide poolt, mis pole enam kättesaadavad. Kuid see ei halda väliseid ressursse deterministlikult.
Kui andmebaasiühendust esitavale objektile enam ei viidata, vabastab GC lõpuks selle mälu. Kuid see ei anna garantiid millal see juhtub ega tea, et ta peab kutsuma .close()
meetodi, et vabastada aluseks olev võrgupesa tagasi operatsioonisüsteemile või ühenduspesa tagasi andmebaasiserverile. Ressursside puhastamisel GC-le tuginemine viib mitte-deterministliku käitumise ja ressursside lekkimiseni, kus teie rakendus hoiab väärtuslikke ühendusi palju kauem kui vaja.
'Using' avalduse emuleerimine: tee deterministliku puhastamiseni
Keeled nagu C# (koos using
) ja Python (koos with
) pakuvad elegantset süntaksit, et tagada ressursi puhastusloogika täitmine kohe, kui see ulatusest välja läheb. Seda kontseptsiooni nimetatakse deterministlikuks ressursside haldamiseks. JavaScript on lävel, et saada natiivne lahendus, kuid vaatame kõigepealt traditsioonilist meetodit.
Klassikaline lähenemine: try...finally
blokk
Ressursside haldamise tööloom JavaScriptis on alati olnud try...finally
blokk. Kood finally
blokis täidetakse garanteeritult, olenemata sellest, kas kood try
blokis lõpetab edukalt, viskab vea või tagastab väärtuse.
Siin on tüüpiline näide andmebaasiühenduse haldamiseks:
async function getUserById(id) {
let connection;
try {
connection = await getDatabaseConnection(); // Hankige ressurss
const result = await connection.query('SELECT * FROM users WHERE id = ?', [id]);
return result[0];
} catch (error) {
console.error("Päringu ajal tekkis viga:", error);
throw error; // Viskage viga uuesti
} finally {
if (connection) {
await connection.close(); // Vabastage ressurss ALATI
}
}
}
See muster töötab, kuid sellel on puudusi:
- Mahukus: Ressursi hankimise ja vabastamise boilerplaat kood varjutab sageli tegelikku äriloogikat.
- Vigadele vastuvõtlik: On lihtne unustada
if (connection)
kontrolli või käidelda vigufinally
bloki sees valesti. - Pesastuskomplekssus: Mitme ressursi haldamine viib sügavalt pesastatud
try...finally
blokkideni, mida sageli nimetatakse "surma püramiidiks".
Kaasaegne lahendus: TC39 'using' deklaratsiooni ettepanek
Nende puuduste kõrvaldamiseks on TC39 komitee (mis standardiseerib JavaScripti) edendanud Selgesõnalise ressursside haldamise ettepanekut. See ettepanek, mis on praegu 3. etapis (mis tähendab, et see on kandidaat ECMAScripti standardisse lisamiseks), tutvustab kahte uut märksõna – using
ja await using
– ning mehhanismi objektidele oma puhastusloogika määratlemiseks.
Selle ettepaneku tuumaks on "kõrvaldatava" ressursi kontseptsioon. Objekt muutub kõrvaldatavaks, rakendades konkreetse meetodi tuntud sümboli võtme all:
[Symbol.dispose]()
: Sünkroonse puhastusloogika jaoks.[Symbol.asyncDispose]()
: Asünkroonse puhastusloogika jaoks (nt võrguühenduse sulgemine).
Kui deklareerite muutuja using
või await using
abil, kutsub JavaScript automaatselt välja vastava kõrvaldamismeetodi, kui muutuja ulatusest välja läheb, kas bloki lõpus või kui visatakse viga.
Loome kõrvaldatava andmebaasiühenduse ümbrise:
class ManagedDatabaseConnection {
constructor(connection) {
this.connection = connection;
this.isDisposed = false;
}
// Paljastage andmebaasi meetodid nagu päring
async query(sql, params) {
if (this.isDisposed) {
throw new Error("Ühendus on juba kõrvaldatud.");
}
return this.connection.query(sql, params);
}
async [Symbol.asyncDispose]() {
if (!this.isDisposed) {
console.log('Ühenduse kõrvaldamine...');
await this.connection.close();
this.isDisposed = true;
console.log('Ühendus kõrvaldatud.');
}
}
}
// Kuidas seda kasutada:
async function getUserByIdWithUsing(id) {
// Eeldab, et getRawConnection tagastab ühenduse objekti lubaduse
const rawConnection = await getRawConnection();
await using connection = new ManagedDatabaseConnection(rawConnection);
const result = await connection.query('SELECT * FROM users WHERE id = ?', [id]);
return result[0];
// Pole vaja lõpuks blokki! `connection[Symbol.asyncDispose]` kutsutakse siin automaatselt.
}
Vaadake erinevust! Koodi eesmärk on kristallselge. Äriloogika on ees ja keskel ning ressursside haldamine toimub automaatselt ja usaldusväärselt kulisside taga. See on monumentaalne parandus koodi selguses ja turvalisuses.
Kogumise jõud: miks taastada, kui saate taaskasutada?
using
muster lahendab *garanteeritud puhastuse* probleemi. Kuid suure liiklusega rakenduses on andmebaasiühenduse loomine ja hävitamine iga üksiku taotluse jaoks uskumatult ebaefektiivne. Siin tuleb mängu ressursside kogumine.
Mis on ressursside kogum?
Ressursside kogum on disainimuster, mis säilitab valmis kasutamiseks olevate ressursside vahemälu. Mõelge sellele nagu raamatukogu raamatukogu. Selle asemel, et osta uus raamat iga kord, kui soovite ühte lugeda, ja see siis ära visata, laenate selle raamatukogust, loete selle läbi ja tagastate selle kellelegi teisele kasutamiseks. See on palju tõhusam.
Tüüpiline ressursside kogumi rakendamine hõlmab järgmist:
- Initsialiseerimine: Kogum luuakse minimaalse ja maksimaalse ressursside arvuga. See võib eelnevalt täita ennast minimaalse arvu ressurssidega.
- Hankimine: Klient taotleb kogumist ressurssi. Kui ressurss on saadaval, laenab kogum selle välja. Kui ei, võib klient oodata, kuni see saab kättesaadavaks, või võib kogum luua uue, kui see on alla selle maksimaalse piiri.
- Vabastamine: Pärast kliendi lõpetamist tagastab ta ressursi kogumile selle asemel, et seda hävitada. Kogum saab seejärel laenata sama ressursi teisele kliendile.
- Hävitamine: Kui rakendus sulgub, sulgeb kogum graatsiliselt kõik hallatavad ressursid.
Kogumise eelised
- Vähendatud latentsus: Ressursi hankimine kogumist on oluliselt kiirem kui uue loomine nullist.
- Madalam kulu: Vähendab CPU ja mälu survet nii teie rakendusserveris kui ka välises süsteemis (nt andmebaasis).
- Ühenduse drossel: Seades maksimaalse kogumi suuruse, takistate oma rakendusel andmebaasi või välisteenuse ülekoormamist liiga paljude samaaegsete ühendustega.
Suur süntees: `using` kombineerimine ressursside kogumiga
Nüüd jõuame oma strateegia tuumani. Meil on fantastiline muster garanteeritud puhastamiseks (using
) ja tõestatud strateegia jõudluse tagamiseks (kogumine). Kuidas me need sujuvaks, tugevaks lahenduseks ühendame?
Eesmärk on hankida kogumist ressurss ja tagada, et see vabastatakse tagasi kogumisse, kui oleme lõpetanud, isegi vigade korral. Saame selle saavutada, luues ümbriseobjekti, mis rakendab kõrvaldamise protokolli, kuid mille kõrvaldamismeetod kutsub välja pool.release()
, mitte resource.close()
.
See on maagiline lüli: `dispose` toiming muutub "tagasi kogumisse" asemel "hävitada".
Samm-sammuline rakendamine
Ehitame selle toimimiseks üldise ressursside kogumi ja vajalikud ümbrised.
1. samm: lihtsa, üldise ressursside kogumi ehitamine
Siin on asünkroonse ressursside kogumi kontseptuaalne rakendamine. Tootmisvalmis versioonil oleks rohkem funktsioone, nagu ajalõpud, jõudeoleku ressursside väljaviskamine ja uuesti proovimise loogika, kuid see illustreerib peamist mehaanikat.
class ResourcePool {
constructor({ create, destroy, min, max }) {
this.factory = { create, destroy };
this.config = { min, max };
this.pool = []; // Salvestab saadaolevad ressursid
this.active = []; // Salvestab praegu kasutuses olevad ressursid
this.waitQueue = []; // Salvestab lubadused klientidele, kes ootavad ressurssi
// Initsialiseerige minimaalsed ressursid
for (let i = 0; i < this.config.min; i++) {
this._createResource().then(resource => this.pool.push(resource));
}
}
async _createResource() {
const resource = await this.factory.create();
return resource;
}
async acquire() {
// Kui kogumis on ressurss saadaval, kasutage seda
if (this.pool.length > 0) {
const resource = this.pool.pop();
this.active.push(resource);
return resource;
}
// Kui me oleme alla maksimaalse piiri, looge uus
if (this.active.length < this.config.max) {
const resource = await this._createResource();
this.active.push(resource);
return resource;
}
// Muidu oodake, kuni ressurss vabastatakse
return new Promise((resolve, reject) => {
// Tõeline rakendus omaks siin ajalõpu
this.waitQueue.push({ resolve, reject });
});
}
release(resource) {
// Kontrollige, kas keegi ootab
if (this.waitQueue.length > 0) {
const waiter = this.waitQueue.shift();
// Andke see ressurss otse ootavale kliendile
waiter.resolve(resource);
} else {
// Muidu tagastage see kogumile
this.pool.push(resource);
}
// Eemaldage aktiivsest loendist
this.active = this.active.filter(r => r !== resource);
}
async close() {
// Sulgege kõik ressursid kogumis ja need, mis on aktiivsed
const allResources = [...this.pool, ...this.active];
this.pool = [];
this.active = [];
await Promise.all(allResources.map(r => this.factory.destroy(r)));
}
}
2. samm: 'PooledResource' ümbrise loomine
See on oluline osa, mis ühendab kogumi using
süntaksiga. See hoiab ressurssi ja viidet kogumile, kust see tuli. Selle kõrvaldamismeetod kutsub välja pool.release()
.
class PooledResource {
constructor(resource, pool) {
this.resource = resource;
this.pool = pool;
this._isReleased = false;
}
// See meetod vabastab ressursi tagasi kogumisse
[Symbol.dispose]() {
if (this._isReleased) {
return;
}
this.pool.release(this.resource);
this._isReleased = true;
console.log('Ressurss vabastati tagasi kogumisse.');
}
}
// Saame luua ka asünkroonse versiooni
class AsyncPooledResource {
constructor(resource, pool) {
this.resource = resource;
this.pool = pool;
this._isReleased = false;
}
// Kõrvaldamismeetod võib olla asünkroonne, kui vabastamine on asünkroonne toiming
async [Symbol.asyncDispose]() {
if (this._isReleased) {
return;
}
// Meie lihtsas kogumis on vabastamine sünkroonne, kuid me näitame mustrit
await Promise.resolve(this.pool.release(this.resource));
this._isReleased = true;
console.log('Asünkroonne ressurss vabastati tagasi kogumisse.');
}
}
3. samm: kõik kokku ühtses halduris
API veelgi puhtamaks muutmiseks saame luua haldurklassi, mis kapseldab kogumi ja müüb kõrvaldatavaid ümbriseid.
class ResourceManager {
constructor(poolConfig) {
this.pool = new ResourcePool(poolConfig);
}
async getResource() {
const resource = await this.pool.acquire();
// Kasutage asünkroonset ümbrist, kui teie ressursi puhastamine võib olla asünkroonne
return new AsyncPooledResource(resource, this.pool);
}
async shutdown() {
await this.pool.close();
}
}
// --- Näide kasutamisest ---
// 1. Määratlege, kuidas luua ja hävitada meie näivressursse
let resourceIdCounter = 0;
const poolConfig = {
create: async () => {
resourceIdCounter++;
console.log(`Ressursi #${resourceIdCounter} loomine...`);
return { id: resourceIdCounter, data: `data for ${resourceIdCounter}` };
},
destroy: async (resource) => {
console.log(`Ressursi #${resource.id} hävitamine...`);
},
min: 1,
max: 3
};
// 2. Looge haldur
const manager = new ResourceManager(poolConfig);
// 3. Kasutage mustrit rakenduse funktsioonis
async function processRequest(requestId) {
console.log(`Taotlus ${requestId}: Proovitakse hankida ressurssi...`);
try {
await using client = await manager.getResource();
console.log(`Taotlus ${requestId}: Hankis ressursi #${client.resource.id}. Töötamine...`);
// Simuleerige mõnda tööd
await new Promise(resolve => setTimeout(resolve, 500));
// Simuleerige juhuslikku riket
if (Math.random() > 0.7) {
throw new Error(`Taotlus ${requestId}: Simuleeritud juhuslik rike!`);
}
console.log(`Taotlus ${requestId}: Töö on valmis.`);
} catch (error) {
console.error(error.message);
}
// `client` vabastatakse siin automaatselt tagasi kogumisse, nii edu- kui ka rikkekorral.
}
// --- Simuleerige samaaegseid taotlusi ---
async function main() {
const requests = [
processRequest(1),
processRequest(2),
processRequest(3),
processRequest(4),
processRequest(5)
];
await Promise.all(requests);
console.log('\nKõik taotlused on lõpetatud. Kogumi sulgemine...');
await manager.shutdown();
}
main();
Kui käivitate selle koodi (kasutades kaasaegset TypeScripti või Babeli seadistust, mis toetab ettepanekut), näete, et ressursse luuakse kuni maksimaalse piirini, taaskasutavad erinevad taotlused ja vabastatakse alati tagasi kogumisse. `processRequest` funktsioon on puhas, keskendunud oma ülesandele ja täielikult vabastatud ressursside puhastamise kohustusest.
Täiustatud kaalutlused ja parimad tavad ülemaailmsele publikule
Kuigi meie näide pakub tugeva aluse, nõuavad reaalsed, ülemaailmselt levitatud rakendused nüansirikkamaid kaalutlusi.
Samaaegsus ja kogumi suuruse häälestamine
min
ja max
kogumi suurused on kriitilised häälestusparameetrid. Ühtset maagilist numbrit pole; optimaalne suurus sõltub teie rakenduse koormusest, ressursi loomise latentsusest ja taustateenuse piirangutest (nt teie andmebaasi maksimaalsed ühendused).
- Liiga väike: Teie rakendusniidid veedavad liiga palju aega oodates, millal ressurss muutub kättesaadavaks, luues jõudluse kitsaskoha. Seda nimetatakse kogumi konkurentsiks.
- Liiga suur: Te kulutate nii oma rakendusserveris kui ka taustal liigset mälu ja CPU-d. Ülemaailmselt levitatud meeskonna jaoks on oluline dokumenteerida nende numbrite põhjendused, võib-olla koormustesti tulemuste põhjal, et erinevate piirkondade insenerid mõistaksid piiranguid.
Alustage konservatiivsete numbritega, mis põhinevad eeldataval koormusel, ja kasutage rakenduse jõudluse jälgimise (APM) tööriistu, et mõõta kogumi ooteaegu ja kasutusmäära. Reguleerige vastavalt.
Ajalõpp ja veatöötlus
Mis juhtub, kui kogum on oma maksimaalses suuruses ja kõik ressursid on kasutusel? Meie lihtne kogum paneks uued taotlused igavesti ootama. Tootmisklassi kogumil peab olema hankimise ajalõpp. Kui ressurssi ei saa hankida teatud aja jooksul (nt 30 sekundit), peaks acquire
kõne ebaõnnestuma ajalõpu veaga. See takistab taotluste lõputult rippumist ja võimaldab teil ebaõnnestuda graatsiliselt, võib-olla tagastades kliendile 503 Teenus pole saadaval
oleku.
Lisaks peaks kogum käitlema aegunud või katkiseid ressursse. Sellel peaks olema valideerimismehhanism (nt testOnBorrow
funktsioon), mis saab enne selle välja laenamist kontrollida, kas ressurss on endiselt kehtiv. Kui see on katki, peaks kogum selle hävitama ja looma selle asendamiseks uue.
Integreerimine raamistike ja arhitektuuridega
See ressursside haldamise muster ei ole isoleeritud tehnika; see on suurema arhitektuuri alustala.
- Sõltuvuse süstimine (DI): Meie loodud
ResourceManager
on ideaalne kandidaat singleton teenuseks DI konteineris. Selle asemel, et luua uus haldur igal pool, süstite sama eksemplari kogu oma rakenduses, tagades, et kõik jagavad sama kogumit. - Mikroteenused: Mikroteenuste arhitektuuris haldaks iga teenuse eksemplar oma ühenduste kogumit andmebaaside või muude teenustega. See isoleerib rikkeid ja võimaldab igat teenust iseseisvalt häälestada.
- Serverless (FaaS): Sellistel platvormidel nagu AWS Lambda või Google Cloud Functions on ühenduste haldamine tuntud kui keeruline funktsioonide olekutu ja efemeerse olemuse tõttu. Globaalne ühendusehaldur, mis püsib funktsioonide käivitamiste vahel (kasutades globaalset ulatust väljaspool käitlejat) koos selle
using
/kogumi mustriga käitleja sees, on standardne parim tava, et vältida andmebaasi ülekoormamist.
Järeldus: puhtama, turvalisema ja suurema jõudlusega JavaScripti kirjutamine
Tõhus ressursside haldamine on professionaalse tarkvaratehnika tunnus. Liikudes kaugemale manuaalsest ja sageli kohmakast try...finally
mustrist, saame kirjutada koodi, mis on vastupidavam, suurema jõudlusega ja tunduvalt loetavam.
Kordame võimsat strateegiat, mida oleme uurinud:
- Probleem: Kulukate, piiratud väliste ressursside, nagu andmebaasiühendused, haldamine on keeruline. Prügikoristajale tuginemine ei ole deterministliku puhastuse jaoks võimalus ning manuaalne haldamine
try...finally
abil on mahukas ja vigadele vastuvõtlik. - Turvavõrk: TC39 selgesõnalise ressursside haldamise ettepaneku osaks olev
using
jaawait using
süntaks pakub deklaratiivse ja peaaegu lollikindla viisi, et tagada ressursi puhastusloogika alati täidetakse. - Jõudlusmootor: Ressursside kogumine on ajaproovitud muster, mis väldib ressursside loomise ja hävitamise kõrgeid kulusid, taaskasutades olemasolevaid ressursse.
- Süntees: Luues ümbrise, mis rakendab kõrvaldamise protokolli (
[Symbol.dispose]
või[Symbol.asyncDispose]
) ja mille puhastusloogika on ressursi vabastamine tagasi oma kogumisse, saavutame mõlema maailma parima. Saame kogumise jõudluse koosusing
avalduse turvalisuse ja elegantsusega.
Kuna JavaScript areneb jätkuvalt juhtivaks keeleks suure jõudlusega, suuremahuliste süsteemide ehitamiseks, ei ole selliste mustrite kasutuselevõtt enam vabatahtlik. See on see, kuidas me ehitame järgmise põlvkonna tugevaid, skaleeritavaid ja hooldatavaid rakendusi ülemaailmsele publikule. Alustage täna oma projektides katsetamist using
deklaratsiooniga TypeScripti või Babeli kaudu ja arhitektuurige oma ressursside haldamist selguse ja enesekindlusega.