likes
comments
collection
share

带你详细了解Redis事务锁机制-加实列演示-加连接池-包括解决遗留问题-下

作者站长头像
站长
· 阅读数 15

Redis_事务_锁机制_秒杀

连接池技术

连接池介绍

1、节省每次连接redis 服务带来的消耗,把连接好的实例反复利用。

2、链接池参数

  • MaxTotal:控制一个pool 可分配多少个jedis 实例,通过pool.getResource()来获取;如果赋值为-1,则表示不限制
  • -maxIdle:控制一个pool 最多有多少个状态为idle(空闲)的jedis 实例
  • -MaxWaitMillis:表示当获取一个jedis 实例时,最大的等待毫秒数,如果超过等待时间,则直接抛JedisConnectionException
  • -testOnBorrow:获得一个jedis 实例的时候是否检查连接可用性(ping());如果为true,则得到的jedis 实例均是可用的

使用连接池, 优化连接超时

通过连接池,可以指定连接超时时间, 这个连接超时时间,也需要合理设置,要考虑到用户的实际体验

创建JedisPoolUtil

src\com\seckill\utils\JedisPoolUtil.java

public class JedisPoolUtil {
    
    //解读volatile作用
    //1. 线程的可见性: 当一个线程去修改一个共享变量时, 另外一个线程可以读取这个修改的值
    //2. 顺序的一致性: 禁止指令重排
    private static volatile JedisPool jedisPool = null;
    private JedisPoolUtil() {
    }
    //保证每次调用返回的 jedisPool是单例-这里使用了双重校验
    public static JedisPool getJedisPoolInstance() {

        if (null == jedisPool) {
            synchronized (JedisPoolUtil.class) {
                if (null == jedisPool) {
                    JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
                    //对连接池进行配置
                    jedisPoolConfig.setMaxTotal(200);
                    jedisPoolConfig.setMaxIdle(32);
                    jedisPoolConfig.setMaxWaitMillis(60 * 1000);
                    jedisPoolConfig.setBlockWhenExhausted(true);
                    jedisPoolConfig.setTestOnBorrow(true);
                    jedisPool = new JedisPool(jedisPoolConfig, "192.168.198.135", 6379, 60000);
                }
            }
        }
        return jedisPool;
    }
    //释放连接资源
    public static void release(Jedis jedis) {

        if(null != jedis) {
            jedis.close();//如果这个jedis是从连接池获取的,这里jedis.close(),就是将jedis对象/连接,释放到连接池
        }
    }
}

修改SecKillRedis

ticket\src\com\seckill\redis\SecKillRedis.java

//- 连接到Redis, 得到jedis对象
        //Jedis jedis = new Jedis("192.168.198.135", 6379);

        //- 通过连接池获取到jedis对象/连接
        JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
        Jedis jedis = jedisPoolInstance.getResource();
        System.out.println("---使用的连接池技术----");

测试可用即可

带你详细了解Redis事务锁机制-加实列演示-加连接池-包括解决遗留问题-下

线程可见性

--简单说一下指令重排 带你详细了解Redis事务锁机制-加实列演示-加连接池-包括解决遗留问题-下

版本3:利用Reids 事务机制,解决超卖

1、控制超卖-Redis 事务底层(乐观锁机制分析)

带你详细了解Redis事务锁机制-加实列演示-加连接池-包括解决遗留问题-下

2 、修改SecKillRedis

src\com\seckill\redis\SecKillRedis.java

public class SecKillRedis {

    /**
     * 编写一个测试方法-看看是否能够连通到指定的Redis
     */

    @Test
    public void testRedis() {

        Jedis jedis = new Jedis("192.168.198.135", 6379);
        //jedis.auth("foobared");//如果需要认证, 就使用auth
        System.out.println(jedis.ping());
        jedis.close();
    }

    /**
     * 秒杀过程/方法
     */

