通过本文你将学习到
我们通过商品超卖的场景来测试验证不同情景下的锁实现。
目录
单机
未加锁
加锁
集群
编辑
单机加锁
分布式锁
数据库表锁
Redis的setNX实现
总结
单机开发场景中涉及并发同步时,往往采用Synchronized(同步)或同一个JVM内Lock机制来解决多线程间的同步问题。
// 不加锁
@GetMapping("/reduce")
public Response reduce() {
Good good = goodMapper.selectByPrimaryKey(1);
if (good == null || good.getCount() < 1) {
log.info("库存不足");
return Response.handleFail("库存不足");
}
goodMapper.reduceGoodCount(1);
log.info("扣除成功");
return Response.success();
}
商品good表:初始状态商品1库存为2
使用ab(Apache Benchmark)进行测试
#发送200个请求,并发为200
ab -n 200 -c 200 http://127.0.0.1:8081/test/reduce
日志输出:
可以看到扣除了多次,再看看库存结果,和明显超卖了不少:
// 加锁
@GetMapping("/reduce")
public synchronized Response reduce() {
Good good = goodMapper.selectByPrimaryKey(1);
if (good == null || good.getCount() < 1) {
//只观察扣除成功日志
//log.info("库存不足");
return Response.handleFail("库存不足");
}
goodMapper.reduceGoodCount(1);
log.info("扣除成功");
return Response.success();
}
将库存更新为2后再次测试
#发送200个请求,并发为200
ab -n 200 -c 200 http://127.0.0.1:8081/test/reduce
日志只有两条:
通过本地启动不同端口的两个进程模拟集群情况,用JMeter进行测试,添加两个线程组,每个线程组100个线程,并发请求两个进程下的相同接口模拟集群情况。
// 加锁
@GetMapping("/reduce")
public synchronized Response reduce() {
Good good = goodMapper.selectByPrimaryKey(1);
if (good == null || good.getCount() < 1) {
//只观察扣除成功日志
//log.info("库存不足");
return Response.handleFail("库存不足");
}
goodMapper.reduceGoodCount(1);
log.info("扣除成功");
return Response.success();
}
初始库存:
执行日志:
可以看到8081端口的扣除了4次,8082扣除了2次,很明显超买了1次,数据库库存为-1
关于分布式锁这里我们带来两种简单的实现方案
其他更多更高级的欢迎评论区留言讨论
测试的并发条件和库存和单机一致,下面就不重复贴出。
@GetMapping("/reduce")
public Response reduce() throws Exception{
Connection conn = dataSource.getConnection();
boolean connAutoCommit = conn.getAutoCommit();
conn.setAutoCommit(false);
PreparedStatement preparedStatement =
conn.prepareStatement("select * from good_lock where name = 'good_lock' for update" );
// 加锁
preparedStatement.execute();
Good good = goodMapper.selectByPrimaryKey(1);
if (good == null || good.getCount() < 1) {
// 提交释放锁
release(conn, connAutoCommit, preparedStatement);
return Response.handleFail("库存不足");
}
goodMapper.reduceGoodCount(1);
// 提交释放锁
release(conn, connAutoCommit, preparedStatement);
log.info("扣除成功");
return Response.success();
}
private static void release(Connection conn,
boolean connAutoCommit,
PreparedStatement preparedStatement) throws SQLException {
conn.commit();
conn.setAutoCommit(connAutoCommit);
conn.close();
preparedStatement.close();
}
锁实现类
@Slf4j
public class RedisLock implements AutoCloseable {
private RedisTemplate redisTemplate;
private static final String KEY = "good_lock";
private static final int EXPIRE_TIME = 30 * 1000;
private String VALUE ;
/**
* 没有传递 value,因为直接使用的是随机值
*/
public RedisLock(RedisTemplate redisTemplate){
this.redisTemplate = redisTemplate;
this.VALUE = UUID.randomUUID().toString();
}
@Override
public void close() throws Exception {
unLock();
}
/**
* 获取分布式锁
* SET key value NX PX 30000
* 每一个线程对应的随机值 key value 不一样,用于释放锁的时候校验
* NX 表示 key 不存在的时候成功,key 存在的时候设置不成功
* PX 表示过期时间
*/
public boolean getLock(){
RedisCallback redisCallback = connection -> {
//设置NX
RedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.ifAbsent();
//设置过期时间
Expiration expiration = Expiration.seconds(EXPIRE_TIME* 1000);
//序列化key
byte[] redisKey = redisTemplate.getKeySerializer().serialize(KEY);
//序列化value
byte[] redisValue = redisTemplate.getValueSerializer().serialize(VALUE);
//执行setnx操作
Boolean result = connection.set(redisKey, redisValue, expiration, setOption);
return result;
};
//获取分布式锁
Boolean lock = (Boolean)redisTemplate.execute(redisCallback);
return lock;
}
/**
* 释放锁的时候随机数相同的时候才可以释放,避免释放了别人设置的锁(自己的已经过期了所以别人才可以设置成功)
* 释放的时候采用 LUA 脚本,因为 delete 没有原生支持删除的时候校验值,证明是当前线程设置进去的值
* 脚本是在官方文档里面有的
*/
public boolean unLock() {
// key 是自己才可以释放,不是就不能释放别人的锁
String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
" return redis.call(\"del\",KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end";
RedisScript redisScript = RedisScript.of(script,Boolean.class);
List keys = Arrays.asList(KEY);
// 执行脚本的时候传递的 value 就是对应的值
Boolean result = (Boolean)redisTemplate.execute(redisScript, keys, VALUE);
return result;
}
}
业务方法
@GetMapping("/reduce")
public Response reduce() throws Exception{
// try-resource-close
try (RedisLock lock = new RedisLock(redisTemplate)) {
// 获取锁
if (lock.getLock()) {
Good good = goodMapper.selectByPrimaryKey(1);
if (good == null || good.getCount() < 1) {
return Response.handleFail("库存不足");
}
goodMapper.reduceGoodCount(1);
log.info("扣除成功");
return Response.success();
}
} catch (InterruptedException e) {
log.error("e", e);
} catch (Exception e) {
log.error("e", e);
}
// 没有获取到锁
return Response.handleFail("请重试");
}
该锁情况下并发测试通过,测试case同数据库表锁。
分布式集群情况下,无法通过单机的锁来避免高并发下的超买等问题,该篇文章场景是业务场景的减化版,并不能直接用于生产,所以使用的简单的分布式锁,在实际场景中还需要考虑事物,加锁的时机等。当然还有其他的分布式锁,欢迎评论区留言交流