MySQL Redis分布式锁实现秒杀案例(jfinal框架)

发布时间:2018-08-19作者:laosun阅读(3592)

MySQL

    我们加锁就一行代码:jedis.set(String key, String value, String nxxx, String expx, int time),这个set()方法一共有五个形参:

        第一个为key,我们使用key来当锁,因为key是唯一的。

        第二个为value,我们传的是requestId,很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用UUID.randomUUID().toString()方法生成。

        第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;

        第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。

        第五个为time,与第四个参数相呼应,代表key的过期时间。

    总的来说,执行上面的set()方法就只会导致两种结果:1. 当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。2. 已有锁存在,不做任何操作。

    心细的童鞋就会发现了,我们的加锁代码满足我们可靠性里描述的三个条件。首先,set()加入了NX参数,可以保证如果已有key存在,则函数不会调用成功,也就是只有一个客户端能持有锁,满足互斥性。其次,由于我们对锁设置了过期时间,即使锁的持有者后续发生崩溃而没有解锁,锁也会因为到了过期时间而自动解锁(即key被删除),不会发生死锁。最后,因为我们将value赋值为requestId,代表加锁的客户端请求标识,那么在客户端在解锁的时候就可以进行校验是否是同一个客户端。由于我们只考虑Redis单机部署的场景,所以容错性我们暂不考虑。

    我们解锁只需要两行代码就搞定了!第一行代码,我们写了一个简单的Lua脚本代码,上一次见到这个编程语言还是在《黑客与画家》里,没想到这次居然用上了。第二行代码,我们将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行。

    那么这段Lua代码的功能是什么呢?其实很简单,首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)。那么为什么要使用Lua语言来实现呢?因为要确保上述操作是原子性的。


    下边来看简单的实现源码,不用在乎代码的质量,这里只是简单地做个演示!!!

    RedisUtil:

    package com.demo.utils;
    
    import redis.clients.jedis.Jedis;
    import redis.clients.jedis.JedisPool;
    import redis.clients.jedis.JedisPoolConfig;
    
    public final class RedisUtil {
    
        //Redis服务器IP
        private static String ADDR = "127.0.0.1";
    
        //Redis的端口号
        private static int PORT = 6379;
    
        //访问密码
        private static String AUTH = "redis123";
    
        //可用连接实例的最大数目,默认值为8;
        //如果赋值为-1,则表示不限制;如果pool已经分配了maxActive个jedis实例,则此时pool的状态为exhausted(耗尽)。
        private static int MAX_ACTIVE = -1;
    
        //控制一个pool最多有多少个状态为idle(空闲的)的jedis实例,默认值也是8。
        private static int MAX_IDLE = 200;
    
        //等待可用连接的最大时间,单位毫秒,默认值为-1,表示永不超时。如果超过等待时间,则直接抛出JedisConnectionException;
        private static int MAX_WAIT = 10000;
    
        private static int TIMEOUT = 10000;
    
        //在borrow一个jedis实例时,是否提前进行validate操作;如果为true,则得到的jedis实例均是可用的;
        private static boolean TEST_ON_BORROW = true;
    
        private static JedisPool jedisPool = null;
    
        /**
         * 初始化Redis连接池
         */
        static {
            try {
                JedisPoolConfig config = new JedisPoolConfig();
                config.setMaxTotal(MAX_ACTIVE);
                config.setMaxIdle(MAX_IDLE);
                config.setMaxWaitMillis(MAX_WAIT);
                config.setTestOnBorrow(TEST_ON_BORROW);
                jedisPool = new JedisPool(config, ADDR, PORT, TIMEOUT, AUTH);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        /**
         * 获取Jedis实例
         * @return
         */
        public static Jedis getJedis() {
            try {
                if (jedisPool != null) {
                    Jedis resource = jedisPool.getResource();
                    return resource;
                } else {
                    return null;
                }
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
    
        /**
         * 释放jedis资源
         * @param jedis
         */
        public static void returnResource(final Jedis jedis) {
            if (jedis != null) {
                jedisPool.returnResource(jedis);
            }
        }
    }

    RedisTool:

    package com.demo.utils;
    
    import java.util.Collections;
    
    import redis.clients.jedis.Jedis;
    
    public class RedisTool {
    
    	private static final String LOCK_SUCCESS = "OK";
    	private static final String SET_IF_NOT_EXIST = "NX";
    	private static final String SET_WITH_EXPIRE_TIME = "PX";
    	private static final Long RELEASE_SUCCESS = 1L;
    
    	/**
    	 * 尝试获取分布式锁
    	 * 
    	 * @param jedis
    	 *            Redis客户端
    	 * @param lockKey
    	 *            锁
    	 * @param requestId
    	 *            请求标识
    	 * @param expireTime
    	 *            超期时间
    	 * @return 是否获取成功
    	 */
    	public static boolean tryGetDistributedLock(String lockKey,
    			String requestId, int expireTime) {
    		Jedis jedis = RedisUtil.getJedis();
    		String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST,
    				SET_WITH_EXPIRE_TIME, expireTime);
    		RedisUtil.returnResource(jedis);
    		if (LOCK_SUCCESS.equals(result)) {
    			return true;
    		}
    		return false;
    
    	}
    
    	/**
    	 * 释放分布式锁
    	 * 
    	 * @param jedis
    	 *            Redis客户端
    	 * @param lockKey
    	 *            锁
    	 * @param requestId
    	 *            请求标识
    	 * @return 是否释放成功
    	 */
    	public static boolean releaseDistributedLock(String lockKey,
    			String requestId) {
    		Jedis jedis = RedisUtil.getJedis();
    		String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    		Object result = jedis.eval(script, Collections.singletonList(lockKey),
    				Collections.singletonList(requestId));
    		RedisUtil.returnResource(jedis);
    		if (RELEASE_SUCCESS.equals(result)) {
    			return true;
    		}
    		return false;
    
    	}
    
    }

    测试主源码:

    package com.demo.index;
    
    import com.demo.utils.RedisTool;
    import com.jfinal.core.Controller;
    import com.jfinal.plugin.activerecord.Db;
    import com.jfinal.plugin.activerecord.Record;
    
    /**
     * 秒杀/MySQL redis分布式锁
     * 
     * @author sun
     */
    public class SecKillForRedisController extends Controller {
    	
    	public static int goodsTotalNum = 100;
    
    	public void index() {
    		//每一次新的请求都会模拟抢购100件商品,所以先恢复商品的数量,这个可以不要,也可以手动去更新数据库
    		Db.update("update t_goods set num = ? where id = 1", goodsTotalNum);
    		
    		//这块我们就不使用redis的那种集合插件的方式了,我们自己添加redis工具包
    //		Jedis jedis = RedisUtil.getJedis();
    		
    		//正规的秒杀系统流程是(如果使用redis): 将商品库存存入redis中,然后增加redis分布式锁,
    		//当有用户抢到了商品后,会使用mq消息队列通知数据库进行库存减1(库存虽然存到redis中,但是还是要以数据库库存为准)
    		//我们这里只是演示redis分布式锁的使用,就不增加mq了,后期我会再专门整理一片redis+mq的使用方式的
    		
    		Record goods = Db.findFirst("select * from t_goods where id = 1");
    		Integer num = 0;
    		if(goods!=null){
    			num = goods.getInt("num");
    		}
    		
    		SecKillRedis sk = new SecKillRedis(num);
    		// 模拟120个人抢购。 模拟120人分别在不同的服务器进行发起请求
    		for (int i = 1; i <= 120; i++) {
    			new Thread(sk, i + " 号人").start();
    		}
    		
    		renderHtml("<center><h2>抢购成功!</h2></center>");
    	}
    }
    
    /**
     * 这里我们只是简单演示实现方式,至于事务等其他的都不添加了。
     * 
     * @author sun
     */
    class SecKillRedis implements Runnable {
    	
    	private int num = 0;
    	
    	public SecKillRedis(int num) {
    		this.num = num;
    	}
    	
    	@Override
    	public void run() {
    		//设置10000毫秒锁失效,避免出现死锁
    		//这里的redis value值我们就用线程名字了,当然也可以使用requestId等,只要保证每个用户的唯一即可,因为在释放锁的时候需要校验这个值
    		while(num>0){
    			if(RedisTool.tryGetDistributedLock("lock_sun", Thread.currentThread().getName(), 10000)){
    				sale();
    				return ;
    			}
    		}
    		System.err.println(Thread.currentThread().getName() + " 太悲剧了,没抢到商品...");
    	}
    	
    	private void sale() {
    		if(num>0){
    			Db.update("update t_goods set num=num-1 where id = 1");
    			num--;
    			System.out.println(Thread.currentThread().getName()+" 抢到了第"+(SecKillForRedisController.goodsTotalNum - num)+" 件商品;"+"释放锁状态:"+RedisTool.releaseDistributedLock("lock_sun", Thread.currentThread().getName()));
    		}
    	}
    }

    打印结果:

    26 号人 抢到了第1 件商品;释放锁状态:true
    24 号人 抢到了第2 件商品;释放锁状态:true
    73 号人 抢到了第3 件商品;释放锁状态:true
    47 号人 抢到了第4 件商品;释放锁状态:true
    56 号人 抢到了第5 件商品;释放锁状态:true
    33 号人 抢到了第6 件商品;释放锁状态:true
    40 号人 抢到了第7 件商品;释放锁状态:true
    14 号人 抢到了第8 件商品;释放锁状态:true
    31 号人 抢到了第9 件商品;释放锁状态:true
    27 号人 抢到了第10 件商品;释放锁状态:true
    59 号人 抢到了第11 件商品;释放锁状态:true
    17 号人 抢到了第12 件商品;释放锁状态:true
    7 号人 抢到了第13 件商品;释放锁状态:true
    43 号人 抢到了第14 件商品;释放锁状态:true
    ......
    60 号人 抢到了第97 件商品;释放锁状态:true
    120 号人 抢到了第98 件商品;释放锁状态:true
    6 号人 抢到了第99 件商品;释放锁状态:true
    116 号人 太悲剧了,没抢到商品...
    97 号人 太悲剧了,没抢到商品...
    114 号人 太悲剧了,没抢到商品...
    117 号人 太悲剧了,没抢到商品...
    39 号人 太悲剧了,没抢到商品...
    58 号人 太悲剧了,没抢到商品...
    41 号人 太悲剧了,没抢到商品...
    105 号人 太悲剧了,没抢到商品...
    2 号人 太悲剧了,没抢到商品...
    110 号人 太悲剧了,没抢到商品...
    107 号人 太悲剧了,没抢到商品...
    64 号人 太悲剧了,没抢到商品...
    81 号人 太悲剧了,没抢到商品...
    57 号人 太悲剧了,没抢到商品...
    68 号人 太悲剧了,没抢到商品...
    45 号人 太悲剧了,没抢到商品...
    72 号人 太悲剧了,没抢到商品...
    48 号人 太悲剧了,没抢到商品...
    79 号人 太悲剧了,没抢到商品...
    16 号人 抢到了第100 件商品;释放锁状态:true


    t_goods表结构:

    CREATE TABLE `d_sunjs_test`.`t_goods`  (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
      `num` int(11) NULL DEFAULT NULL,
      `version` int(11) NULL DEFAULT 1 COMMENT '乐观锁',
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;


    下一次我们将使用zookeeper来做个简单的分布式锁实现秒杀抢购

3 +1

版权声明

 Java  源码  多线程

 请文明留言

1 条评论