13-06-2012: artikel oprettet
14-06-2012: tilføjelse af overvejelse ifm. webgardens og webfarms.
03-07-2012: ændring i sektion vedr. udskiftning af caching-container.
Introduktion
Nu er caching, såvidt jeg ved, ikke understøttet pr. default i ASP 3.0, så derfor må man enten have fat i en 3. partskomponent, eller flikke sin egen løsning sammen. Jeg har gjort det sidste :-)
Løsningen baserer sig på kendte teknikker til at gemme oplysninger på tværs af forespørgsler. Jeg har blot forsøgt at pakke tingene ind, så det ikke er så omstændigt at benytte caching. Et groft bud på, hvordan man kan cache ting i ASP 3.0 kunne være:
session("opdateret") = now
Denne kan aflæses således:
opdateret = session("opdateret")
Denne metode har dog nogle ulemper. Dels binder man sig til session som caching-container alle steder i koden, hvor cachen benyttes, dels er der ikke nogen styring af, hvor gammel indholdet i cachen må være, før det udløber.
Indpakning af afhængighed
Hvis man skal bløde op på den første ulempe, kunne cachingmekanismen pakkes ind i en funktion, således referencen til session kun findes to steder (get- og set-funktionerne) og dermed relativt let kan udskiftes, hvis det skulle blive nødvendigt.
function GetCache(key)
GetCache = session(key)
end function
function SetCache(key, value)
session(key) = value
end function
GetCache = session(key)
end function
function SetCache(key, value)
session(key) = value
end function
Nu kan man i sin kode sætte og læse cache-værdier således:
SetCache "opdateret", now
Ligeledes kan de hentes således:
opdateret = GetCache("opdateret")
Nu er dette jo ikke imponerende, selvom det kan spare noget arbejde, hvis man vil udskifte caching-containeren, som illustreret senere.
Forældelse af cache
Den anden ulempe med de hidtil demonstrerede løsninger er at cachen ikke udløber og data derfor kan blive noget forældet med tiden. Dette kan håndteres ved at gemme lidt metadata om den cachede værdi. Dette kunne f.eks. være tidspunktet for hvornår værdien blev gemt.
function SetCache(key, value)
session(key) = value
session(key & "$metadata") = now
end function
session(key) = value
session(key & "$metadata") = now
end function
Nu kan vi ifm. aflæsningen vurdere om værdien er forældet.
function GetCache(key)
dim setTime
setTime = session(key & "$metadata") & ""
if setTime = "" then
GetCache = Empty
exit function
end if
if dateadd("s", 300, setTime) > now then
' værdien er stadig frisk (nok)
GetCache = session(key)
else
' værdien er forældet
GetCache = Empty
end if
end function
dim setTime
setTime = session(key & "$metadata") & ""
if setTime = "" then
GetCache = Empty
exit function
end if
if dateadd("s", 300, setTime) > now then
' værdien er stadig frisk (nok)
GetCache = session(key)
else
' værdien er forældet
GetCache = Empty
end if
end function
Ovenstående cacher alle værdier i 300 sekunder (5 minutter) og det kan jo være fint nok, men det ville måske være rart selv at have lidt kontrol over hvor lang tid en værdi tager om at blive forældet. Derfor udvides funktionerne til også at modtage information om forældelsesfrist.
function SetCache(key, value, expireSeconds)
session(key) = value
session(key & "$metadata") = now & ";" & expireSeconds
end function
session(key) = value
session(key & "$metadata") = now & ";" & expireSeconds
end function
Nu kan vi ifm. aflæsningen vurdere om værdien er forældet.
function GetCache(key)
dim metadata, setTime, expires
metadata = session(key & "$metadata") & ""
if metadata = "" then
GetCache = Empty
exit function
end if
arrMetadata = split(metadata, ";")
setTime = arrMetadata(0)
expires = arrMetadata(1)
if dateadd("s", expires, setTime) > now then
' værdien er stadig frisk (nok)
GetCache = session(key)
else
' værdien er forældet
GetCache = Empty
end if
end function
dim metadata, setTime, expires
metadata = session(key & "$metadata") & ""
if metadata = "" then
GetCache = Empty
exit function
end if
arrMetadata = split(metadata, ";")
setTime = arrMetadata(0)
expires = arrMetadata(1)
if dateadd("s", expires, setTime) > now then
' værdien er stadig frisk (nok)
GetCache = session(key)
else
' værdien er forældet
GetCache = Empty
end if
end function
Nu kan cachen sættes således:
SetCache "opdateret", now, 1000
Når den hentes skal der så tages højde for om cachen er frisk eller ej. Hvis den ikke er frisk returneres Empty og dette kan bruges som kriterium for at genopfriske cachen.
opdateret = GetCache("opdateret")
if isEmpty(opdateret) then
opdateret = now
SetCache "opdateret", opdateret, 1000
end if
Response.Write opdateret
if isEmpty(opdateret) then
opdateret = now
SetCache "opdateret", opdateret, 1000
end if
Response.Write opdateret
Udløb efter relevans
Fint nok! Nu er der mulighed for at angive hvor lang tid en værdi er relevant, men nogle gange kunne det være rart, at de værdier som bliver brugt ofte ikke udløber, men at de de ikke bliver brugt gør. Dette omtales ofte på engelsk som "sliding expiration", det jeg har valgt at kalde "udløb efter relevans".
Dette kan f.eks. håndteres ved at angive, om en værdi kun skal udløbe, hvis den ikke bliver brugt indenfor den specificerede udløbsperiode. Cache-funktionerne tilpasses således:
function SetCache(key, value, expireSeconds, sliding)
dim sval
if sliding then
sval = "S"
else
sval = "A"
end if
session(key) = value
SetCacheMetadata key, expireSeconds, sval
end function
dim sval
if sliding then
sval = "S"
else
sval = "A"
end if
session(key) = value
SetCacheMetadata key, expireSeconds, sval
end function
Aflæsningen rettes nu til også at opdatere tidspunktet for værdien hver gang den aflæses, hvis der er valgt "udløb efter relevans".
function GetCache(key)
dim metadata, setTime, expires, sliding
metadata = session(key & "$metadata") & ""
if metadata = "" then
GetCache = Empty
exit function
end if
arrMetadata = split(metadata, ";")
setTime = arrMetadata(0)
expires = arrMetadata(1)
sliding = arrMetadata(2)
if dateadd("s", expires, setTime) > now then
' værdien er stadig frisk (nok)
GetCache = session(key)
if sliding = "S" then
SetCacheMetadata key, expires, sliding
end if
else
' værdien er forældet
GetCache = Empty
end if
end function
sub SetCacheMetadata(key, expires, sliding)
session(key & "$metadata") = "" & _
now & ";" & _
expires & ";" & _
sliding
end sub
dim metadata, setTime, expires, sliding
metadata = session(key & "$metadata") & ""
if metadata = "" then
GetCache = Empty
exit function
end if
arrMetadata = split(metadata, ";")
setTime = arrMetadata(0)
expires = arrMetadata(1)
sliding = arrMetadata(2)
if dateadd("s", expires, setTime) > now then
' værdien er stadig frisk (nok)
GetCache = session(key)
if sliding = "S" then
SetCacheMetadata key, expires, sliding
end if
else
' værdien er forældet
GetCache = Empty
end if
end function
sub SetCacheMetadata(key, expires, sliding)
session(key & "$metadata") = "" & _
now & ";" & _
expires & ";" & _
sliding
end sub
Da jeg vil undgå at indføre for meget redundans i min kode, har jeg indført en lille "service"-funktion (SetCacheMetadata) til min caching-løsning, nemlig en funktion til at sætte metadata, da de nu både skal sættes i SetCache og GetCache.
Udskiftning af cache-container
Nu bliver det jo spændende, for vi har lige fundet den fedeste komponent til caching, men vi har kald til caching spredt ud over 1 mia asp-filer. Så er det jo fedt at vi pakket caching-mekanismen ind i 2-3 funktioner, som bare kan ændres og så er vi kørende. Nu HAR jeg ikke lige en fed caching-komponent, men vi kan jo prøve at ændre vores caching fra at være lokal pr. bruger til at være global for alle brugere. Til dette kan vi benytte Application-objektet i stedet for Session. Samtidig vil jeg lige sørge for at udskiftning af caching-container kun skal gøre ét sted i stedet for 4-5 steder.
dim cache
set cache = application
function SetCache(key, value, expireSeconds, sliding)
dim sval
if sliding then
sval = "S"
else
sval = "A"
end if
cache(key) = value
SetCacheMetadata key, expireSeconds, sval
end function
function GetCache(key)
dim metadata, setTime, expires, sliding
metadata = cache(key & "$metadata") & ""
if metadata = "" then
GetCache = Empty
exit function
end if
arrMetadata = split(metadata, ";")
setTime = arrMetadata(0)
expires = arrMetadata(1)
sliding = arrMetadata(2)
if dateadd("s", expires, setTime) > now then
' værdien er stadig frisk (nok)
GetCache = cache(key)
if sliding = "S" then
SetCacheMetadata key, expires, sliding
end if
else
' værdien er forældet
GetCache = Empty
end if
end function
sub SetCacheMetadata(key, expires, sliding)
cache(key & "$metadata") = "" & _
now & ";" & _
expires & ";" & _
sliding
end sub
set cache = application
function SetCache(key, value, expireSeconds, sliding)
dim sval
if sliding then
sval = "S"
else
sval = "A"
end if
cache(key) = value
SetCacheMetadata key, expireSeconds, sval
end function
function GetCache(key)
dim metadata, setTime, expires, sliding
metadata = cache(key & "$metadata") & ""
if metadata = "" then
GetCache = Empty
exit function
end if
arrMetadata = split(metadata, ";")
setTime = arrMetadata(0)
expires = arrMetadata(1)
sliding = arrMetadata(2)
if dateadd("s", expires, setTime) > now then
' værdien er stadig frisk (nok)
GetCache = cache(key)
if sliding = "S" then
SetCacheMetadata key, expires, sliding
end if
else
' værdien er forældet
GetCache = Empty
end if
end function
sub SetCacheMetadata(key, expires, sliding)
cache(key & "$metadata") = "" & _
now & ";" & _
expires & ";" & _
sliding
end sub
Det var jo ikke så galt! Nu kan vi desuden bare skifte caching-container ved at ændre referencen i cache.
Håndtering af objekter
Normalt vil jeg ikke anbefale at denne cache benyttes alt for intensivt til objekter, men det kan til tider være relevant at gemme et objekt i en periode (f.eks. en XML-fil med sprogtekster). Dette kan vores caching-mekaniske pt. ikke håndtere, men vi kan tilpasse den lidt, så den kan.
function SetCache(key, value, expireSeconds, sliding)
dim sval
if sliding then
sval = "S"
else
sval = "A"
end if
if isObject(value) then
set cache(key) = value
else
cache(key) = value
end if
SetCacheMetadata key, expireSeconds, sval
end function
function GetCache(key)
dim metadata, arrMetadata, setTime, expires, sliding
metadata = cache(key & "$metadata") & ""
if metadata = "" then
GetCache = Empty
exit function
end if
arrMetadata = split(metadata, ";")
setTime = arrMetadata(0)
expires = arrMetadata(1)
sliding = arrMetadata(2)
if dateadd("s", expires, setTime) > now then
' værdien er stadig frisk (nok)
if isObject(cache(key)) then
set GetCache = cache(key)
else
GetCache = cache(key)
end if
if sliding = "S" then
SetCacheMetadata key, expires, sliding
end if
else
' værdien er forældet
GetCache = Empty
end if
end function
sub SetCacheMetadata(key, expires, sliding)
cache(key & "$metadata") = "" & _
now & ";" & _
expires & ";" & _
sliding
end sub
dim sval
if sliding then
sval = "S"
else
sval = "A"
end if
if isObject(value) then
set cache(key) = value
else
cache(key) = value
end if
SetCacheMetadata key, expireSeconds, sval
end function
function GetCache(key)
dim metadata, arrMetadata, setTime, expires, sliding
metadata = cache(key & "$metadata") & ""
if metadata = "" then
GetCache = Empty
exit function
end if
arrMetadata = split(metadata, ";")
setTime = arrMetadata(0)
expires = arrMetadata(1)
sliding = arrMetadata(2)
if dateadd("s", expires, setTime) > now then
' værdien er stadig frisk (nok)
if isObject(cache(key)) then
set GetCache = cache(key)
else
GetCache = cache(key)
end if
if sliding = "S" then
SetCacheMetadata key, expires, sliding
end if
else
' værdien er forældet
GetCache = Empty
end if
end function
sub SetCacheMetadata(key, expires, sliding)
cache(key & "$metadata") = "" & _
now & ";" & _
expires & ";" & _
sliding
end sub
Oprydning i cachen
I takt med at cachen bliver brugt, ophober der sig en del værdier i cachen som er udløbet. Nogle vil blive overskrevet med friske værdier, mens andre bare vil ligge i cachen indtil caching-containeren (session eller application i ovenstående eksempler) nulstilles. Dette kan godt (og vil givetvis) blive et problem over tid, så man bør overveje en oprydningsfunktion der køres fra tid til anden. Noget i stil med dette:
function CleanupCache()
dim metadata, arr, setTime, expires, keylen, key
keylen = len("$metadata")
for each key in application.contents
if right(key, keylen) = "$metadata" then
metadata = application(key) & ""
arr = split(metadata, ";")
setTime = arr(0)
expires = arr(1)
if dateadd("s", expires, setTime) < now then
application.Contents.Remove key
application.Contents.Remove mid(key, 1, len(key) - keylen)
end if
end if
next
end function
dim metadata, arr, setTime, expires, keylen, key
keylen = len("$metadata")
for each key in application.contents
if right(key, keylen) = "$metadata" then
metadata = application(key) & ""
arr = split(metadata, ";")
setTime = arr(0)
expires = arr(1)
if dateadd("s", expires, setTime) < now then
application.Contents.Remove key
application.Contents.Remove mid(key, 1, len(key) - keylen)
end if
end if
next
end function
Et eller andet sted i systemet (f.eks. i forbindelse med caching-funktionerne) kan man så lave flg. kode:
cleanupts = application("cleanupts")
if not isEmpty(cleanupts) then
doClean = dateadd("s", 120, cleanupts) < now
else
application("cleanupts") = now
doClean = false
end if
if doClean then
application("cleanupts") = now
CleanupCache
end if
if not isEmpty(cleanupts) then
doClean = dateadd("s", 120, cleanupts) < now
else
application("cleanupts") = now
doClean = false
end if
if doClean then
application("cleanupts") = now
CleanupCache
end if
Denne tjekker om det er tid til at udføre en oprydning i cachen. Ovenstående rydder op hver andet minut (eller første gang koden køres efter 2 minutter er gået). Dette kan naturligvis sættes efter behov.
Overvejelser
Hvis man benytter application som caching-container, skal man være opmærksom på at objekter ikke uden videre spiller sammen med denne. Hvis man ønsker at gemme objekter i application-objektet, skal man sørge for at det er et MTA-objekt, dvs. et flertrådet objekt. Hvis man forsøger at lægge et STA-objekt (enkelttrådet) i application, får man en kørselsfejl smidt i nakken.
Ovenstående implementering udfører ikke konsistent caching i webgardens eller webfarms, da hverken application- eller session-objektet pr. default understøttes på tværs af workerprocesser. Hvis man ønsker caching i sådan et setup, er man nok nød til at finde en professionel cachingløsning i stedet for den hjemmestrikkede udgave jeg har illustreret (tak til Arne for den påmindelse).
Ud over det, kunne der arbejdes videre med at pakke caching-mekanismen ind i en class, men den øvelse vil jeg pt. overlade til dem der finder dette tiltag relevant... :-)
Happy Caching!