    /**
     * @param uid      用户id - 在后台生成
     * @param ticketNo 票的编号, 比如北京-成都的ticketNo 就是bj_cd
     * @return
     */
    public static boolean doSecKill(String uid, String ticketNo) {

        //- uid 和 ticketNo进行非空校验
        if (uid == null || ticketNo == null) {
            return false;
        }
        //- 连接到Redis, 得到jedis对象
        //Jedis jedis = new Jedis("192.168.198.135", 6379);

        //- 通过连接池获取到jedis对象/连接
        JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
        Jedis jedis = jedisPoolInstance.getResource();
        System.out.println("---使用的连接池技术----");

        //- 拼接票的库存key
        String stockKey = "sk:" + ticketNo + ":ticket";

        //- 拼接秒杀用户要存放到的set集合对应的key,这个set集合可以存放多个userId
        String userKey = "sk:" + ticketNo + ":user";

        //监控库存
        jedis.watch(stockKey);

        //- 获取到对应的票的库存, 判断是否为null
        String stock = jedis.get(stockKey);
        if (stock == null) {
            System.out.println("秒杀还没有开始, 请等待..");
            jedis.close(); //如果jedis是从连接池获取的,则这里的close就是将jedis对象/连接释放到连接池
            return false;
        }

        //- 判断用户是否重复秒杀/复购
        if (jedis.sismember(userKey, uid)) {
            System.out.println(uid + " 不能重复秒杀...");
            jedis.close();
            return false;
        }

        //- 判断火车票,是否还有剩余
        if (Integer.parseInt(stock) <= 0) {
            System.out.println("票已经卖完了, 秒杀结束..");
            jedis.close();
            return false;
        }

        ////- 可以购买
        ////1. 将票的库存量-1
        //jedis.decr(stockKey);
        ////2. 将该用户加入到抢购成功对应的set集合中
        //jedis.sadd(userKey, uid);

        //使用事务,完成秒杀
        Transaction multi = jedis.multi();

        //组队操作
        multi.decr(stockKey);//减去票的库存
        multi.sadd(userKey, uid);//将该用户加入到抢购成功对应的set集合中

        //执行
        List<Object> results = multi.exec();

        if(results == null || results.size() == 0) {
            System.out.println("抢票失败...");
            jedis.close();
            return false;
        }

        System.out.println(uid + " 秒杀成功..");
        jedis.close();
        return true;

    }
}

3、完成测试

  1. 重启Tomcat
  2. 重置Redis 相关数据

带你详细了解Redis事务锁机制-加实列演示-加连接池-包括解决遗留问题-下

  1. 执行指令
ab -n 1000 -c 100 -p ~/postfile -T application/x-www-form-urlencoded http://192.168.98.1:8080/seckill/secKillServlet

带你详细了解Redis事务锁机制-加实列演示-加连接池-包括解决遗留问题-下

带你详细了解Redis事务锁机制-加实列演示-加连接池-包括解决遗留问题-下

版本4:抢票并发模拟,出现库存遗留问题

1、先重置一下redis 的数据 带你详细了解Redis事务锁机制-加实列演示-加连接池-包括解决遗留问题-下

解读:

  1. 这里我们把库存量设的较大, 为600
  1. 执行指令
ab -n 1000 -c 300 -p ~/postfile -T application/x-www-form-urlencoded http://192.168.98.1:8080/seckill/secKillServlet

解读:

  1. 这里我们并发数变大-c 300
  2. 执行结果

带你详细了解Redis事务锁机制-加实列演示-加连接池-包括解决遗留问题-下

  1. 可以看到, 剩余票数为543, 并不是0

  2. 出现库存遗留问题的分析

带你详细了解Redis事务锁机制-加实列演示-加连接池-包括解决遗留问题-下

LUA 脚本

LUA 介绍

1、Lua 是一个小巧的脚本语言,Lua 脚本可以很容易的被C/C++ 代码调用,也可以反过来调用C/C++的函数,Lua 并没有提供强大的库,一个完整的Lua 解释器不过200k,所以Lua 不适合作为开发独立应用程序的语言,而是作为嵌入式脚本语言。

2、很多应用程序、游戏使用LUA 作为自己的嵌入式脚本语言,以此来实现可配置性、可扩展性。

3、将复杂的或者多步的Redis 操作,写为一个脚本,一次提交给redis 执行,减少反复连接redis 的次数。提升性能。

4、LUA 脚本是类似Redis 事务,有一定的原子性,不会被其他命令插队,可以完成一些redis 事务性的操作

5、Redis 的lua 脚本功能,只有在Redis 2.6 以上的版本才可以使用

6、通过lua 脚本解决争抢问题,实际上是Redis 利用其单线程的特性,用任务队列的方式解决多任务并发问题

LUA 脚本, 解决库存遗留-思路分析图

带你详细了解Redis事务锁机制-加实列演示-加连接池-包括解决遗留问题-下

