分布式锁之mysql实现

本地jvm锁

分布式锁之mysql实现_第1张图片

 搭建本地卖票案例

package com.test.lockservice.service.impl;

import com.test.lockservice.mapper.TicketMapper;
import com.test.lockservice.service.TicketService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @Author sl
 */
@Service
public class TicketServiceImpl implements TicketService {

    @Autowired
    private TicketMapper ticketMapper;

    private  int count = 5000;

    @Override
    public void sellTicket() {
        count = count -1;
        System.out.println("count:"+ count);
    }
}

使用jmeter压测

5000个请求测试买票,查看是否出现超卖问题

分布式锁之mysql实现_第2张图片

出现了超卖问题分布式锁之mysql实现_第3张图片

本地synchronized和ReentrantLock解决本地超卖问题

package com.test.lockservice.service.impl;

import com.test.lockservice.mapper.TicketMapper;
import com.test.lockservice.service.TicketService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @Author sl
 */
@Service
public class TicketServiceImpl implements TicketService {

    @Autowired
    private TicketMapper ticketMapper;

    private  int count = 5000;
    
    @Override
    public synchronized void sellTicket() {
        count = count -1;
        System.out.println("count:"+ count);
    }
}

或者使用ReentrantLock

package com.test.lockservice.service.impl;

import com.test.lockservice.mapper.TicketMapper;
import com.test.lockservice.service.TicketService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @Author sl
 */
@Service
public class TicketServiceImpl implements TicketService {

    @Autowired
    private TicketMapper ticketMapper;

    private  int count = 5000;

    private ReentrantLock lock = new ReentrantLock();
    @Override
    public  void sellTicket() {
        lock.lock();
        try {
            count = count -1;
            System.out.println("count:"+ count);
        }finally {
            lock.unlock();
        }
    }
}

jmeter压测结果显示,5000总票数,压测5000,都能够解决超卖的现象 

分布式锁之mysql实现_第4张图片

将共享资源放入mysql

分布式锁之mysql实现_第5张图片

 查库操作,演示超卖现象

package com.test.lockservice.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.test.lockservice.mapper.TicketMapper;
import com.test.lockservice.model.Ticket;
import com.test.lockservice.service.TicketService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @Author sl
 */
@Service
public class TicketServiceImpl implements TicketService {

    @Autowired
    private TicketMapper ticketMapper;

//    private ReentrantLock lock = new ReentrantLock();
    @Override
    public  void sellTicket() {
//        lock.lock();
        try {
            // 查询票数
            Ticket ticket = ticketMapper.selectOne(new QueryWrapper().eq("sell_company", "12306"));
            // 判断不为空和票数大于0
            if(ticket!=null&& ticket.getCount() > 0){
                ticket.setCount(ticket.getCount()-1);
                ticketMapper.updateById(ticket);
            }
        }finally {
//            lock.unlock();
        }
    }
}

5000总票数,压测5000,压测结果,显示超卖 

分布式锁之mysql实现_第6张图片

加锁,本地锁解决超卖现象

package com.test.lockservice.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.test.lockservice.mapper.TicketMapper;
import com.test.lockservice.model.Ticket;
import com.test.lockservice.service.TicketService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Service;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @Author sl
 */
@Service
public class TicketServiceImpl implements TicketService {

    @Autowired
    private TicketMapper ticketMapper;

    private ReentrantLock lock = new ReentrantLock();
    @Override
    public  void sellTicket() {
        lock.lock();
        try {
            // 查询票数
            Ticket ticket = ticketMapper.selectOne(new QueryWrapper().eq("sell_company", "12306"));
            // 判断不为空和票数大于0
            if(ticket!=null&& ticket.getCount() > 0){
                ticket.setCount(ticket.getCount()-1);
                ticketMapper.updateById(ticket);
            }
        }finally {
            lock.unlock();
        }
    }
}

5000总票数,压测5000,压测结果显示,可以解决超卖现象 

分布式锁之mysql实现_第7张图片

本地jvm锁失效的三种情况

1多例模式失效

2事务失效(@Transactional)

3集群部署失效(相当于多例模式,只不过是多个节点)

多例模式失效

