本地jvm锁
搭建本地卖票案例
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个请求测试买票,查看是否出现超卖问题
出现了超卖问题
本地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
查库操作,演示超卖现象
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<Ticket>().eq("sell_company", "12306"));
// 判断不为空和票数大于0
if(ticket!=null&& ticket.getCount() > 0){
ticket.setCount(ticket.getCount()-1);
ticketMapper.updateById(ticket);
}
}finally {
// lock.unlock();
}
}
}
5000总票数,压测5000,压测结果,显示超卖
加锁,本地锁解决超卖现象
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<Ticket>().eq("sell_company", "12306"));
// 判断不为空和票数大于0
if(ticket!=null&& ticket.getCount() > 0){
ticket.setCount(ticket.getCount()-1);
ticketMapper.updateById(ticket);
}
}finally {
lock.unlock();
}
}
}
5000总票数,压测5000,压测结果显示,可以解决超卖现象
本地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<Ticket>().eq("sell_company", "12306"));
// 判断不为空和票数大于0
if(ticket!=null&& ticket.getCount() > 0){
ticket.setCount(ticket.getCount()-1);
ticketMapper.updateById(ticket);
}
}finally {
lock.unlock();
}
}
}
5000总票数,压测1000,压测显示超卖现象
事务失效
@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<Ticket>().eq("sell_company", "12306"));
// 判断不为空和票数大于0
if(ticket!=null&& ticket.getCount() > 0){
ticket.setCount(ticket.getCount()-1);
ticketMapper.updateById(ticket);
}
}finally {
lock.unlock();
}
}
}
5000总票数,压测1000,压测显示超卖现象
集群部署失效
下载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
修改压测地址
5000总票数,压测1000,压测显示超卖现象
一条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<Ticket> {
@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悲观锁解决失效问题
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<Ticket> {
@Select("select * from ticket where sell_company='12306' for update")
List<Ticket> 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<Ticket> 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乐观锁解决失效问题
mysql乐观锁,采用加时间戳、版本号的方式采用cas的方式解决,mysql中没有提供cas的实现方式,需要在程序中手动实现,无需加事务注解,因为查询为for update,update本身也会加锁
添加版本号列
修改实体
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<Ticket> 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<Ticket>().eq("id",ticket.getId()).eq("version",version))==0){
Thread.sleep(20);
this.sellTicket();
}
}
}
}
压测测试
5000总票数,压测1000,压测无超卖现象
小结
性能: 一个sql > 悲观锁 > JVM锁 > 乐观锁
在解决分布式锁的问题中,不要使用JVM锁,因为基本分布式问题,jvm锁都避免不了三种失效的场景,根据实际情况选择即可