Redis基本使用二(事务与锁)

开发环境:

  1. JDK11
  2. Redis3.2

Redis事务机制:

与传统的关系型数据库类似,NoSQL也存在许多并发访问的情况,因此出现了如何保证数据一致性的问题,处理的方式有很多。
针对不同的业务层次有不同的解决方案:

  1. 视图层:前端来保证数据一致性,笔者对前端技术熟悉程度还不足以搞定,暂不讨论;
  2. 业务层:可以使用线程同步来保证数据一致性;
  3. 持久层:在持久层解决数据一致性问题是最优的选择,此时有悲观锁、乐观锁等解决方案;

对于Redis而言,可以通过事务与锁保证数据一致性,首先了解下Redis事务,与关系性数据库流程类似,代码如下:
 

    /**
     * Jedis实现事务
     * @return
     */
    @GetMapping("test6")
    public Object test6()
    {
        String key = "key_1";
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        // 开启事务
        Transaction transaction = jedis.multi();
        transaction.hset(key, "a", "新恒结衣");
        transaction.hset(key, "b", "hello");
        // 提交事务
        List<Object> list = transaction.exec();
        return list;
    }

    /**
     * redisTemplate包装类实现事务
     * @return
     */
    @GetMapping("test7")
    public Object test7()
    {
        SessionCallback<String> callback = new SessionCallback<>()
        {
            @Override
            public String execute(RedisOperations operations) throws DataAccessException
            {
                operations.multi();
                operations.opsForValue().set("key_1", "hello");
                System.out.println(1 / 0);
                operations.opsForValue().set("key_2", "world");
                operations.exec();
                return "ok";
            }
        };
        Object result = null;
        try
        {
            result = redisTemplate.execute(callback);
        } catch (Exception e)
        {
            System.out.println("发生异常");
        }
        System.out.println("value1=" + redisTemplate.opsForValue().get("key_1"));
        System.out.println("value2=" + redisTemplate.opsForValue().get("key_2"));
        return result == null ? "result为空" : result;
    }

如上代码展示了Redis实现事务的两种方式,重点看test7方法,事务中出现1/0的异常,而最后执行结果为返回result为空,value1=hello,value2=null,可以发现即使发生异常,事务也会提交,但异常后的数据定义语句不会执行且事务执行结果返回null。

 

Redis锁机制:

与传统关系型数据库类似,事务操作并不能保证数据的一致性,原因也特别简单,并发量比较大的时候同一个Redis服务器可能在执行多个事务,以抢购商品为例,假设库存var=100,对于客户端来说,每次取出var都需要一次判断,看var是否大于0,如果是则执行购买业务,反之则返回抢购完毕,以var=1为例,此时3个客户端几乎同时读取var,则均返回1,那么这三个客户端都会执行购买业务,造成var=-2的超发情况,程序示例如下:

    public static void main(String[] args)
    {
        var context = new AnnotationConfigApplicationContext(RedisConfig.class);

        Jedis jedis = new Jedis("127.0.0.1", 6379);
        // 初始库存100
        jedis.set("var", "100");
        test1();
        try
        {
            // 等待测试方法执行完毕
            Thread.sleep(5000);
        }catch (Exception e)
        {
            e.printStackTrace();
        }
        System.out.println("test方法执行完毕,当前var="+jedis.get("var")); 
    }
    
    /**
     * 购买测试
     */
    private static void test1()
    {
        // 模拟200次购买请求
        for (int i = 0; i < 200; i++)
        {
            new Thread(() ->
            {
                Jedis jedis = new Jedis("127.0.0.1", 6379);
                if (Integer.parseInt(jedis.get("var")) <= 0)
                {
                    System.out.println("var已经不足");
                }else
                {
                    Transaction transaction = jedis.multi();
                    transaction.incrBy("var", -1);
                    System.out.println("成功购买");
                    transaction.exec();
                }
            }).start();
        }
    }

最后程序打印出var=-20(具体打印结果具有不确定性)。

接下来使用Redis的锁机制解决超发问题,程序如下:

    /**
     *  应用锁机制的购买测试
     */
    private static void test2()
    {
        // 模拟200次购买请求
        for (int i = 0; i < 200; i++)
        {
            new Thread(() ->
            {
                Jedis jedis = new Jedis("127.0.0.1", 6379);
                // 监控key
                jedis.watch("var");
                if (Integer.parseInt(jedis.get("var")) <= 0)
                {
                    System.out.println("var已经不足");
                    return;
                } else
                {
                    Transaction transaction = jedis.multi();
                    transaction.incrBy("var", -1);
                    List list = transaction.exec();
                    if (list.size() == 0)
                    {
                        System.out.println("事务被取消,重新购买");
                        again(jedis);
                    } else
                    {
                        System.out.println("成功购买,list=" + list);
                    }
                }
            }).start();
        }
    }

    /**
     * 重新购买方法
     * @param jedis
     */
    private static void again(Jedis jedis)
    {
        jedis.watch("var");
        if (Integer.parseInt(jedis.get("var")) <= 0)
        {
            System.out.println("var已经不足");
            return;
        }
        Transaction transaction = jedis.multi();
        transaction.incrBy("var", -1);
        List list = transaction.exec();
        if (list.size() == 0)
        {
            System.out.println("事务被取消,重新购买");
            again(jedis);
        } else
        {
            System.out.println("成功购买,list=" + list);
        }
    }

可能有些小伙伴会问为什么需要再写一个重新购买方法,原因是固定的循环200次中会有大量的购买失败(watch监控值改变引起的),如果不加重新购买方法会导致最后var值大于0,也就是明明已经200人抢购完毕,但还是有商品没卖出去。

Redis的锁机制是由watch命令控制的,它在事务开启之前去监控一个或者多个key,在所有事务命令入队执行前一刻会去查看该key是否被其他客户端修改过(注意是其他客户端,自身不算),如果没有则正常执行,如果有则事务回滚,返回list长度为0。

Redis锁机制应用流程:

  1. 定义Jedis对象;
  2. watch命令绑定监控的key;
  3. 开启事务,设置执行命令;
  4. 执行事务,并判断事务是否回滚;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值