@Scope(scopeName="prototype",proxyMode = ScopedProxyMode.TARGET_CLASS)

package com.test.lockservice.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.test.lockservice.mapper.TicketMapper;
import com.test.lockservice.model.Ticket;
import com.test.lockservice.service.TicketService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @Author sl
 */
@Service
@Scope(scopeName="prototype",proxyMode = ScopedProxyMode.TARGET_CLASS)
public class TicketServiceImpl implements TicketService {

    @Autowired
    private TicketMapper ticketMapper;

    private ReentrantLock lock = new ReentrantLock();
    @Override
    public  void sellTicket() {
        lock.lock();
        try {
            // 查询票数
            Ticket ticket = ticketMapper.selectOne(new QueryWrapper().eq("sell_company", "12306"));
            // 判断不为空和票数大于0
            if(ticket!=null&& ticket.getCount() > 0){
                ticket.setCount(ticket.getCount()-1);
                ticketMapper.updateById(ticket);
            }
        }finally {
            lock.unlock();
        }
    }
}

5000总票数,压测1000,压测显示超卖现象

分布式锁之mysql实现_第8张图片事务失效

@Transactional注解是aop开启的手动事务,代表一组操作,要么都成功,要么都失败,在代码中,释放锁过后,如果当前事务还未提交,其他线程获得了锁,在可重复读的隔离级别之下,会出现重复售卖的问题

a用户 b用户
begin开启事务 begin开启事务
获取锁
查询票数5000
扣减票数4999
释放锁
得到锁
查询票数5000
扣减票数4999
package com.test.lockservice.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.test.lockservice.mapper.TicketMapper;
import com.test.lockservice.model.Ticket;
import com.test.lockservice.service.TicketService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @Author sl
 */
@Service
@Transactional
public class TicketServiceImpl implements TicketService {

    @Autowired
    private TicketMapper ticketMapper;

    private ReentrantLock lock = new ReentrantLock();
    @Override
    public  void sellTicket() {
        lock.lock();
        try {
            // 查询票数
            Ticket ticket = ticketMapper.selectOne(new QueryWrapper().eq("sell_company", "12306"));
            // 判断不为空和票数大于0
            if(ticket!=null&& ticket.getCount() > 0){
                ticket.setCount(ticket.getCount()-1);
                ticketMapper.updateById(ticket);
            }
        }finally {
            lock.unlock();
        }
    }
}

5000总票数,压测1000,压测显示超卖现象

分布式锁之mysql实现_第9张图片

集群部署失效

下载nginx

http://nginx.org/en/download.html

配置nginx.conf


    upstream test{
        server localhost:10010;
        server localhost:10086;
    }
    server {
        listen       80;
        server_name  localhost;

        location / {
            proxy_pass http://test;
        }

    }

启动两个程序

复制服务 -Dserver.port = 10086

分布式锁之mysql实现_第10张图片

修改压测地址

分布式锁之mysql实现_第11张图片

 5000总票数,压测1000,压测显示超卖现象

分布式锁之mysql实现_第12张图片

一条sql语句解决本地锁三种失效情况

优化所有操作为一条语句,因为数据库增删改自动加锁,保证了原子性问题

缺点

  • 注意下锁的范围(当更新条件或者查询条件没命中索引时,是表锁,命中索引为行锁)
  • 同一票数在多个售票点存在售卖记录
  • 无法记录票数变化前后的数据

修改mapper

package com.test.lockservice.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.test.lockservice.model.Ticket;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
import org.springframework.stereotype.Repository;

/**
 * @Author sl
 */
@Mapper
@Repository
public interface TicketMapper extends BaseMapper {

    @Update("update ticket set count = count - #{count} where sell_company=#{company} and count> 1")
    void updateByCompany(@Param("company") String company ,@Param("count") Integer count);
}

修改service

package com.test.lockservice.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.test.lockservice.mapper.TicketMapper;
import com.test.lockservice.model.Ticket;
import com.test.lockservice.service.TicketService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @Author sl
 */
@Service
public class TicketServiceImpl implements TicketService {

    @Autowired
    private TicketMapper ticketMapper;


    @Override
    public  void sellTicket() {

        ticketMapper.updateByCompany("12306",1);
    }
}

