前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >基于数据库的分布式锁实现

基于数据库的分布式锁实现

作者头像
用户6055494
发布2019-10-24 03:31:16
8010
发布2019-10-24 03:31:16
举报
文章被收录于专栏:AVAJAVAJ

续上次用nginx搭建好反向代理负载均衡的俩个实例后,我在项目中关联了如下这张表:

然后写了个接口,去买一个表里的商品,大致思路就是:读取库存,库存减一,回写数据库,返回成功。在单个实例里面加个synchronized后完全正常的减库存,然后当我启动俩个实例后多次疯狂调用接口后出现如下情况:

出现问题的代码如下,其主要原因是俩个实例如果在查询库存时候同时查询数据库的话,由于减库存后没来得及更新,俩次查询的结果是一致的,从而更新库存的值也是一直的,导致卖出俩个商品而只减了一次库存,出现了超卖的情况!

代码语言:javascript
复制
public class StockServiceImpl implements StockService{

    @Autowired
    StockMapper stockMapper;

    @Override
    public Stock selectByPrimaryKey(Integer goodsId) {
        return stockMapper.selectByPrimaryKey(goodsId);
    }
    // 加锁也只能保证单个实例线程安全性
    public synchronized void byGoods() throws InterruptedException {
        // 这里写死,数据库里就一条记录且ID为1,拿到数据
        Stock stock = selectByPrimaryKey(1);
        // 获取到商品的库存
        Long goodsStock = stock.getGoodsStock();
        // 减库存
        goodsStock -= 1;
        stock.setGoodsStock(goodsStock);
        // 为了将问题放大这里睡上几秒 拉长查库存和更新库存的之间的时间间隔
        Thread.sleep(3000);
        // 更新
        updateByPrimaryKeySelective(stock);
        // 输出
        System.out.println("更新后库存为:" + goodsStock);
    }

    @Override
    public int updateByPrimaryKeySelective(Stock record) {
        return stockMapper.updateByPrimaryKeySelective(record);
    }
}

接下来进行改造,使用数据库层面的“锁”,我们知道向一张表中出入俩条相同主键的数据,只可能成功一条,因为主键具有约束性,所以利用这个特点,当我们向数据库插入成功时,即代表获取到锁,从而去运行我们的业务代码,当我们的业务代码运行完时,我们把数据库的该条记录进行删除,即代表释放锁,从而其他线程即有机会获取到锁,再去跑业务代码,这样即使运行的是俩个实例,同一时间也只能一个线程去运行业务代码,也就不会出现超卖这种情况了。下面给出加锁和解锁的代码:

代码语言:javascript
复制
// 上锁,由于上锁失败的话会直接返回失败,并不会再次
 // 获取,是非阻塞的,这里利用循环实现阻塞。
    @Override
    public boolean tryLock() {
        Lock lock = new Lock();
        lock.setLockId(1);
        while (true) {
            try {
                if (lockMapper.selectByPrimaryKey(1) == null) {
                    int i = lockMapper.insert(lock);
                    if (i == 1) {
                    return true;
                    }
                }
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
        }
    }
    // 解锁代码
    @Override
    public void unLock() {
        deleteByPrimaryKey(1);
    }

对service层的购买商品的代码就行加锁:

代码语言:javascript
复制
    // 买商品
    public void byWithLock() throws InterruptedException {
        // 上锁
       lockService.tryLock();
       // 业务代码
       byGoods();
       // 释放锁并跳出循环
       lockService.unLock();
    }

controller层的代码:

代码语言:javascript
复制
@RestController
public class LoadBalance {
    @Autowired
    StockServiceImpl stockService;
    @RequestMapping("/balance")
    public String balance() {
        try {
            stockService.byWithLock();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "success";
    }
}

再次跑代码,疯狂点接口后俩个实例控制台输出满意结果如下:

总结:

1、通过insert插入数据库是非阻塞的,这里采用while循环“不优雅”的实现了阻塞。数据库自己也可以使用“for update”来实现阻塞。

2、这种数据库层面的锁其实是很粗糙的,非常依赖于数据库,如果数据库宕机,那么是没有办法再使用锁的。

3、如果有线程加锁后运行业务代码时出现如数据库断电恢复,或者请求超时等导致数据库的锁没能解锁,这时我们可以给每个锁一个时间,定时清理超时的锁。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-10-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序员面试鸭 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
数据库
云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档