Microservices in Go: Caching with Redis and Heroku

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
so a very common pattern in software development is the usage of caching behind apis so users will come in third party users will come in and they will hit your apis and typically without a cache your api will directly call to some data source that data source could be a database like mysql or postgres or even another third-party api for example maybe you're using google maps api to search places or things or maybe you're using yelps api to get reviews on restaurants for your users but what as you scale as you scale an api up and you get more and more request volume you're going to start getting more and more latency and latency and performance issues from your data sources if you're using a database and you have thousands and thousands of reads coming in maybe it's not able to scale with that maybe it's not able to handle that and you'd either need to add on replicas of your database or put a cache in front of it if it's a third-party api maybe you're hitting google maps api you might start running into latency issues and also you'll run into rate limiting all right so they'll limit you to maybe a thousand requests a minute or some amount of requests per second and if you have thousands and thousands of users on your application you just won't be able to make that work you'll get rate limited and your users will your users will be upset from that so a common solution that comes in is caching in front of these data stores user makes a request your api first talks to a caching layer one of the most common ones uh we'll be using it today is redis so your api will talk to redis and see if the query is already cached if it is it just quickly returns that response back to the user there's typically an expiration time on these caches so values and there don't live forever they'll live for whatever you set them to could be 15 seconds it could be one hour it could be one week you choose how long you want you choose the ttl time to live and your cache you choose that value so if the cache miss you will actually make that call to your data source but before returning it to the user you'll cache it first you'll save it so the next time the user returns or another user has the same query you'll hit the cache first and you'll save a lot of latency and you'll reduce load that you're putting you'll reduce the load and strain that you're putting on your data sources all right so let's get started so our api that we'll be building um for our data source we're going to be using a third party api called nomentum and we're going to be caching its results and returning them for our users so you could use many types of data sources you could use a database something like mysql postgres or even mongodb or your data source could be some third-party api that you just don't want to put a bunch of load and unneeded strain on right so it could be an external system and you might have rate limiting in place there so you can only make a certain number of requests per minute or per second or per day so caching's really useful in this case especially if your users are going to be searching a lot of the same stuff so nominatem provides some nice api documentation and just for this for today we're going to let our users search for places so we can pass in for example this base url here and our users can type in a query they can give us a query here which is basically just a free form text string to search on and we can specify output formats from nominated so we can get back a json response to return to our user so let's get started i have just an empty folder here let's set up some things here we'll need a main.go we'll need for deploying and local development we'll use docker and docker compose so we'll need a docker file and docker compose and then finally uh let's just set up this up so go modinet go redis demo we'll create a go package here our module all right so in main let's set up a simple http server so go does have some really nice third-party open source libraries mux and go chi would be some of these and here's an example of kind of how you use them you'll set up a router you can use things like middleware for logging or authentication and then you just set up your handlers but today just for simplicity we're not going to use either of those we'll build using the standard library all right so let's get started let's set up a main function here and main let's go ahead and log out print line starting server and let's get started on this so we'll set up a handler um i believe handle func is what we want yep registers the handler function for the given pattern and needs a handle with it so we can say pattern we'll just call that slash api and for our handler we'll just have a general handler we'll need to declare that function the handler and it takes a specific kind um it takes a specific kind of function here that needs to have a responsewriter and a request object in it so we can copy these over and this is r and we'll call this w all right and then here we can just return now well let's let's have a log line format dot format.print in the handover all right so now we've set up our handler now we actually need to start our server so we can say http.listen and serve and that's just it that's literally how simple it is here so let's look at the documentation here listens on the network address and calls the server with various handlers um it says here the handler this parameter is typically nil in which case the default servermux is used so we're going to use that default uh server mux here so let's start listening on port 8080 and we'll use their default handler so that's all we need to do here and let's test this out we should be able to just run this really quickly go over to main and we see starting server and then let's curl that so we can say localhost yep 8080 api and verbose so we see here we get logged out in the handler above in our server and then below we just get an empty 200 okay response back so that looks good all right so next let's set up a little bit more with our response let's set up how our response is going to look so to do that let's make some types here so we'll call it api response and this will be a struct and right we're caching results so we do want our users to be able to see if it's a cache hit or cache miss so let's put that in so this right here is just a boolean of the response and it'll be true or false based on whether or not we hit the cache or not finally we'll have some data and it'll be something so let's figure out what exactly we need to put in data so this data object is literally just going to be the response from the meditem so let's take a look at what momentum uh looks like here so we can do curl we'll paste in their base um give us some of this here we'll paste in their base url and our query could just be san francisco and we need a format format equals json let's take a look at this um so we have an error here so it looked like when i pasted this in um this kind of messed up so q equals san francisco yep let's try that all right so we get the output from their api response and we basically we have an array of these objects so it's a list of all of these objects here and these are like places right so we get things back like san francisco city and county san francisco zip codes all of that so what we want to do now is convert this into a go struct so how do we do that json to struct there's a really cool website out there here json2go and you can paste in any json object and it gives you back this auto-generated go struct which is really nice to work with here so let's paste that in and let's change this to um let's change this to minutes and response let me make sure i spell that right no man that's a yes all right so our data is just a list of these no matter 10 responses call it data okay that's good there um and returning this is very simple you just json.new encoder and this encoder takes a i o writer and it just so happens we have our writer right here w so we can pass that in and we can call the encode function and encode takes some um sum interface and encodes into json so let's make that right now so i'm gonna say resp um is an api response and cache i'm just gonna hard code these so cache is false for now and then data is let's make an empty momentum response array okay and then let's encode and return it and encode does return an error so it can't error out so let's catch that here error does not equal nil at one i'm just going to use print f so let's deform the print f error encoding response and let's put the error in okay and let's write back right header um we'll say ht dot internal server okay so that's all good let's just run this again test this out really quick all right let me jq this okay so see still we get a 200 back and we also get our api response so there we go so next we have just an empty api response let's actually make this call to nominate here so up here let's just make a call for data is data or error is going to be equal to git data okay so what we're setting up here this is what we want to happen we want to call some function and this would hit our data source that could be a third party api like we're doing or it could be your mysql database or something along those lines but we just want to call something that gives us our data back so let's write that function now and this function should have a string it should take in a query string and it should return back a list of nomination responses and an error so for now let's just do nil no all right so what we want to do now is format the api we're going to hit so we want to get our address right so let's do this q is going to equal something and we need to have our format equals json and let's give it any spaces at the start here so q is going to be our string here our query string and so the user passes in a string for example like san francisco there's a space in the middle and we need to escape that so we need to have it end up looking like this we need to escape the url so let's do that first and we can say escaped q and go has this built in url dot let's see query escape what do they have here path escape let's see let's check the documentation really quick for these path escape escapes the string so they can be safely placed inside of a ural path segment yes that's what we want there i believe that's what we want so let's see what is oops hit the wrong button let's reopen up all right so path escape and let's see what does this take i believe it just takes in a string yep just takes in a string so we can just pass in this and finally what we can say here format dot sprint f comma escaped q and let's make this a little bit wider so we can read all of this okay so this will give us our endpoint that we want and then go it's very simple to make uh basic requests so we can do http.get and this will return a response and an error and it takes in the url so we can just say address here and then we can say response comma error if error does not equal no let's handle our error we want to return nil comma error otherwise we do have a response and we need to read it now so we need to read that response we need to build our actual nomination response array so we can say data make and we'll do an empty momentum response array and now just like above here we had this encoder we can also use json.new decoder new decoder so that takes a reader so we need to find the reader and we have our response object dot body which just happens to be what we're looking for here it's a reader and then we can say decode and this now we need to pass the address of wherever we want to decode so we'll say address data and this could throw an error so let's catch that error equals this if error does not equal nil we want to return nil comma error otherwise we've decoded our data successfully and let's return data with a nil error okay so now we have this git data we see that it needs a query string so to get our query string we can do r dot url dot query dot get and that takes the whatever parameter that we want to get from the user which is just q okay and we can pass that in so now we need to use data here so now instead of just using this hard coded mc array we can pass data okay and let's see what is this the value of error is never used let's handle that here so let's handle the error we can potentially get f error calling data source and then let's print our error here i think i'm forgetting to do new lines so let me do that all right print the error and then let's write a header okay handle there now we're actually returning the error all right so that should be good let's test this out here go run main.go localhost api and now we need to pass in our query so we can do san francisco when we get back so we actually get back our response here so this is that cached uh this is that response from the amenitive here so we actually called their api and it was successful and we see cache false and data is that large array so we're working we're doing things now this is good okay so we have our data source wired up here we have our api response getting built and we're sending the response back awesome now let's add in caching so that's the next step here so get data i think that's the that's the place that should live here let's do some reformatting yep this looks good so now at the start before we make this request here we need to check to see if we have a cached value or not so this is where the cache should live up at the very top here is query cached that is the answer that's the question that we want to answer here so now is the time we revisit our docker compose and our docker file we need to actually set up our docker compose to run our server and a local redis server so let's get started so version 3.3 you can use whatever version you want and we're going to make two services our first one is going to be web and this will be our go service we'll need to expose our port that we're listening on 8080 8080 and we'll set up environment as well there will be a couple things here the first one um that we'll specify here will be our port and this is the port we want to listen on and this is important because when we deploy later to heroku heroku um when you make a deployment only opens up a single port and your server needs to listen on that port and it could be any port it's a random port so it's important that we set this up now locally it'll be 8080 but we have no idea what port heroku wants us to listen on so ports that's good here let's set up our redis service now and we're just going to pull in the default redis alpine don't need any environment variables here let's link these together redis url and this will be linked to redis all right so let's get there in our docker file now so we're building we need to actually build and it looks for a local docker file let me specify this dot here so let's set up a really basic um go docker file so go lang don't care about the version um it'll just take the default or the latest we're gonna make a directory to store our code and our binary that we build here we're going to add everything into that directory we're going to specify that that's our working directory finally let's build our binary so go build and we're going to output that to main and we're just going to say everything here finally we'll expose our port 8080 and let's run command app sesh main okay so this is all good here so let's try running this now um see if we can catch any errors docker compose up build let's give this a second here all right we're back and our server is running let's test it out so curl localhost awesome so we get our log in the handler and we get our response back great let's close this down all right let's get back in to our application here so we're going to need to reformat some things we're going to need to add in go redis first so let's do this go redis is the library that we'll be using here there's a few other redis libraries out there you can use whichever one you like or familiar with we'll use this one so we need to install it let's run that command and then next so let's go ahead and bring this over and we are going to make another type here so let's go down let's call this type api so this api struct is going to hold relevant things for api like our data sources are if we had a logger it would hold the logger as well but for us we're just going to have a cache and it's going to be a pointer to redis dot client and that's it and next we'll have a function here new api and we want that to return a pointer to an api address of an api cache for now is no okay and let's add our code in here to connect to redis cache will be rdb okay so here's our connection for redis here and let's get that from the environment redis url and yep so let's do a sprint f here sprint f and when we run this locally we need to specify the port when we deploy we'll have to do something a little bit different was it six three seven nine that's the default port so this will work locally when we go to deploy for heroku uh we'll have to change this back we'll have to alter this a little bit so we're putting in our base um we're setting apparatus address here and this is what we're getting from the environment from our docker compose and then we're just adding a port on the end and passing it through okay so that should run um next we need to have a way for our handler and our getdata function to access this api so we're going to add these methods now onto that struct so a api same thing for data a api all right that's good now and now you see here undeclared name handler because we can't just call it anymore we need to call it on a struct so we need to say api is new api okay and then we need to call api.handler that works there and here we need to call a.getdata awesome so we're good so what this lets us do inside handler or get data now we can access that cache so for example i can do something like a dot cache dot get right we can do things like this um and that's that's what we're trying to do here okay so we want to see if the user's query has been cached so you can check that right here and this would be the example we want here so there's a couple of different options right so this first error you see redis.nil that means the key doesn't exist so the key wasn't found otherwise it's some other error and that's like an error with the redis server we couldn't connect um something went wrong behind the scenes and that's a bad error that we want to handle and then the third one would be the value was found so let's check over here bring this over all right so is the query cached so we'll say a dot dot get and this key is going to be just our query string so this this takes a context so context in this case let's say a user makes a request and this request is going slowly and it's not completed yet but the user closes the web page or they cancel the request in that case passing a context allows us to cancel the redis query as well so if a user isn't listening for a response anymore we shouldn't keep trying to fetch a result we should just cancel that request and move on uh with our date that's what this context allows us to do so let's um let's add another parameter here context context.context context.context and now we can just pass that straight through now we need to add this parameter up here so where do we get this context every http request and go has a built in context so we can say r.context and that just gives us the request context so this request gets cancelled midway through we'll also recancel we'll cancel our request to redis and we'll just speed up and save resources that way okay we'll change this to value so here this is the case that the key doesn't exist so we want to set in this case we want to call external data source set the value return the response that's what we want to do there in this case there was some error with redis and we want to log it so format print f error calling redis put a new line and then we want to return nil and just air okay otherwise we found the value and we can just we need to decode or build response return response okay so let's handle this first case first so if the value is nil then we want to call the external data source so this means we can bring up a lot of code from here let's just copy the whole thing and we'll put it in there so we just called the external data source and now here before we return the value we need to set the value so let's put this here where we're returning this our response but here we need to set the value so to do this let's look over here this is the function we'll use it's our cache.set we give it a context whatever key and then this needs to be the string the encoded value basically so we need to convert our json object into like a string and then this third parameter is going to be the xperie how long it lives in redis let's just copy this over and we'll say a dot cash dot set and we'll handle our error here so same thing we'd return nil and error and the key is the user's query we're passing under remember and then our value we need to say encoded data we'll do that encoded data this doesn't exist yet we'll make it and then expiration let's say time dot duration uh we'll say time.second times 15. we'll make this a 15 second expiration just so it's a quick for the demo right so we can query and see some cache hits and some cache misses all right let's encode the data here so for this we can use our json encoder so we'll do json.new encoder um let's see here uh marshaller i believe is what we want json.marshall yes so this will return this takes in some interface here some objects which is our data response and it gives us a byte array or an error okay so let's go ahead and marshal this data and this will give us byte our byte or error and let's handle the error otherwise we can use this now so we'll say b and i believe we'll have to convert this to bytes so new buffer string and let's say byte bytes b okay so i believe we need to declare that cannot use b byte as string okay there we go so we want just new buffer so we set the value now times out at 15 seconds and we return our response awesome so that should be working okay and so this is handled here if it's just another redis error so last thing we need to do is handle the case when it's a cache hit so this is a cache hit down here so we need to build that response so we need to say data and let's actually build the response so we'll do remember we are setting the response in this kind of marshaled encoded buffer so now we need to unmarshal it here so data json.on marshall and it just takes value what does this take here so this takes the value that we're on marshalling and it also takes the address that we want to on marshall 2. so in this case it'd be value and this should be data and it just returns an error so here we'll say error but we need to say we need to define data up here okay what is this cannot use variable string okay um can i just cast this or do i need to actually convert this okay so we need to convert that byte stop buffer string value dot bytes okay and let's handle the error so if that error is not nil otherwise we can return data and no okay what is this crawl of unmarshall pass this non-pointer okay so it wants a pointer so we need to give the address of data to unmarshall to okay so it's a cache hit so let's just review this really quickly so now when we go to get our data we first before we call our actual data source we first check our cache so we get the value from the cache we check is it a is there error equal to redis nil and in that case that means we just didn't find the value in our cache so then we actually reach out call our external data source um yeah we call our external data source we get the data back here we save it so we marshal it up into some bytes and then we save it into our cache and then finally return the response if it's just a random error from redis we just log that and return it and otherwise it was a cache hit so we'll um actually return this the value directly from the cache and we don't even call the data source so one thing as well we need to figure out if this was a cache hit or not right so let's add on another response here so boolean and this will determine if it's a cache hit or miss so in these cases we'll just put false this is a miss false this was a miss this was a miss this was a miss so this case this was a miss here so it's false this was a mess here and this this is a miss here so this one is the one where it's cache hit where we return true okay that um that's just a simple way of doing it uh you might have a cleaner different way of doing it without adding on this cache hit or miss variable so up here we'll say cache hit so now we check if it's a cache yet or not so this will be true or false based on what we got returned from git data so now we just pass that through to cache it um so that should do it for this uh let's run it and see if we can find any errors here all right so if you run into any errors here docker goes up build um if you run into any errors where it's like missing a package or anything along those lines you can add in a line into your docker file here go get t and this will add in any mini packages there's any missing packages as well feel free to put this in if you're not getting in there don't worry about it all right so our server is up and running here starting server and let's test it out curl localhost san francisco jq let's hit it and what do we get in handler i just want to see if it's a cache hit or miss so cache false so now we call it again we should get a cash hit and we see we do um cash true and we see in the handler here so let's wait a few seconds and let's see if we can get a cash miss so it should expire within 15 seconds and then the next time we call it here uh let's see if it was 15 seconds so cash false so the value expired um awesome so it looks like we're working here um yep looks like everything's working here one thing i realized we forgot we're still hard-coding our port so we can change this here let's say dot get in port and let's just change this over to b [Music] sprint f a string so we need a string comma os dot get in and [Music] missing uh there we go missing a parenthesis there awesome yeah so i think we're good here one thing we are going to have to change is um our redis address and so let's go ahead and start with deployment process and we'll figure out where we need to change our redis address when we get there so here we don't have any apps set up you can create an app from the dashboard here or you can do it from your terminal you can do it for the dashboard let's jump in i believe there's a guide right here for doing it from the terminal yes all right so this is heroku's guide for container registry and runtime so when you're deploying with docker you'll use heroku's container registry um heroku container registry allows you to deploy your docker images to heroku so let's just follow this process here i think i'm already signed in but we'll just double check yep all right login succeeded and next we can do broker create this will create a application here all right we have our application and next thing we should just be able to push our container up um so before we do that actually i'm realizing we'll need to provision our resource here so we need redis right our application defense uh depends on redis so if we refresh our page we see here's our app we just made um you can just go right into resources and let's search for redis so there's a bunch of different options here i'm just going to use heroku redis it's the simplest so it's free and let's order that and this provisioning process will take a little bit of time so i'll check back in with you when that completes so while our resource is provisioning there's one last step we'll need to do we'll need to change how we get our redis address configured so when we're running local we want to configure our redis address just the way we're doing here when we deploy onto heroku we need to set this up in a different manner so we need to have a way to determine if we're local or not so we can set a variable here local and we can just say true so when we're using docker compose locally this will be set to true when we deploy to heroku they're not going to set this local variable and we'll know we're remote so we can check that if os dot get n local is true we want our redis address to look like this and we want to create some opts right so we want to create this here these are options otherwise we're going to be remote and we want to create a different set of options here so let's bring this back up and finally we just want to set this up with opts and then up here we can go ahead and say redis dot opts redistop options okay redis address all right so when we're locally running um we are going to parse our address just the way we've been doing and we're going to build our client with those options uh when we're deployed on heroku we're going to parse it a little bit differently all right so we see we've provisioned our resource here and now let's see why we need to parse this differently so we see here we have the redis url environment variable but it looks a lot different right there's a port in there there's a long endpoint in there there's also a password or a username in there as well and there's this prefix so redis go redis has this built-in function here parse url so we can call redis.parse url and we can get it a a long format url and it will let us parse it so see here's the format this is the exact format we're using here that heroic is using so this is kind of what we want to do so now let's do this parse url and we're just going to give it os.getn redis url and this returns an option and an error so we need to say error and let's handle that error we're just going to panic here with this error what is this complaining about undeclared name so we can declare it there and we'll set that equal all right so we should be good to deploy now and yeah let's go ahead and deploy this so if we go back to the uh roku guide here we've created our app so now we need to build the image and push to our container registry here so let's run this command and then after that completes we will run container release and we have a missing app so we just need to pass in the app name which is this so we can do this hyphen a and then pass in peaceful depths all right and we've successfully pushed it up and let's release it so this should release that uh image we just put up and now we need that flag let's get our app name again and also while we're here let's find our base url so this is where we can call our app all right so we just released that and now we should be able to curl that endpoint slash api and then we give it query san francisco and let's test this out all right and we see we get our result back and let's check our cache so cache is false and let's rerun that query and we see caching is true here awesome and we can rerun this one more time once we get to 15 seconds i think it's been about 15 seconds run it again yeah cash is false and cash is true awesome so there you have it we have built out a just a proof of concept a simple api and go with a very simple redis cache and this code is not the cleanest and it's definitely not production ready that's not the goal we're trying to do here today we're just trying to get a proof of concept and some basic app for you to hopefully um hopefully take away some new ideas for your product or whatever you're working on a good problem to have is having a bunch of users and needing to refactor some of your code to productionize it more having your clients asking for better performance and needing to scale that's a good problem to have right and that's typically when you'd move off of heroku and onto a a bigger cloud cloud provider like aws or google cloud or azure but a bad problem to have is spending tons and tons of your dev time on setting up your complicated infrastructure and productionizing all of your code before you even have a product and before you even have a customer so that's really i highly recommend just for ease of use using technologies like heroku or other just quick out of the box providers where you don't need to configure a lot of things things are wired together quickly they just work and you can get up and off the ground running so hope you learned something today hope you enjoyed this please make sure to like and subscribe and comment on these videos lets me know i'm doing something that you value and if you have any suggestions or things you'd like me to cover in the future leave a comment down below
Info
Channel: devtopics
Views: 766
Rating: undefined out of 5
Keywords: microservice, golang, microservices, heroku, redis, cache, caching, api, backend, software, docker, docker-compose, docker compose, deployment, deploy
Id: WVx0xavR9c8
Channel Id: undefined
Length: 45min 30sec (2730 seconds)
Published: Tue May 25 2021
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.