对上图解读

  1. LUA 脚本是类似Redis 事务,有一定的原子性,不会被其他命令插队,能完成Redis事务性的操作
  2. 通过lua 脚本解决争抢问题,Redis 利用其单线程的特性,将请求形成任务队列, 从而解决多任务并发问题

LUA 脚本, 解决库存遗留-代码实现

1、编写lua 脚本文件

local userid=KEYS[1]; -- 获取传入的第一个参数
local ticketno=KEYS[2]; -- 获取传入的第二个参数
local stockKey='sk:'..ticketno..:ticket; -- 拼接stockKey
local usersKey='sk:'..ticketno..:user; -- 拼接usersKey
local userExists=redis.call(sismember,usersKey,userid); -- 查看在redis 的
usersKey set 中是否有该用户
if tonumber(userExists)==1 then
	return 2; -- 如果该用户已经购买, 返回2
end
	local num= redis.call("get" ,stockKey); -- 获取剩余票数
	if tonumber(num)<=0 then
	return 0; -- 如果已经没有票, 返回0
else
	redis.call("decr",stockKey); -- 将剩余票数-1
	redis.call("sadd",usersKey,userid); -- 将抢到票的用户加入set
end
	return 1 -- 返回1 表示抢票成功

-- 参考文档: blog.csdn.net/qq_41286942…

2 、创建SecKillRedisByLua

\src\com\seckill\redis\SecKillRedisByLua.java

public class SecKillRedisByLua {

    /**
     * 说明
     * 1. 这个脚本字符串是在lua脚本上修改的, 但是要注意不完全是字符串处理
     * 2. 比如 : 这里我就使用了 \" , 还有换行使用了 \r\n
     * 3. 这些都是细节,如果你直接把lua脚本粘贴过来,不好使,一定要注意细节
     * 4. 如果写的不成功,就在这个代码上修改即可
     */
    static String secKillScript = "local userid=KEYS[1];\r\n" +
            "local ticketno=KEYS[2];\r\n" +
            "local stockKey='sk:'..ticketno..\":ticket\";\r\n" +
            "local usersKey='sk:'..ticketno..\":user\";\r\n" +
            "local userExists=redis.call(\"sismember\",usersKey,userid);\r\n" +
            "if tonumber(userExists)==1 then \r\n" +
            "   return 2;\r\n" +
            "end\r\n" +
            "local num= redis.call(\"get\" ,stockKey);\r\n" +
            "if tonumber(num)<=0 then \r\n" +
            "   return 0;\r\n" +
            "else \r\n" +
            "   redis.call(\"decr\",stockKey);\r\n" +
            "   redis.call(\"sadd\",usersKey,userid);\r\n" +
            "end\r\n" +
            "return 1";

    //使用lua脚本完成秒杀的核心方法
    public static boolean doSecKill(String uid,String ticketNo) {
        //先从redis连接池,获取连接
        JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
        Jedis jedis = jedisPoolInstance.getResource();
        //就是将lua脚本进行加载
        String sha1 = jedis.scriptLoad(secKillScript);
        //evalsha是根据指定的 sha1校验码, 执行缓存在服务器的脚本
        Object result = jedis.evalsha(sha1, 2, uid, ticketNo);
        String resString = String.valueOf(result);

        //根据lua脚本执行返回的结果,做相应的处理
        if("0".equals(resString)) {
            System.out.println("票已经卖光了..");
            jedis.close();
            return false;
        }

        if("2".equals(resString)) {
            System.out.println("不能重复购买..");
            jedis.close();
            return false;
        }

        if("1".equals(resString)) {
            System.out.println("抢购成功");
            jedis.close();
            return true;
        } else {
            System.out.println("购票失败..");
            jedis.close();
            return false;
        }
    }
}

3 、修改SecKillServlet

src\com\seckill\web\SecKillServlet.java

public class SecKillServlet extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //1. 请求时,模拟生成一个userId
        String userId = new Random().nextInt(10000) + "";
        //2. 获取用户要购买的票的编号
        String ticketNo = request.getParameter("ticketNo");

        //3. 调用秒杀的方法
        //boolean isOk = SecKillRedis.doSecKill(userId, ticketNo);

        //4. 调用lua脚本完成秒杀方法
        boolean isOk = SecKillRedisByLua.doSecKill(userId, ticketNo);

        //4. 将结果返回给前端-这个地方可以根据业务需要调整
        response.getWriter().print(isOk);
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doPost(request, response);
    }
}