Flash and JavaScript are required for this feature.
Download the video from iTunes U or the Internet Archive.
Topics covered: Isolation
Instructors: Prof. Hari Balakrishnan and Prof. Samuel Madden
Lecture 18: Isolation
Related Resources
Lecture notes (PDF)
Quiz two is this week on Friday from, I hope that's correct, whatever's on the webpage is correct, but I think it's lecture 9 through recitation number 15. So, in particular, the log structure file system paper is not on the exam.
Some of you may have been told otherwise.
What we're going to do today is continue our discussion of atomicity.
Which, as you'll recall, is two properties that have to hold in order for atomicity to hold.
And the first property that we talked about was recoverability.
And the other property, which is what we're going to spend most of our time on today, is isolation.
So, before we get into isolation, I just want to wrap up the discussion of recoverability because we left it a little bit of three-quarters and didn't quite finish it.
So the story here was what we did first was talked about how to achieve a recoverable sector using two copies of the data and the chooser sector to choose between them.
We talked about one way of achieving recoverability using version histories.
And we did complete that discussion.
And then we started talking about a more efficient way of achieving recoverability in situations where you cared a lot about performance using logging.
And the main in logging is this protocol for when you decide to write the log called the write ahead logging (WAL) protocol.
And the idea is very simple.
Always write to the log before you update cell store.
And that's the main discipline that if you always follow then you'll get things right.
But one of the consequences of logging, you get two things from having this log.
The first is when an action aborts.
Independent of failure, when you're running an atomic action and the program calls abort or the system aborts that action, what the log allows you to do is go back through the log, scan the log backwards, look at all of the steps that that action took.
And that action didn't quite commit so you have to back those changes out, and the log helps you unroll backward.
So primarily what you need with a logging scheme, in or to do aborts, is the ability to undo.
Which means that what you need in the log, whenever you update a cell store to something else, you need to keep track of the old value.
And this is what we called in the log as an undo step.
But now failures also happen in addition to aborts, and we need a little bit more mechanism in addition to just the ability to undo.
And to understand why, let's take a specific example where you have an application and it is writing data to a database that is on disk.
And what we said was there was a log and the log is stored on disk as well.
And let's assume that the log is actually stored on a different disk.
Now, the write ahead log protocol basically says that whenever you update, so you have an action which has things like reads and writes of cell store.
And whenever there's a write to a cell store, the write ahead log protocol says write to the log before you write to the database or to the cell store.
So two things can happen.
The first thing, the simplest model here is that all writes are synchronous.
What this means is that if you see a write statement in an atomic action, the first thing you have to do is to write to the log.
And then that returns.
And then you write to the database.
And then you run the next action, the next step of the action.
So clearly by the time in this action, if you ever get to the commit point and you're about to exit your commit, it means that all of the data has already been written to the database.
Because, by assumption, we assume that all of the writes are synchronous and you write to the database and only then do you go to the next step.
And, assuming there's only one thread of execution, then in this simple model, you always write to the database, you first write to the log and then you write to this self-store in the database.
But by construction, when you get to the commit point, you know for sure that all of the data has been written to the database.
So if a failure happens now and you didn't get to commit and the system failed before the commit ran any time in the middle of this action, the only thing you really need to do is to roll back all of the changes made by actions that didn't get to commit, which means that in this model that I've described so far, the only thing you need to do is to undo actions that respond to actions that didn't commit.
Any action that committed by construction must have written all of its data and installed that data into cell store.
Because if an action gets to the statement to run commit and then finishes commit, when it got to commit, you know that because of all the writes being synchronous to the cell store, you know that that write got written.
And, by the write protocol's definition, you know the log got written before that.
So you don't actually have to do anything.
Even though the log contains the results of these actions that committed, you don't have to do anything for the committed actions.
So, in fact, this undo log is enough to handle the simple database as well where all of the writes are being done synchronously in this application.
But there are a few things that can happen.
One of the reasons we threw out version histories or discarded the idea to go to this logging oriented model is for higher performance.
And one of the ways we got higher performance is we didn't have to do these link list reversals in order to read and write items.
But it's also going to be tempting, and you've seen this before, to not want to do synchronous writes to cell store on a database in order to get high performance.
You might have asynchronous writes happening to the database and you don't know when those writes complete.
And what could happen, as a result of that, is you'd issue some writes to the database and then you go ahead and you commit the action.
But what could happen is the data that was supposed to be written to this database as store may not actually have gotten written because there's some kind of a cache in memory that actually returned to you from the write, but the write actually never made it to the database.
So this system, for higher performance, if you stick the cache in here, what could happen is that you might reach the commit point and finish commit but not be guaranteed that the cell store data actually got written to the database.
And if you fail now after commit, you have to go through the log and redo the actions for all of those actions that committed because some of those actions may not have made it into the cell storage in the database.
So what this means is that in general, when you put a cash here, any memory cash here or if you have any situation where the writes are asynchronously being done to cell store, you need both an undo log in order to handle aborts and to handle the simple case and you need the ability to redo the results of certain actions from the log.
And both of these are done, so the system fails and then, when you recover, before you allow other actions that might be ready to go to take over, the system has to recover running the recovery process.
And the recovery happens from the log.
And the way that recovery works is, and we went through this the last time, you scan the log backwards from the most recent entry in the log.
And, while scanning the log backwards, you make two lists.
You make a list of winners and a list of losers.
And the winners are all of those actions that either committed or aborted.
And there's one important point in the abort step, which is that when the system calls abort or when the application calls abort and abort returns, what the system does is goes through the log and looks at all the changes made by that action and undoes all of them.
And then it writes the record, called the abort record, onto the log.
So when you recovery and you see an abort record, you already know that those changes made by an aborted action have been undone.
When you compose a list of winners that are committed actions and aborted actions, the only things you really need to redo are the steps corresponding to the committed actions.
You don't have to undo the steps corresponding to the aborted actions because they already got undone.
Because only after they got undone that the abort entry was written to the log.
In addition to these committed and aborted actions that are winners, there are all other actions that were pending or active at the time of the cache, and those are losers.
And so what you have to do to the losers is to undo the actions done by losers.
So the actual recovery step runs after composing these winners and losers and it corresponds to redoing the committed winners and undoing the losers.
And independent of crashes, independent of failures, when you just have actions that might abort, the only thing you really need is undo.
You don't need to redo anything.
If you don't ever have any failures but just actions aborting, the only thing you need to do with the log is to undo the results of uncommitted actions before abort returns.
Now, this procedure is OK.
Failures happen rarely and you're willing to take a substantial amount of time to recover from a failure then the log might be quite long.
If a system fails once a week, your log might be quite long.
And if you're willing to take the time to scan through the log entirely and build up these winners and losers list then this is fine, this approach works just fine.
But people are often interested in optimizing the time it takes to recover from a crash.
And a common optimization that's done is called a checkpoint.
And I believe you've seen this in System R.
And, if you haven't seen it, you'll probably see it in tomorrow's recitation.
And the main idea in the checkpoint is it's an optimization that allows this recovery process not to have to scan the log all the way back to time zero.
That the system periodically, while it's normally operating, takes this checkpoint, which is to write a special record into the log.
And that record basically says at this point in time here are all of the actions that have committed and whose results have already been installed into the cell store.
In other words, they've committed and already been installed so when you recover you don't have to go and scan the log all the way back.
You just have to go back enough so you find all of the actions whose results haven't yet been installed completely which have been committed.
And the checkpoint also contains a list of all the actions that are currently active.
So once you write a checkpoint record to the log, during recovery you don't have to go all the way back in time.
There are a few other optimizations that you can do with checkpoints that are not that interesting to get into here, but the primary optimization that's done to speed up the recovery process is to use this checkpoint record.
And most database systems, the checkpoint record is pretty small.
It's not like you're check pointing the entire state of the database.
The main thing you're doing is it's a pretty small amount of state that you're using to just speed up recovery.
So you shouldn't be thinking that the checkpoint is something where you take the entire database and copy it over.
That's not what goes on.
It's a pretty lightweight, small amount of state rather than the size of all of the data in the system.
So that's the story behind recoverability.
And we're actually going to come back to a piece of the story after we talk about isolation now because it will turn out that the mechanisms for isolation and the mechanisms for recoverability using logs interact in certain ways, so we have to come back to this either later today or on Wednesday.
So now we're going to start talking about isolation.
If you remember, the idea behind isolation is when you have a set of actions that run concurrently, what you would like is an equivalent ordering of the steps of the actions running concurrently such that the results are equivalent to some serial ordering of the actions.
The simple way of describing isolation is do it all before or do it all after.
If you have actions, let's say T1, T2 and so on running at the same time.
And these actions might operate on some data that is in common, they might act on data that's not at all uncommon, what you would like is an equivalent of the state of the system after running these concurrent actions should be equivalent to some serial ordering of the actions.
And it will turn out that what's tricky about isolation is that if you want isolation and you don't care about performance it's very, very easy to do.
So it's very easy to get isolation if you don't care about performance.
It's very easy to run fast if you don't care about correctness.
So, you know, fast and correct is the hard problem.
Fast and not correct is trivial and correct and slow is also trivial.
I mean correct and slow is very easy because you could take all of these concurrent actions and just run them one after the other so you don't actually take advantage of any potential concurrency that might be possible.
So suddenly slow and correct is very easy to do.
And we'll actually start out with slow and correct and then optimize a simple scheme.
There has also been a huge amount of work that's been done on fast and correct schemes.
And, in the end, they all boil down to this one basic idea that we'll talk about toward the end of lecture today.
So let's take an example.
Let's say you have two actions.
I'm going to call them with Ts because very often these are intended to be transactions which are consistency and durability in addition to isolation and recoverability.
So I'm just going to use the word T to represent an action.
Let's say you have an action that does read of some variable x and it does write of a variable y and you have transaction T2 that does write x and it does write y.
So we're going to take a few examples like this in order to understand what it means for actions to run such that they're steps equivalent to some serial order.
So we're going to spend some time really understanding that.
And then, once we understand what we want, it will turn out to be relatively easy to come up with schemes to achieve it.
Let's say that what happens here is that the system gets presented with these concurrent actions.
And let's assume each of these is an atomic step.
So there are four steps that can be interleaving in arbitrary ways, in any number of ways.
Let's say that what happens is you run that first and then you run that second and then you run this third and then you run this fourth.
So what you get is that I'm going to introduce a little bit of a notation here.
I'm going to write these steps as r1(x), w2(x), w1(y) and w2(y). What the r represents is a read, w represents a write, what the subscripts represent is the identifier of the action doing the read or write.
And what's in parenthesis is the variable that's being written.
So this says that action one does a read of x, action two does a write of x, action one does a write of y and action two does a write of y.
Now, if you look at these different actions, there are a few steps that conflict with each other and a few that don't.
If you look at the read of x and the write of y, they're independent of everything else.
If you just have read x here and write y here, they don't conflict with each other because those can happen in any order and the results are exactly the same.
Similarly, the write y and the write x don't conflict with each other.
The only things that really conflict with each other are the read x and the write x in this example because the results depend on which goes first.
Write y conflicts with this write y, and those are basically the two things that conflict with each other in this example.
Generally, if you have two actions conflict with one another, if they both contain a read and a write of the same variable or a write and a write of the same variable.
So there are basically three things that conflict.
If you have some variable, call it z, if you have a read of z and a write of z in one action or the other, if you have write of z and read of z or if you have write of z and write of z and those conflict with one another.
Now a read and a read don't conflict.
Because it doesn't matter what order they run in, they are going to give you the same answer.
If you look at this ordering of r1(x), w2(x), w1(y) and w2(y), the things that conflict are these two and these two.
Now, if you look at the two things that conflict and you draw arrows from which one happens before the other, what you'll find is that for this x conflict one runs before two and for this y conflict one runs before two.
So the arrows, if I were to draw them in time order, would point this way.
And this ordering is basically the same as the same ordering you would get, even though the steps run in different order, it's exactly the same as if you ran T1 completely before T2. Because running T1 completely before T2 says the ordering is r1(x), w1(y), w2(x) and w2(y). But the results are exactly the same, this different interleaving, which does one, two, three, four.
So what this says is that this trace that you get, we're going to use the word trace for this, you present this concurrent actions each of which has one or more steps and the system runs them.
And then the order in which the individual steps run produces a trace.
And then the question is whether that trace is what is called serializable.
A trace is serializable if the trace's results are identical to running the actions in some serial order one after the other.
And so what we're going to be trying to do is, what we're going to do today is to come up with schemes which take these different steps corresponding to the action that produces an order that turns out to be a serializable order.
And the challenge is to do it in a way that allows you to get reasonable performance.
To give you an example of a nonserializable order, if we had the following trace, r1(x) followed by w2(x) followed by w2(y) followed by w1(y). What happens here is that these two guys conflict so you've got an arrow from one to two going this way.
And, similarly, these two guys conflict but the arrow in time goes from two to one, which means that if you drew the arrow one to two this way, you would have an arrow going the opposite direction.
So this doesn't correspond to any serial order because, as far as this conflict is concerned, this trace says that action one should run before action two.
And, as far as this conflict is concerned, it says that action two should run before action one.
Which means you're in trouble because there is really no serial ordering here.
This trace does not correspond to either T1 before T2 or T2 before T1 which means this trace, if you have a scheme that runs your actions, the steps of your action that produces the stress it means that that scheme does not provide isolation.
Notice that we don't actually care whether T1 runs before T2 or T2 runs before T1. We're not worried about that.
We're just worried about producing some serial equivalent order.
So that's the definition of the property that we want, serializability.
And what it says is a trace whose conflict arrows, these are these conflict arrows, are equivalent to some serial ordering of the steps of the action.
What we want is a trace conflict that should be in the same order as some serial schedule or some serial order of the actions.
So what we're going to do is in three parts.
The first part is we're going to look at one of these traces.
And given a trace, the first problem is to figure out whether that trace corresponds to some serial order.
And then we're going to derive a property that guarantees that if a sudden property holds we would be assured that a trace is in serial order.
And then the second part is we are going to come up with various schemes for achieving serializability.
And the third part is in order to prove that those schemes are correct, what we are going to do is to prove that this property, that all serial orderings should satisfy holes for the protocol or for the algorithm that we design.
That is the plan for the rest of today.
This property for serializability is going to use data structure or a construction called an action graph.
And it turns out what we are going to do, given one of these traces, is produce a graph out of those traces called the action graph.
And then we are going to look to see whether a certain property holds for that action graph.
Let me show you what this action graph is by example.
The graph itself consists of nodes and edges [UNINTELLIGIBLE].
And the nodes are not these r1s and w2s. What the nodes are, are the actions themselves.
So, if you have four actions running, you have four nodes on the graph.
And then there are edges between these nodes on the graph.
Let's do it by an example.
Let's say you have first action T1 which has read of x and write of y.
And just so we are sure that it is action one, I'm going to draw one underneath as a subscript because they're just reads.
Action two has a write of x and a write of y.
Action three has a read of y and a write of another variable z.
And action four has a read of x.
First of all, given these actions, which of the actions conflict with each other?
Let's first write for T1. Does T2 conflict with T1? Yes it does because the read x, I mean that's the same as that example, so suddenly T2 conflicts with T1. What about T3? Does T3 conflict with T1? What that means is the interleaving of the individual steps matter as far as the final answer is concerned.
Yes, it does because the write of y and the read of y conflict, so you've got T1 and T3 that you have to worry about.
Does T1 conflict with T4? No it doesn't.
The read and the read do not conflict.
Does T2 conflict with T3? Yes, it does because it has got the y.
Does T2 conflict with T4? It does.
And does T3 conflict with T4? It does not.
There's nothing that's shared.
Out of the six possible, or whatever, four, choose two possible conflicts, for conflicts you've got four of them that you've got to worry about.
Now we're going to draw this graph that has T1, T2, T3 and T4, and we're going to call this the action graph.
And what it's going to do is to draw arrows between actions that conflict with one another.
But of course the answer, the arrows depend on the order in which these individual steps get scheduled by the system running these actions.
We need an actual example for that.
Let's say that what happens is you present these concurrent actions.
And what happens is this guy runs first and then two, three, four, five, six and seven.
That's the order in which the system runs the individual steps of this action.
Now we're going to draw these arrows between actions.
If two actions share a conflicting operation and in the first action the conflicting operation occurs before the second action then you're going to draw an arrow from the first action to the second action.
So more generally there's an arrow from PI to PJ.
If I and J have a conflicting operation that individual step is run by the system for I first before J.
If you look here at T1 to T2 there's an arrow between T1 and T2. Because it ran r1(x) before it run w2(x). If you look at T1 and T3, this is a little subtle because if ran read of x before it ran r3(y), but that doesn't matter because read of x there and read of y there don't conflict.
But then it ran w1(y) after it ran read 3y which means that the conflict is equivalent to running that step of T3 before the step of T1. So you actually have an arrow going back from T3 to T1. Now what about T2 and T3? T2 and T3, the same story, w2(x) and r3(y) don't conflict.
But r3(y) before w2(y), that's the conflict, so you have an arrow going this way.
So we've got three of them, you need a fourth one, and that is between T2 and T4. Between T2 and T4, w2(x) runs before r4(x) which means you have an arrow going this way.
If you actually look at this picture, and we'll come up with a method to systematically argue this point, but if you look at that schedule, as shown here, where the system runs the individual steps in that order, this is actually equivalent to T3 running and then T1 running and then T2 running and then T4 running.
The order of interleaving these different steps in the way shown in that picture, one, two, three, four, five, six, seven is actually equivalent to the same result, it's the same result that you get if you run T3 completely and then T1 and then T2 and then T4. In fact, if you think about it, it's also equivalent to the same ordering that you get if you run T3 and then you run T1 and then you run T4 and then you run T2. That just says that for the exact same scheduling of individual steps in the concurrent actions you might find multiple equivalent serial orders that all give you the same answer.
Is that clear?
The arrow from two to four is correct.
Write effects runs before read effects.
So I think this is correct.
Is there a problem?
All right.
So, in this example, it isn't multiple serial orders.
But in general it appears that there are multiple serial orders possible.
What does this action graph got to do with anything?
It turns out, and we'll prove this, that if the action graph-- --for any ordering of steps within an action, if the action graph does not have a cycle-- --then the corresponding trace from which the action graph was derived is serializable.
What that means is that what you have to do is, given a certain ordering of steps, you construct this action graph, you look to see if it has any cycles.
And, if it doesn't have any cycles, then you know that the order is equivalent to some serial order of the steps of the individual actions.
And it actually turns out the result is a bit more powerful.
The converse is also true that if you have a serializable trace then the corresponding action graph has no cycles.
Now, to turn out, the more interesting result for us is going to be the following direction where if the graph is acyclic then the trace is serializable.
Because what we're going to end up doing is inventing one or two protocols for achieving isolation, for achieving serializability.
And we're going to prove that those protocols are correct by proving that the corresponding action graph, all of the possible action graphs produced by those protocols all have no cycles.
So this direction is the direction that's actually more important for us, but the opposite is also true and not that hard to prove.
So what is the intuition behind why, if you have an action graph that's serializable, sorry, that doesn't have cycles the trace is serializable?
Well, notice one thing about this which is to draw a little bit of intuition.
Suppose, in fact, what happened here was we didn't execute the actions, the steps in that order, but what we did was to run this at step five and that at step six.
What would happen with the resulting action graph is that you would actually have an arch from T2 to T1 going the other way as well.
Because what this says is between T1 and T2 there is one conflicting operation that goes this way where action one runs before two.
And these two guys conflict where the step in two runs before the step in one and those two steps conflict with one another.
And this is actually the cycle here that causes the whole scheme to be not serializable anymore.
That's a little bit of intuition as to why this acyclic property is important.
But to really prove this notice that if you have a directed acyclic graph you could do something called a topological sort on the graph.
How many people know what a topological sort is?
OK.
For those who don't, the idea is very simple.
In any directed acyclic graph there is going to be at least one node that has no arrows coming into it.
All of the arrows going out are only going out of the node.
And you can actually prove that by arguing the contradiction.
If it turns out that every node has an arch coming in and an arch going out then by traversing that chain of pointers you'll end up with a cycle.
So it's pretty easy to see that in any directed acyclic graph you're going to have some node that has no arrows coming into it and only arrows going out of it.
So find that action, in this picture it is T3, and take that action and put it in first.
That's the first action that you run.
Because it has no arrows coming into it and only arrows going out, what that means is that no other action in a serial order runs before it.
Or at least there's no reason for any action to run before it because there are no arrows from any other action coming into this action.
So you put that in first.
Now, remove that node from the entire graph.
The resulting graph is acyclic, right?
You cannot manufacture a cycle by removing a node so you recursively apply the same idea, find some other node which has no other things coming into it and only things going out of it.
And if there are ties just pick one at random.
And, therefore, construct an order.
And all such orders that you construct are topological sort orders.
This topological sort order, by construction, is a serial order of the actions.
And this topological sort, if you now draw the arrows in the topological sort, they're going to be the same arrows as in the original directed graph, it's exactly the same graph, and now you have an equivalent serial order.
That's the reason why the cyclic property is actually important as far as serializability is concerned.
Now we can look at schemes that actually guaranty serializability.
And the schemes we're going to discuss all are in this system where you have cell storage and where you have logs for recovery.
The logs are not going to matter, for the most part.
You can also do isolation in version histories, and one of the sections of the notes deals with that at length.
I personally think it's not that important, but that doesn't mean it's not on the quiz.
I think you get a little bit more intuition reading that in addition to this discussion.
And a bulk of this discussion is actually not in the notes.
It's just another way of looking at the problem.
The mechanism we're going to build on is, in fact, described in the notes as well.
It's a mechanism you've seen before, and it is called locks.
As you recall, if you have variable x or any chunk of data you can protect it using two calls, acquire and release.
So you could do things like acquire lock of x and release lock of x.
And the lock protocol is that only one person can acquire a lock at a time.
And all of the people wanting all other actions wishing to acquire the same lock will wait until the lock is released and then they fight to acquire it.
And ultimately at the lowest level the lock has to be implemented with some low level atomic instruction.
For example, something like a test and set lock instruction.
It's the same story as before.
Now, there are a few things to worry about with locks.
The first one is the granularity of the lock.
You could be very, very conservative and decide that the granularity of the lock is your entire system.
So all of the data on the system gets protected with one lock, which means that you're running this very slow scheme because what you're guaranteeing is, in fact, isolation but you're running essentially one action at a time and you have no concurrency at all.
The other extreme, you could be very, very aggressive and every individual cell stored item is protected with the lock.
And now you're trying to max out the degree of concurrency, which means that although things could be fast you have to be a lot more careful if you want things to be correct.
And correct here means that you have an order that is equivalent to some serial order.
Why does the locking protocol actually matter?
Well, let's go back to these two action examples of r1(x) w2(x) and w1(y) and w2(y). And let's just throw in an acquire lock of x here and an acquire lock of y here and a release lock of x here and in between here you have the read and here you do the release of the lock of y and similarly you do an acquire lx here and you do an acquire ly here.
And then you release the locks at the end here.
So acquire lock x, read x, release lock x, acquire lock y, write y, release lock y, and acquire right, acquire right, release, release.
And, on the face of it, this kind of thing is actually reasonable for the old style synchronization, some of the old style synchronization things that we wanted because we didn't actual care about atomicity of full actions.
If you look at what happens with this kind of locking, as shown here, you might be a little bit in trouble because it could be that these three steps happen first and then this whole set of steps up to here happen second, actually, up to the end.
And then this chunk happens third.
Now you're in trouble because read x happens before write x and the write y happens before this write y.
And now you have not achieved isolation because, if you draw the conflict graph, you'll get an arrow from T1 to T2 and an arrow coming back from T2 to T1 and you have a cycle, which means that just throwing in the right acquires and releases isn't going to be sufficient to achieve isolation.
So we're going to need a much better set of skills, a better scheme than just this throw in the right acquires and releases.
The first scheme we're going to see is a simple scheme.
It's called simple locking.
The idea in simple locking is that every action knows beforehand all of the data items that it wants to read or write, and it acquires all of the locks before it does anything.
Before doing any reads or writes it acquires all of the locks.
The idea would be here you would acquire the lock of x and acquire the lock of y, and then you run the steps.
Similarly, the other action does the same thing.
And by construction, if you have actions that conflict with one another and one of the actions reaches the point where all of the locks it needs have been acquired then by construction no other conflicting action could be in the same state.
Because if you have another action that conflicts then it means that there's at least one data item common to them which means that only one of them could have reached this point because they're both trying to acquire.
This protocol where every action acquires all of the locks before running any step of the action will guaranty isolation.
And the isolation order, the equivalent serial order that it guarantees is the same as the order in which the different actions reach this point where they have acquired all of the locks.
And that point is also called the lock point.
The lock point of an action is defined as the point where all of the locks that it needs or it wants to acquire have been acquired.
And, because in this discipline where no action does any operation until all of the locks it needs have been acquired, you're guaranteed that this serial order of every action following this protocol, independent of knowledge of any other actions, everybody just blindly follows this protocol, the serial order is the same as if the actions had run in the order of acquiring the lock points, whatever that order might be.
This is called simple locking, and it does provide isolation.
But the problem with simple locking where you acquire all of the locks before running anything is two-fold. The first problem is that this dictates that all of the actions know the different items that they want to read or write beforehand.
Which means that if you're deep inside some action and there are all sorts of conditional statements you kind of have to know beforehand all of the data items you might be reading and writing.
And that could be pretty tricky.
In practice, if you want to adopt simple locking, you might have to be very conservative and sort of try to lock everything or lock a large amount of data which reduces performance.
Second, it actually doesn't give you enough opportunity to get high performance, even when you do know all of the data items beforehand.
For example, one thing you could do is acquire the lock of x and then read x.
And while you're off trying to read x then you might acquire a lock of y and read y.
And so, depending on how you have structured your system scheme where you do some work, maybe some computation as well in between the lock acquisition steps might give you higher performance.
So the question is can we be a little bit more aggressive than the simple locking scheme in order to get isolation as well as a little bit higher performance?
And the answer is that there is such a scheme and it's called two-phase locking.
And although a lot of work has happened for maybe a couple of decades on very high performance locking schemes, it turns out in the end they all, or at least for a large class of schemes that use this kind of locking they all boil down to some variable of two-phase locking.
And the idea is very simple.
The two-phase locking idea says there should be no release before all the acquires are done.
So do not release any lock until all of the locks that you need have been applied.
That's what happens here.
This schemes violates two-phase locking because you actually release lock x before you acquire lock y, and that violated two-phase locking.
And this idea that you don't release before all the acquires, there is a little bit of a subtlety that happens because you want recoverability which is that the action could at any stage abort.
And, in order to abort an action, you need to go back and undo that variable, the value to a previous value.
Which means that in order to abort you need to hold onto the lock.
You better make sure that an action in order to abort has the locks for all of the data items whose values it wishes to change to the original value.
Which means that in practice, at least for all the items that you're writing, the locks that you hold should not be released until the commit point, until the place where the action calls commit.
So no release before all acquires is basically equivalent to-- There should be no acquire statement after any release statement.
The moment you see a release statement and then an acquire after that of anything, then you know that this violates two-phase locking.
It turns out that two-phase locking is correct that it provides isolation.
And to see why let's look at a picture.
We're going to go back to this action graph idea and look at a picture of what happens with two-phase locking.
What we're going to prove is that if you use two-phase locking and you construct an action graph of what you get from running a sequence of steps that action graph has no cycles.
And we know that if you have no cycles in the action graph you're guaranteed that it is equivalent to some serial order.
We will argue this by contradiction.
Let's say you have T1 and T2 all the way through some action Tk and you have a cycle going back from Tk to T1 in the action graph.
Now, if there is an arrow from T1 to T2, it means that there is some data item x1 in common between T1 and T2. And you know that T2 ran after T1 which means that in T1 there was a release done of l1 after which in the system there was an acquire done of l1. Likewise, between T2 and T3, there is some data item x2 such that a release was done of l2 by T2. And after that an acquire was done of l2 by T3. And the release has to have been done after the acquire of l1 because we're following two-phase locking.
If you continue that down up to here out for Tk, Tk did an acquire somewhere later in time of lk minus one.
And then it did a release of some data item, a lock of lk where lk is actually some data item that is shared between Tk and T1, so there is actually some data item xk whose lock is lk.
And you know that Tk did a release of lk before T1 did an acquire of lk.
But the T1's acquire of lk must have happened after the release for lk so it must have happened at some point here. T1 must have done an acquire of lk at some point at the bottom here.
Just going out in time, by two-phase locking you get release of l1, then the other guy doesn't acquire of l1, release of l2, acquire of l2 all the way out, then release of lk, and then after that in time an acquire of lk.
So that must have happened later on in time, but now this picture here violates two-phase locking because T1, for this cycle to hold, has to have done an acquire of lk after release of l1. But that violates two-phase locking because you're not allowed to acquire anything after you've released something.
So two-phase locking, therefore, cannot have a cycle in the action graph.
And, from the previous story, it means that it's equivalent to some serial order that corresponds to the topological sort of the directed acyclic graph.
I'm going to stop here.
We will continue with this stuff and then talk about other aspects of transactions next time.
And if there are any questions either send me an email or ask me the next time.
Free Downloads
Video
- iTunes U (MP4 - 117MB)
- Internet Archive (MP4 - 224MB)
Free Streaming
Subtitle
- English - US (SRT)