Redis事务详解与秒杀超卖案例(lua脚本保证原子性)

Redis6事务

Multi

命令并未执行,而是进入排队阶段

当排队的命令存在错误,则执行阶段所有命令均执行失败

Exec

执行Multi队列中的命令

当执行过程中命令存在问题,只有存在问题的命令执行失败,其余命令执行成功

Discard

放弃执行Multi命令

// 排队
multi
// 加入命令
set k1 v1
set k2 v2
// 执行
exec
// 放弃执行
discard

事务冲突

悲观锁:每次获取资源都先上锁,禁止其他事务获取资源,关系型数据库的行锁、表锁、读锁、写锁都是悲观锁

乐观锁:获取资源时记录版本号,执行操作之前先判断版本号是否发生改变,发生改变则不执行,避免了ABA问题,因为是通过版本号记录资源状态,而非根据资源的值判断资源是否发生改变。

拓展:java中的 CAS(compare and swap)是对了乐观锁的一种实现,CAS是通过比较内存中的一个数据是否是预期值,如果是就将它修改成新值,如果不是则进行自旋,重复比较的操作,但是CAS存在ABA问题,ABA问题的根本在于cas在修改变量的时候,无法记录变量的状态,比如修改的次数,否修改过这个变量。这样就很容易在一个线程将A修改成B时,另一个线程又会把B修改成A,造成cas多次执行的问题。

Watch

作用就是监听版本号

unwatch 取消监听

案例:

下面是两个并发执行的客户端,其中只有一个客户端能够执行成功,因为两个客户端都监听到了k1相同的版本号,但是当其中一个客户端执行完操作后,k1的版本号发生改变,另一个客户端执行前,监听到k1的版本号发生改变,则执行失败

客户端1

watch k1
multi
incr k1
exec

客户端2

watch k1
multi
incr k1
exec

事务三特性

单独的隔离操作
事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断
没有隔离级别的概念
队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行
不保证原子性
事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚

秒杀简单案例

单线程安全,但是存在并发超卖

package service;

import redis.clients.jedis.Jedis;
public class SeckillService {
    public static boolean doSecKill(String uid,String prodid) {
        if(null == uid || null == prodid){
            return false;
        }

        Jedis jedis = new Jedis("101.43.146.65", 6379);

        String productCountStr = prodid+"count";
        String productUserStr = prodid+"user";
        String productCount = jedis.get(productCountStr);

        if(null == productCount) {
            System.out.println("秒杀还没有开始");
            jedis.close();
            return false;
        }

        if(jedis.sismember(productUserStr, uid)) {
            System.out.println(uid + "用户已经秒杀成功");
            jedis.close();
            return false;
        }

        int prodCount = Integer.parseInt(productCount);
        if(prodCount <= 0) {
            System.out.println("秒杀结束");
            jedis.close();
            return false;
        }

        jedis.decr(productCountStr);
        jedis.sadd(productUserStr, uid);
        System.out.println(uid + "秒杀成功");
        jedis.close();
        return true;
    }
}

Watch事务机制

存在存量遗留问题

JedisPollTool.java

public class JedisPollTool {
    private static JedisPool pool = null;
    /**
     *
     * 方法描述 构建redis连接池
     *
     * @return
     */
    static {
        if(null == pool) {
            synchronized (JedisPollTool.class){
                JedisPoolConfig config = new JedisPoolConfig();
                //控制一个pool可分配多少个jedis实例,通过pool.getResource()来获取;
                //如果赋值为-1,则表示不限制;如果pool已经分配了maxActive个jedis实例,则此时pool的状态为exhausted(耗尽)。
                config.setMaxTotal(50);
                //控制一个pool最多有多少个状态为idle(空闲的)的jedis实例。
                config.setMaxIdle(5);
                //表示当borrow(引入)一个jedis实例时,最大的等待时间,如果超过等待时间,则直接抛出JedisConnectionException;单位毫秒
                //小于零:阻塞不确定的时间,  默认-1
                config.setMaxWaitMillis(1000*100);
                //在borrow(引入)一个jedis实例时,是否提前进行validate操作;如果为true,则得到的jedis实例均是可用的;
                config.setTestOnBorrow(true);
                //return 一个jedis实例给pool时,是否检查连接可用性(ping())
                config.setTestOnReturn(true);
                //connectionTimeout 连接超时(默认2000ms)
                //soTimeout 响应超时(默认2000ms)
                pool = new JedisPool(config, "101.43.146.65", 6381,  2000);
            }
        }
    }
    /**
     * 方法描述 获取Jedis实例
     *
     * @return
     */
    public static JedisPool getInstance() {
        return pool;
    }
    /**
     *
     * 方法描述 释放jedis连接资源
     *
     * @param jedis
     */
    public static void release(Jedis jedis) {
        if(null != jedis) {
            jedis.close();
        }
    }
}

Seckill2Service.java

public class Seckill2Service {
    public static boolean doSecKill(String uid,String prodid) {
        JedisPool jedisPool = JedisPollTool.getInstance();
        Jedis jedis = jedisPool.getResource();
        String productCountStr = prodid+"count";
        String productUserStr = prodid+"user";
        jedis.watch(productCountStr);	//开始监视
        String productCount = jedis.get(productCountStr);

        if(null == productCount) {
            System.out.println("秒杀还没有开始");
            JedisPollTool.release(jedis);
            return false;
        }

        if(jedis.sismember(productUserStr, uid)) {
            System.out.println(uid + "用户已经秒杀成功");
            JedisPollTool.release(jedis);
            return false;
        }

        int prodCount = Integer.parseInt(productCount);
        if(prodCount <= 0) {
            System.out.println("秒杀结束");
            JedisPollTool.release(jedis);
            return false;
        }

        Transaction transaction = jedis.multi();
        transaction.decr(productCountStr);
        transaction.sadd(productUserStr, uid);
        List<Object> exec = transaction.exec();


        if(exec == null || exec.size() == 0) {
            System.out.println("秒杀失败,稍后重试");
            JedisPollTool.release(jedis);
            return false;
        }
        JedisPollTool.release(jedis);
        System.out.println(uid + "秒杀成功");
        return true;
    }
}

LUA脚本

最好用

local userid=KEYS[1]; 
local prodid=KEYS[2];
local qtkey="sk:"..prodid..":qt";
local usersKey="sk:"..prodid.":usr'; 
local userExists=redis.call("sismember",usersKey,userid);
if tonumber(userExists)==1 then 
  return 2;
end
local num= redis.call("get" ,qtkey);
if tonumber(num)<=0 then 
  return 0; 
else 
  redis.call("decr",qtkey);
  redis.call("sadd",usersKey,userid);
end
return 1;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值