压测测试 

 5000总票数,压测1000,压测无超卖现象

分布式锁之mysql实现_第13张图片

mysql悲观锁解决失效问题

select ... for update,为语句加锁,解决失效问题

注意使用行级锁:

  • 锁的查询和更新条件必须是索引字段
  • 查询或者更新条件必须为具体值
  • 注意添加事务注解

修改mapper

package com.test.lockservice.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.test.lockservice.model.Ticket;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import org.springframework.stereotype.Repository;

import java.util.List;

/**
 * @Author sl
 */
@Mapper
@Repository
public interface TicketMapper extends BaseMapper {


    @Select("select * from ticket where sell_company='12306' for update")
    List findList();
}

修改service添加事务

package com.test.lockservice.service;

import org.springframework.transaction.annotation.Transactional;

/**
 * @Author sl
 */

public interface TicketService {

  @Transactional
  public void sellTicket();
}

修改service

package com.test.lockservice.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.test.lockservice.mapper.TicketMapper;
import com.test.lockservice.model.Ticket;
import com.test.lockservice.service.TicketService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @Author sl
 */
@Service

public class TicketServiceImpl implements TicketService {

    @Autowired
    private TicketMapper ticketMapper;

    @Override
    @Transactional(rollbackFor = {})
    public  void sellTicket() {
        // 查询所有结果
        List tickets = ticketMapper.findList();

        //取第一条记录,默认不为空指针,不做判空了哈
        Ticket ticket = tickets.get(0);

        if(ticket!= null && ticket.getCount()>0){
            ticket.setCount(ticket.getCount()-1);
            ticketMapper.updateById(ticket);
        }
    }
}

压测测试

 5000总票数,压测1000,压测无超卖现象,一定要用行级锁,否则性能太慢

分布式锁之mysql实现_第14张图片

mysql乐观锁解决失效问题

mysql乐观锁,采用加时间戳、版本号的方式采用cas的方式解决,mysql中没有提供cas的实现方式,需要在程序中手动实现,无需加事务注解,因为查询为for update,update本身也会加锁

添加版本号列

分布式锁之mysql实现_第15张图片

修改实体

package com.test.lockservice.model;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;

/**
 * @Author sl
 */
@TableName(value = "ticket")
public class Ticket {

    @TableId(value = "id",type = IdType.AUTO)
    private Integer id;

    private Integer count;

    private String sellCompany;
    
    private Integer version;

    public Integer getVersion() {
        return version;
    }

    public void setVersion(Integer version) {
        this.version = version;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public Integer getCount() {
        return count;
    }

    public void setCount(Integer count) {
        this.count = count;
    }

    public String getSellCompany() {
        return sellCompany;
    }

    public void setSellCompany(String sellCompany) {
        this.sellCompany = sellCompany;
    }
}

修改service

package com.test.lockservice.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.test.lockservice.mapper.TicketMapper;
import com.test.lockservice.model.Ticket;
import com.test.lockservice.service.TicketService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @Author sl
 */
@Service

public class TicketServiceImpl implements TicketService {

    @Autowired
    private TicketMapper ticketMapper;

    @Override
    public  void sellTicket() throws InterruptedException {
        // 查询所有结果
        List tickets = ticketMapper.findList();

        //取第一条记录,默认不为空指针,不做判空了哈
        Ticket ticket = tickets.get(0);

        if(ticket!= null && ticket.getCount()>0){
            ticket.setCount(ticket.getCount()-1);
            // 更新版本号
            Integer version = ticket.getVersion();
            ticket.setVersion(version+1);
            // 如果影响条数为0的话证明更新失败
            if(ticketMapper.update(ticket,new QueryWrapper().eq("id",ticket.getId()).eq("version",version))==0){
                Thread.sleep(20);
                this.sellTicket();
            }

        }
    }
}

压测测试 

 5000总票数,压测1000,压测无超卖现象

分布式锁之mysql实现_第16张图片小结

性能: 一个sql > 悲观锁 > JVM锁 > 乐观锁

在解决分布式锁的问题中,不要使用JVM锁,因为基本分布式问题,jvm锁都避免不了三种失效的场景,根据实际情况选择即可

你可能感兴趣的:(springCloud,分布式)