分布式锁之数据库实现

分布式锁之数据库实现

什么是分布式锁

在单实例单进程的系统中,当有多个线程同时修改某个共享变量时,为了保证线程安全,就需要对变量或者代码做同步处理,这种同步操作在java中可以使用synchronized、JUC包下的显式锁、cas+volatile来实现。

而目前大部分系统都是分布式部署的,使用synchronized等手动只能保证单个进程内的线程安全,多个进程多个实例下的线程安全就需要分布式锁来实现。

目前主流的分布式锁解决方案有四种:

  • 基于数据库实现(悲观+乐观)
  • 基于Redis实现
  • 基于ZooKeeper实现
  • 基于Etcd实现

数据库之悲观锁实现分布式锁

建表sql:

CREATE TABLE `t_order` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `amount` int(11) NOT NULL,
  `status` int(11) NOT NULL,
  `version` int(11) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

INSERT INTO `t_order` (`amount`, `status`, `version`) VALUES ('100', '1', '1');

先借助sql来模拟分布锁的实现:

步骤SessionASessionB
1begin;begin;
2select * from t_order where id=1 for update;
3select * from t_order where id=1 for update; – 阻塞
4update t_order set status=2 where id=1 and status=1;
5commit;返回查询结果
6update t_order set status=2 where id=1 and status=1; – 状态变了未更新成功
7commit;

说明:

  1. 客户端A和客户端B同时执行前面两行sql,客户端A返回数据,而客户端B阻塞等待获取行锁。
  2. 客户端A执行后面两行sql,提交事务,客户端B获得行锁,立刻返回数据。
  3. 客户端B执行后面两行sql,提交事务,释放行锁。

注意for update语句一定要走主键索引,否则没走索引会锁住整个表,走了其他索引会产生间隙锁,可能会锁住多条记录。

Java代码实现:

package com.morris.distribute.lock.database.exclusive;

import com.morris.distribute.entity.Order;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.concurrent.TimeUnit;

@Service
@Slf4j
public class OrderService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    /**
     * 数据库分布式锁之悲观锁
     *
     * @param id
     */
    @Transactional
    public void updateStatus(int id) {
        log.info("updateStatus begin, {}", id);

        Integer status = jdbcTemplate.queryForObject("select status from t_order where id=? for update", new Object[]{id}, Integer.class);

        if (Order.ORDER_STATUS_NOT_PAY == status) {

            try {
                // 模拟耗时操作
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            int update = jdbcTemplate.update("update t_order set status=? where id=? and status=1", new Object[]{2, id, Order.ORDER_STATUS_NOT_PAY});

            if (update > 0) {
                log.info("updateStatus success, {}", id);
            } else {
                log.info("updateStatus failed, {}", id);
            }
        } else {
            log.info("updateStatus status already updated, ignore this request, {}", id);
        }
        log.info("updateStatus end, {}", id);
    }
}

注意开启事务@Transactional

使用多个线程模拟竞争锁:

package com.morris.distribute.lock.database.exclusive;

import com.morris.distribute.config.JdbcConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.stream.IntStream;

public class Demo {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
        applicationContext.register(JdbcConfig.class);
        applicationContext.register(OrderService.class);
        applicationContext.refresh();

        CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
        IntStream.rangeClosed(1, 3).forEach((i) -> new Thread(() -> {
            OrderService orderService = applicationContext.getBean(OrderService.class);
            try {
                cyclicBarrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
            orderService.updateStatus(1);
        }, "t" + i).start());
    }
}

运行结果如下:

2020-09-16 14:16:53,248  INFO [t2] (OrderService.java:26) - updateStatus begin, 1
2020-09-16 14:16:53,248  INFO [t1] (OrderService.java:26) - updateStatus begin, 1
2020-09-16 14:16:53,248  INFO [t3] (OrderService.java:26) - updateStatus begin, 1
2020-09-16 14:16:56,289  INFO [t2] (OrderService.java:42) - updateStatus success, 1
2020-09-16 14:16:56,289  INFO [t2] (OrderService.java:49) - updateStatus end, 1
2020-09-16 14:16:56,290  INFO [t3] (OrderService.java:47) - updateStatus status already updated, ignore this request, 1
2020-09-16 14:16:56,290  INFO [t3] (OrderService.java:49) - updateStatus end, 1
2020-09-16 14:16:56,291  INFO [t1] (OrderService.java:47) - updateStatus status already updated, ignore this request, 1
2020-09-16 14:16:56,291  INFO [t1] (OrderService.java:49) - updateStatus end, 1

从运行结果可以看出,同一时间只有一个线程持有了锁。

数据库之乐观锁实现分布式锁

乐观锁每次通过版本号来判断记录是否被更新过。

package com.morris.distribute.lock.database.share;

import com.morris.distribute.entity.Order;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
@Slf4j
public class OrderService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    /**
     * 数据库分布式锁之乐观锁
     *
     * @param id
     */
    public void updateStatus(int id) {

        log.info("updateStatus begin, {}", id);
        for (;;) { // 自旋,有可能对订单做其他操作,导致version变了,所以需要自旋
            Order order = jdbcTemplate.queryForObject("select status, version from t_order where id=?",
                    new Object[]{id}, (rs, row) -> {
                        Order o = new Order();
                        o.setStatus(rs.getInt(1));
                        o.setVersion(rs.getInt(2));
                        return o;
                    });

            if (Order.ORDER_STATUS_NOT_PAY == order.getStatus()) {

                try {
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                int update = jdbcTemplate.update("update t_order set status=?,version=? where id=? and version=? and status=?",
                        new Object[]{Order.ORDER_STATUS_PAY_SUCCESS, order.getVersion() + 1, id, order.getVersion(), Order.ORDER_STATUS_NOT_PAY});

                if (update > 0) {
                    log.info("updateStatus success, {}", id);
                    break;
                } else {
                    log.info("updateStatus failed, {}", id);
                }
            } else {
                log.info("updateStatus status already updated, ignore this request, {}", id);
                break;
            }
        }
        log.info("updateStatus end, {}", id);
    }

}

运行结果如下:

2020-09-16 14:21:08,934  INFO [t3] (OrderService.java:25) - updateStatus begin, 1
2020-09-16 14:21:08,934  INFO [t2] (OrderService.java:25) - updateStatus begin, 1
2020-09-16 14:21:08,934  INFO [t1] (OrderService.java:25) - updateStatus begin, 1
2020-09-16 14:21:12,110  INFO [t1] (OrderService.java:50) - updateStatus failed, 1
2020-09-16 14:21:12,110  INFO [t2] (OrderService.java:50) - updateStatus failed, 1
2020-09-16 14:21:12,111  INFO [t3] (OrderService.java:47) - updateStatus success, 1
2020-09-16 14:21:12,111  INFO [t3] (OrderService.java:57) - updateStatus end, 1
2020-09-16 14:21:12,117  INFO [t2] (OrderService.java:53) - updateStatus status already updated, ignore this request, 1
2020-09-16 14:21:12,117  INFO [t1] (OrderService.java:53) - updateStatus status already updated, ignore this request, 1
2020-09-16 14:21:12,117  INFO [t1] (OrderService.java:57) - updateStatus end, 1
2020-09-16 14:21:12,117  INFO [t2] (OrderService.java:57) - updateStatus end, 1

总结

悲观锁:会锁住整行记录,导致对数据的其他业务操作也无法进行,效率低,如果sql没写好,可能会产生间隙锁,锁住多条记录,甚至锁住全表。

乐观锁:每个表都需要增加与业务无关的version字段。

优点:直接基于数据库实现,实现简单。

缺点:IO开销大,连接数有限,无法满足高并发的需求。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

morris131

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值