在并发场景下,多个进程/线程同时对同一个资源进行访问时,会产生冲突。
举个例子:核酸采样时,如果一次100个人同时要求大白进行采样(并发),那么大白就要崩溃了,所以必须要控制一个大白一次只能对一个人采样,其他人等待采样完成,这就是对大白进行”加锁”。
锁是用来解决并发问题的,如:
锁的使用场景:
通过JAVA程序连接Redis,提示以下错误:
redis.clients.jedis.exceptions.JedisConnectionException: FAIled to connect to any host resolved for DNS name.
at redis.clients.jedis.DefaultJedisSocketFactory.connectToFirstSuccessfulHost(DefaultJedisSocketFactory.java:63)
at redis.clients.jedis.DefaultJedisSocketFactory.createSocket(DefaultJedisSocketFactory.java:87)
at redis.clients.jedis.Connection.connect(Connection.java:180)
at redis.clients.jedis.Connection.initializeFromClientConfig(Connection.java:338)
可能有如下原因:
//查看6379端口状态 mo表示未开放
[root@192 bin]# firewall-cmd --zone=public --query-port=6379/tcp
no
//配置放行6379端口
[root@192 bin]# firewall-cmd --zone=public --add-port=6379/tcp --permanent
success
//防火墙重载
[root@192 bin]# firewall-cmd --reload
success
//再次查看端口状态
[root@192 bin]# firewall-cmd --zone=public --query-port=6379/tcp
yes
[root@192 redis]# vim redis.conf
将配置改成如下所示:
# 允许任何主机连接、访问
bind 0.0.0.0
# 关闭保护模式
protected-mode no
# 允许启动后在后台运行,即关闭命令行窗口后仍能运行
daemonize yes
DROP TABLE IF EXISTS `t_goods`;
CREATE TABLE `t_goods` (
`id` int(11) NOT NULL,
`name` varchar(60) DEFAULT NULL,
`qty` int(11) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
--初始化数量qty=20
INSERT INTO `t_goods` VALUES ('1', '华为nova7', '20');
DROP TABLE IF EXISTS `t_order`;
CREATE TABLE `t_order` (
`oid` varchar(120) NOT NULL,
`createtime` datetime DEFAULT NULL,
`goodname` varchar(255) DEFAULT NULL,
`user` varchar(120) DEFAULT NULL,
PRIMARY KEY (`oid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
public static void main(String[] args) throws ParseException {
//设置秒杀开始时间
String startTime="2022-4-27 23:24:00";
SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date start=simpleDateFormat.parse(startTime);
System.out.println("等待中...");
boolean isStart=false;
while (!isStart) {
if (start.compareTo(new Date()) < 0) {
isStart=true;
System.out.println("秒杀开始...");
//同时启动10000条线程
CountDownLatch countDownLatch = new CountDownLatch(10000);
for (int i = 0; i < 10000; i++) {
new Thread(() -> {
try {
countDownLatch.await();
//secKillGoodsByRedisLock();//Redis锁
secKillGoods(); //未加锁
// secKillGoodsByLock();//synchronized
} catch (InterruptedException | SQLException e) {
e.printStackTrace();
}
}).start();
countDownLatch.countDown();
}
}
}
}
private static void secKillGoods() throws SQLException {
List list = DButil.query("select id,name,qty from t_goods where id=1 and qty>0");
//判断是否还有库存
if(list.size()>0){
String id=JedisUtil.getId();
Object[] insertObject={id,new Date(),"nova7",Thread.currentThread().getName()};
DButil.excuteDML("update t_goods set qty=qty-1 where id=1 ");
DButil.excuteDML("insert into t_order values(?,?,?,?);",insertObject);
System.out.println(Thread.currentThread().getName()+">>>抢到了."+id);
}
}
以上代码每次执行都会先判断是否还有库存,如果有库存则秒杀成功,否则秒杀失败,没有并发的情况下是可以正常运行的,但是一旦存在并发,则会出现负库存(超卖)
private static void secKillGoodsByLock() throws SQLException {
synchronized (lockObj) {
secKillGoods();
}
}
以上程序在进入秒杀方法时,都会通过synchronized关键字加锁,再次运行程序,我们发现不会出现负库存了
但是如果在多进程或者分布式环境中,synchronized关键字会失效,让我们再启动一个进程,两个进程同时启动100000个线程进行秒杀(ps:同时启动十万个线程差点让我的电脑没缓过来…),终于出现了负库存的现象
private static void secKillGoodsByRedisLock(){
String id="1";
String key="lock"+id;
JedisUtil jedisUtil=new JedisUtil();
String lockId=jedisUtil.getLock(key,5);
if(null!=lockId){
try{
List list= DButil.query("select id,name,qty from t_goods where id=1 and qty>0");
if(list!=null && list.size()>0){
Object[] insertObject={lockId,new Date(),"nova7",Thread.currentThread().getName()};
DButil.excuteDML("update t_goods set qty=qty-1 where id=1 ");
DButil.excuteDML("insert into t_order values(?,?,?,?);",insertObject);
System.out.println(Thread.currentThread().getName()+">>>抢到了."+lockId);
jedisUtil.unLock(key,lockId);
}else {
System.out.println("抢完了");
}
} catch (SQLException e) {
jedisUtil.unLock(key,lockId);
e.printStackTrace();
}
}
}
以上代码只有在获取到Redis锁成功后,才会去执行扣库存和下单的逻辑,重复和上一步一样,两个进程同时启动100000个线程进行秒杀,看看结果
以上结果没有出现负库存的现象,显然是扛住了“秒杀”,getLock 的实现如下所示,其原理就是利用Redis setnx的原子性操作来控制并发,以下示例还设置了锁失效的时间,避免死锁。当然还有许多的问题需要在实际应用场景中考虑,如在锁失效时间到了,秒杀动作未完成如何处理,Redis服务器崩溃了怎么办等等。
public String getLock(String key,int timeout){
Jedis jedis=null;
try {
jedis=getJedis();
String value=getId();
long end=System.currentTimeMillis()+timeout;
while (System.currentTimeMillis()<end) {
//设置value成功,获取锁
if(jedis.setnx(key,value)==1){
//设置失效时间
jedis.expire(key,timeout);
System.out.println(Thread.currentThread().getName()+">>>>>获取锁成功。");
return value;
}
//当 key 存在但没有设置剩余生存时间时
if(jedis.ttl(key)==-1){
//设置失效时间
jedis.expire(key,timeout);
}
}
} catch (Exception e) {
e.printStackTrace();
}finally {
if(null!=jedis){
jedis.close();
}
}
return null;
}
参考文献:
为什么需要锁,锁分类,锁粒度
各种锁以及使用场景