做程序开发时,都会往并发方面来设计,分布式,集群,负载均衡,读写分离,主从分离,分库分表,缓存等等等手段来提高自己应用服务器的访问负载量 但是也会并发出现另一个问题
当两个线程同时查询到A这个数据 并且都需要对A的某一项数据进行增减这种修改动作时
就拿库存来说
库存为100 A,B两个用户买东西时,同时获取到100个库存,各自买了一个,都是从100个库存的基础上去掉了一个,这时候库存明明没了两个 却还是99个库存,是不是就会出问题?
如果A获取到了100个库存,买了一个 剩下99 B在A买完之后的基础上买一个 99-1=98 这样就不会出错了吧
好了,这时候有一个精神小伙就站出来了----》锁
悲观锁和乐观锁
一,悲观锁和乐观锁
悲观锁是基于一种悲观的态度类来防止一切数据冲突,它是以一种预防的姿态在修改数据之前把数据锁住,然后再对数据进行读写,在它释放锁之前任何人都不能对其数据进行操作,直到前面一个人把锁释放后下一个人数据加锁才可对数据进行加锁,然后才可以对数据进行操作,一般数据库本身锁的机制都是基于悲观锁的机制实现的;
1.悲观锁(Pessimistic Lock) 一个对生活态度很悲观的锁,内心极度缺乏安全感,它总是以为别人会来偷它的数据,所以它就来预防其它线程来防止数据冲突,每次修改数据之前都会把数据锁住,然后再进行读写操作,直到它释放
Java synchronized 就属于悲观锁的一种实现,每次线程要修改数据时都先获得锁,保证同一时刻只有一个线程能操作数据,其他线程则会被block。加锁 操作 释放 增加消耗,所以性能不高
2.乐观锁(Optimistic Lock) 有很乐观态度的锁,从来不担心别人会来抢它东西,操作数据时不会对操作的数据进行加锁(这使得多个任务可以并行的对数据进行操作),只有到数据提交的时候才通过一种机制来验证数据是否存在冲突
使用版本号或者时间戳,每次做数据操作时都把 版本号或者时间戳一起查询出来,做修改时都会把这个版本号或者时间戳做为查询条件之一,这样当版本号或者时间戳与数据库修改的对象的版本号或者时间戳不一致时,修改条件不成立
二,分布式锁实现方式
基于数据库实现;
基于缓存等实现;
基于Zookeeper实现;
1.基于数据库实现分布式锁
当时我基于数据库做这个分布式锁时,采用的就是这种形式,首先自定义注解,将这注解加到需要上锁的方法上,注解需要标识方法的编号
@ZkbLock('001')
public String updateXXX(X x){
........
return XXX
}
那么只要一进入这个方法,我就会去 method_lock表插入一条method_code为001的一条数据就会被锁住
CREATE TABLE `method_lock` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`method_code` varchar(64) NOT NULL DEFAULT '' COMMENT '锁定的方法的编码',
`desc` varchar(1024) NOT NULL DEFAULT '备注信息',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存数据时间,自动生成',
PRIMARY KEY (`id`),
UNIQUE KEY `uidx_method_code` (`method_code`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';
当我们想要锁住某个方法时,执行以下SQL:
insert into method_lock(method_code,desc) values ('001','修改库存')
因为我们对method_code
做了唯一性约束,这里如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,可以执行方法体内容。
当方法执行完毕之后,想要释放锁的话,需要执行以下Sql
delete from method_lock where method_code ='001'
1.1.这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。
解决办法: 搞两个数据库,数据之前双向同步。一旦挂掉快速切换到备库上。
1.2.这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。
解决办法: 做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍。
1.3.这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。
解决办法: 搞一个while循环,直到insert成功再返回成功。
1.4.这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。
解决办法:需要在表中新增一列,用于记录当前获取到锁的机器和线程信息,在再次获取锁的时候,先查询表中机器和线程信息是否和当前机器线程相同,若相同则直接获取锁。
2.基于缓存
Redis/MongoDB/Tair等缓存都可以实现分布式锁
那就来一个最常用的redis分布式锁
分布式锁的原则
1、相互排斥:使用setnx命令保证互斥性,任意时刻,只能有一个客户端持有锁。
2、无死锁:需要设置锁的过期时间,避免死锁,有所的客户端宕机或网络延迟下仍然可以获得此锁。
3、有始有终:一个客户端加了锁只能自己解锁。加锁的Value 值为一个唯一标示。可以采用UUID作为唯一标示。加锁成功后需要把唯一标示返回给客户端来用来客户端进行解锁操作
4、容错性:只要大部分的redis节点还存活,a那么客户端就能正常加锁和释放锁。setnx和设置过期时间需要保持原子性,避免在设置setnx成功之后在设置过期时间客户端崩溃导致死锁
public static boolean acquireLock(String lock) {
// 1. 通过SETNX试图获取一个lock
boolean success = false;
Jedis jedis = pool.getResource();
long value = System.currentTimeMillis() + expired + 1;
System.out.println(value);
long acquired = jedis.setnx(lock, String.valueOf(value));
//SETNX成功,则成功获取一个锁
if (acquired == 1)
success = true;
//SETNX失败,说明锁仍然被其他对象保持,检查其是否已经超时
else {
long oldValue = Long.valueOf(jedis.get(lock));
//超时
if (oldValue < System.currentTimeMillis()) {
String getValue = jedis.getSet(lock, String.valueOf(value));
// 获取锁成功
if (Long.valueOf(getValue) == oldValue)
success = true;
// 已被其他进程捷足先登了
else
success = false;
}
//未超时,则直接返回失败
else
success = false;
}
pool.returnResource(jedis);
return success;
}
//释放锁
public static void releaseLock(String lock) {
Jedis jedis = pool.getResource();
long current = System.currentTimeMillis();
// 避免删除非自己获取得到的锁
if (current < Long.valueOf(jedis.get(lock)))
jedis.del(lock);
pool.returnResource(jedis);
}
//--------------------------
public Long acquireLock(final String lockName,final long expire){
return redisTemplate.execute(new RedisCallback() {
public Long doInRedis(RedisConnection connection) {
byte[] lockBytes = redisTemplate.getStringSerializer().serialize(lockName);
boolean locked = connection.setNX(lockBytes, lockBytes);
connection.expire(lockBytes, expire);
if(locked){
return 1L;
}
return 0L;
}
});
}
//原子操作 -----------------------
public String getAndSet(final String key,final String value){
return redisTemplate.execute(new RedisCallback() {
@Override
public String doInRedis(RedisConnection connection)
throws DataAccessException {
byte[] result = connection.getSet(redisTemplate.getStringSerializer().serialize(key),
redisTemplate.getStringSerializer().serialize(value));
if(result!=null){
return new String(result);
}
return null;
}
});
}
3.基于zookeeper
1,启动服务,应用与zookeeper建立连接,同时建立根节点ROOT_LOCK。
2,在需要加锁的业务代码前调用lock方法加锁,业务代码后调用unlock方法解锁。
3,客户端连接zookeeper建立连接,并在lock_name下建立临时且有序的子节点,例如:node_1,第二个为node_1,以此类推。
4,客户端获取同一lock_name下所有子节点,判断自己是否序号最小,如果是,执行业务代码,业务代码执行完,释放锁,删掉临时节点,如果不是,zookeeper监听临时节点变更,直到获取到锁。