java锁乐观锁Redis锁

乐观锁

MySQL的默认存储引擎是InnoDB,默认行锁。所以在update的时候会在对同一条记录加锁。虽然存在两个线程的update的事务未提交,但是innodb的update操作是采用当前读的方式。假设线程A开启事务A并执行update操作后,但未提交事务;此时线程B也开启事务B并update操作。此时线程B的update是基于当前读的方式获取到线程A更改后的结果。如果事务A在此时想提交事务,这是不允许的。根据两阶段锁协议,事务B没提交,因为此时在这条记录上的写锁还没有释放。所以必须等待事务B提交后释放锁。

超卖
事务只有写锁,并发的时候多个线程都可以读到有足够的库存 比如大家都读取有4件库存
现在用户并发的购买一件商品时
因为事务只是写锁,线程门排队一个个进行写数据,每个线程都将库存数据更新为3 (没错,第一个人将4改成3, 第二个人将3改成3, 第三个人将3改成3,以此类推。 然后一件商品被卖了上万件) // 秒杀商品亏死你
你要明白,读和写是两个操作。 事务只有写锁,没有读锁

悲观锁
update语句后面接for update 这个是读写锁。
// 第一个线程读取到后,不允许别人在去读。 只有第一个线程完成事务后,第二个线程才能读
//比如你在下单时,因为是读锁,锁住商品不让别人去读,然后其他人在首页刷新商品时就刷新不出来了,因为库存被你读锁锁住了,他们读取不到库存。 你一个人成功的把整个网站给堵住了。 超级厉害。 性能超低,所以也不推荐使用

乐关锁
其原理就是利用事务本身就是写锁来实现的

我在这里举例一个金额扣减的例子,你理解这个案例了,那么对于库存的扣减也就理解了

开启两个事务,在这两个事务中同时对数据库表的同一行数据进行更新,则第一个事务的update语句先执行,而第二个事务的update语句后执行,那么要等到第一个事务提交之后,第二个事务的更新语句才能从阻塞状态变成执行状态。
在这里插入图片描述
在上面的图中,左边的事务执行完余额更新操作后commit之后,虽然右边的事务中执行的查询操作,每次都是查询都是20,(当前的事务隔离级别为可重复读)但是执行更新操作后变成了-20,此时我们应该添加乐观锁*,且不能通过java代码进行判断,直接更新某个变量为绝对值,而应该是相对值。

update account set balance=balance-20 where id =1; // 需要添加乐观锁
update account set balance=balabce-20 where id =1 and balance>0;

// 不能在java代码中更具select取出的值为20,然后通过if判断20>0就,简单的调用update字段语句
if(balance>0){
remain = balance-20
executeSql(“update account set balance = remain where id =1 and balance>0”)
因为隔离级别为可重复读,此时读取的balance都是进入事务之前的值,却不是最新的,所以我们的sql语句使用的变量应该是由数据库控制的变量如balance = balance-20,而不是通过java代码读取的balance进行计算,通过数据库的变量能够进行更新,如上面最后出现的-20;
}

@Transactional(propagation = Propagation.REQUIRED)
    @Override
    public void decreaseItemSpecStock(String specId, int buyCounts) {

        // synchronized 不推荐使用,集群下无用,性能低下
        // 锁数据库: 不推荐,导致数据库性能低下
        // 分布式锁 zookeeper redis

        // lockUtil.getLock(); -- 加锁

        // 1. 查询库存
//        int stock = 10;

        // 2. 判断库存,是否能够减少到0以下
//        if (stock - buyCounts < 0) {
        // 提示用户库存不够
//            10 - 3 -3 - 5 = -1
//        }

        // lockUtil.unLock(); -- 解锁


        int result = itemsMapperCustom.decreaseItemSpecStock(specId, buyCounts);
        if (result != 1) {
            throw new RuntimeException("订单创建失败,原因:库存不足!");
        }
    }
<!--乐观锁-->
<update id="decreaseItemSpecStock">

        update
            items_spec
        set
            stock = stock - #{pendingCounts}
        where
            id = #{specId}
        and
            stock >= #{pendingCounts}

    </update>

redis锁

@Component
@Slf4j
public class RedisLock {
    /**
     * 解锁脚本,原子操作
     */
    private static final String UNLOCKSCRIPT =
            "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n"
                    + "then\n"
                    + "    return redis.call(\"del\",KEYS[1])\n"
                    + "else\n"
                    + "    return 0\n"
                    + "end";   //NOSONAR

    private StringRedisTemplate redisTemplate;

    public RedisLock(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }


    /**
     * 加锁,有阻塞
     * @param key key
     * @param expire 过期时间
     * @return
     */
    public String lockNoSave(String key, long expire){
        String token = tryLock(key, expire);
        return token;
    }

    /**
     * 加锁,有阻塞
     * @param key key
     * @param expire 过期时间
     * @param timeout  释放时间
     * @return
     */
    public String lock(String key, long expire, long timeout){
        long startTime = System.currentTimeMillis();
        String token;
        do{
            token = tryLock(key, expire);
            if(token == null) {
                if((System.currentTimeMillis()-startTime) > (timeout - 50)) {
                    break;
                }
                try {
                    //try 50 per sec
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    log.error("redis lock error", e);
                    Thread.currentThread().interrupt();
                    return null;
                }
            }
        }while(token==null);

        return token;
    }

    /**
     * 加锁,无阻塞
     * @param key key
     * @param expire 过期时间
     * @return
     */
    public String tryLock(String key, long expire) {
        String token = UUID.randomUUID().toString();
        RedisConnectionFactory factory = redisTemplate.getConnectionFactory();
        assert factory != null;
        RedisConnection conn = factory.getConnection();
        try{
            Boolean result = conn.set(key.getBytes(StandardCharsets.UTF_8), token.getBytes(StandardCharsets.UTF_8),
                    Expiration.from(expire, TimeUnit.MILLISECONDS), RedisStringCommands.SetOption.SET_IF_ABSENT);
            if(result!=null && result) {
                return token;
            }
        }finally {
            RedisConnectionUtils.releaseConnection(conn, factory,false);
        }
        return null;
    }

    /**
     * 解锁
     * @param key key
     * @param token token
     * @return
     */
    public boolean unlock(String key, String token) {
        byte[][] keysAndArgs = new byte[2][];
        keysAndArgs[0] = key.getBytes(StandardCharsets.UTF_8);
        keysAndArgs[1] = token.getBytes(StandardCharsets.UTF_8);
        RedisConnectionFactory factory = redisTemplate.getConnectionFactory();
        assert factory != null;
        RedisConnection conn = factory.getConnection();
        try {
            Long result = (Long)conn.scriptingCommands().eval(UNLOCKSCRIPT.getBytes(StandardCharsets.UTF_8), ReturnType.INTEGER, 1, keysAndArgs);
            if(result!=null && result>0) {
                return true;
            }
        }finally {
            RedisConnectionUtils.releaseConnection(conn,factory,false);
        }

        return false;
    }
}

使用Redis锁

try{
    token = redisLock.lockNoSave(id.toString(),15000);
    if(token == null){
        throw new ApplicationException("添加中,无需重复添加!");
    }

    //可以卖东西
    //可以更新数据
    //可以保存需要保存的数据
}finally {
    if(token != null) {
        redisLock.unlock(aiAlgorithmResultsId.toString(), token);
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值