基于redis的事务机制以及watch指令(CAS)实现乐观锁的过程。
所谓乐观锁,就是利用版本号比较机制,只是在读数据的时候,将读到的数据的版本号一起读出来,当对数据的操作结束后,准备写数据的时候,再进行一次数据版本号的比较,若版本号没有变化,即认为数据是一致的,没有更改,可以直接写入,若版本号有变化,则认为数据被更新,不能写入,防止脏写。
下面,看看如何基于redis实现乐观锁。
首先,看看redis的事务,涉及到的指令,主要有multi,exec,discard。而实现乐观锁的指令,在事务基础上,主要是watch指令,以及unwatch指令,unwatch通常可以不用!
1.multi,开启Redis的事务,置客户端为事务态。
2.exec,提交事务,执行从multi到此命令前的命令队列,置客户端为非事务态。
3.discard,取消事务,置客户端为非事务态。
4.watch,监视键值对,作用时如果事务提交exec时发现监视的监视对发生变化,事务将被取消。
案例1:redis的纯事务
下面是ssh窗口1里面的操作:
127.0.0.1:6379> set hello 1 OK 127.0.0.1:6379> get hello "1" 127.0.0.1:6379> multi OK 127.0.0.1:6379> incr hello QUEUED 127.0.0.1:6379> incr hello #这一步执行完毕后,去另外一个窗口(ssh窗口2),对hello这个key做incr操作,将hello对应的值变成2。完成后,继续后面的exec指令 QUEUED 127.0.0.1:6379> exec 1) (integer) 3 #注意,这时hello的值是3了,前面执行get hello指令时,值是1哟,说明这个值在其他地方被修改过,这里的其他地方,就是指前面提到的,在另外一个连接窗口里面执行的。 2) (integer) 4 127.0.0.1:6379>
这个情景下,multi和exec之间的指令,依然是可以执行的。
下面的操作,就是在ssh窗口2里面的操作:
127.0.0.1:6379> 127.0.0.1:6379> get hello "1" 127.0.0.1:6379> incr hello (integer) 2 127.0.0.1:6379>
案例2: 利用watch指令,基于CAS机制,简单的乐观锁
下面是ssh窗口1里面的操作:
127.0.0.1:6379> watch hello OK 127.0.0.1:6379> get hello "4" 127.0.0.1:6379> multi OK 127.0.0.1:6379> incr hello QUEUED 127.0.0.1:6379> incr hello #这一步执行完毕后,去另外一个窗口(ssh窗口2),对hello这个key做incr操作,将其值变成5。完成后,继续后面的exec指令 QUEUED 127.0.0.1:6379> exec (nil) #注意,这是exec执行后返回的是nil,表示事务提交执行失败 127.0.0.1:6379> 127.0.0.1:6379> get hello #这个时候,查看hello对应的值,就是在另外一个窗口(ssh窗口2)执行incr后的值 "5"
下面是ssh窗口2里面的操作:
127.0.0.1:6379> incr hello (integer) 5 127.0.0.1:6379>
案例3:watch指令在一次事务执行完毕后,即结束其生命周期
下面是ssh窗口1里面的操作:
127.0.0.1:6379> multi #接着上面案例2后,不再输入watch hello这个指令,直接启动事务 OK 127.0.0.1:6379> incr hello QUEUED 127.0.0.1:6379> incr hello #这一步执行完毕后,就在另外一个窗口(ssh窗口2),执行incr hello,将hello的值变成6。 QUEUED 127.0.0.1:6379> exec #另外一个窗口(ssh窗口2)里面的操作结束后,继续来这个窗口执行该指令,依然完成了上面的两个incr hello的操作。 1) (integer) 7 2) (integer) 8 127.0.0.1:6379>
下面是ssh窗口2里面的操作:
127.0.0.1:6379> incr hello (integer) 6 127.0.0.1:6379>
上述3个案例的操作,指令其实非常的少,两个窗口的指令全集,截图如下:
在另外一个窗口(ssh窗口2)中的操作:
通过这个简单的例子,基于redis的乐观锁,可以得出一个结论:
1. 乐观锁的实现,必须基于WATCH,然后利用redis的事务。
2. WATCH生命周期,只是和事务关联的,一个事务执行完毕,相应的watch的生命周期即结束。
案例2:用redis乐观锁实现的秒杀系统
代码实现:
- package com.github.distribute.lock.redis;
- import java.util.List;
- import java.util.Set;
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
- import redis.clients.jedis.Jedis;
- import redis.clients.jedis.Transaction;
- /**
- * redis乐观锁实例
- * @author linbingwen
- *
- */
- public class OptimisticLockTest {
- public static void main(String[] args) throws InterruptedException {
- long starTime=System.currentTimeMillis();
- initPrduct();
- initClient();
- printResult();
- long endTime=System.currentTimeMillis();
- long Time=endTime-starTime;
- System.out.println("程序运行时间: "+Time+"ms");
- }
- /**
- * 输出结果
- */
- public static void printResult() {
- Jedis jedis = RedisUtil.getInstance().getJedis();
- Set<String> set = jedis.smembers("clientList");
- int i = 1;
- for (String value : set) {
- System.out.println("第" + i++ + "个抢到商品,"+value + " ");
- }
- RedisUtil.returnResource(jedis);
- }
- /*
- * 初始化顾客开始抢商品
- */
- public static void initClient() {
- ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
- int clientNum = 10000;// 模拟客户数目
- for (int i = 0; i < clientNum; i++) {
- cachedThreadPool.execute(new ClientThread(i));
- }
- cachedThreadPool.shutdown();
- while(true){
- if(cachedThreadPool.isTerminated()){
- System.out.println("所有的线程都结束了!");
- break;
- }
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- /**
- * 初始化商品个数
- */
- public static void initPrduct() {
- int prdNum = 100;// 商品个数
- String key = "prdNum";
- String clientList = "clientList";// 抢购到商品的顾客列表
- Jedis jedis = RedisUtil.getInstance().getJedis();
- if (jedis.exists(key)) {
- jedis.del(key);
- }
- if (jedis.exists(clientList)) {
- jedis.del(clientList);
- }
- jedis.set(key, String.valueOf(prdNum));// 初始化
- RedisUtil.returnResource(jedis);
- }
- }
- /**
- * 顾客线程
- *
- * @author linbingwen
- *
- */
- class ClientThread implements Runnable {
- Jedis jedis = null;
- String key = "prdNum";// 商品主键
- String clientList = "clientList"; 抢购到商品的顾客列表主键
- String clientName;
- public ClientThread(int num) {
- clientName = "编号=" + num;
- }
- public void run() {
- try {
- Thread.sleep((int)(Math.random()*5000));// 随机睡眠一下
- } catch (InterruptedException e1) {
- }
- while (true) {
- System.out.println("顾客:" + clientName + "开始抢商品");
- jedis = RedisUtil.getInstance().getJedis();
- try {
- jedis.watch(key);
- int prdNum = Integer.parseInt(jedis.get(key));// 当前商品个数
- if (prdNum > 0) {
- Transaction transaction = jedis.multi();
- transaction.set(key, String.valueOf(prdNum - 1));
- List<Object> result = transaction.exec();
- if (result == null || result.isEmpty()) {
- System.out.println("悲剧了,顾客:" + clientName + "没有抢到商品");// 可能是watch-key被外部修改,或者是数据操作被驳回
- } else {
- jedis.sadd(clientList, clientName);// 抢到商品记录一下
- System.out.println("好高兴,顾客:" + clientName + "抢到商品");
- break;
- }
- } else {
- System.out.println("悲剧了,库存为0,顾客:" + clientName + "没有抢到商品");
- break;
- }
- } catch (Exception e) {
- e.printStackTrace();
- } finally {
- jedis.unwatch();
- RedisUtil.returnResource(jedis);
- }
- }
- }
- }
和上文的使用悲观锁相比,乐观锁的实现更加的简单,并发性能也会更好。