勿以浮沙筑高台
当我们多用户请求的时候,多个线程去拿一个内存地址进行修改,线程还没修改完,另一个线程又进行了读取,读取的值并不是修改后的值而是原始值,这个时候就会出现修改值错误的情况,比如A线程拿值为10,这个时候A线程进行修改为9,但B线程在还没开始修改之前也拿到了值为10,这个时候就会修改为9,并不是我想要的8.
e## 代码模拟
public class SynchronizeTest {
public int i=20;
public void get(){
if (i>0){
i--;
System.out.println(i);
}
}
}
public static void main( String[] args )
{
SynchronizeTest sy = new SynchronizeTest();
for (int i = 0; i < 30; i++) {
new Thread(()->{
sy.get();
}).start();
}
}
为了解决这种多线程情况下的并发问题,我们已堵塞线程为代价换取数据的准确性,阻塞线程的过程就称之为锁。
在java中我们用synchronize关键字进行锁的管理,即锁定一个资源每次只能拥有一个线程进行访问。
基础语法:
public class SynchronizeTest {
public int i=20;
public void get(){
synchronized(this.getClass()){
if (i>0){
i--;
System.out.println(i);
}
}
}
}
需要主要的是synchronize()括号当中必须在当前JVM中是唯一的,因为如果不是唯一的话,多个字段标记为一处锁的地段,还是能多个线程拿取。所以这里用this.getclass()唯一标记
悲观锁,对一张表中的一条行数据或者表数据进行锁定,当表中的数据修改完成后其他的sql语句才会执行
实现方式:select …for update
select * from books FOR UPDATE;
当查询的where 条件中带有主键时,会进行行锁(row lock)
select * from books where id =3 FOR UPDATE;
当where条件中没有主键时,会进行表锁(table lock)
select * from books where bookname like “%书%” FOR UPDATE;
开启事务后,只要我们的查询没有没有进行最终的commit对于其他的用户是不能对数据进行查询的。有名排它锁。
乐观锁及在数据库字段增加一个version字段,当数据发生更改时version的数据由0变为1。当其他锁进行修改
SELECT version as ver,count as ct FROM BOOKS WHERE ID = 1
update books set version+1 , count -1 WHERE count = ct and VERSION = ver
乐观锁利用的是update操作加了锁的特性,增加一个查询字段代表历史版本当版本更改,其余操作都是无效的特点实现了锁的步骤
总结
上面2种方式都是基于数据库实现的分布式锁,但是基于数据库方式实现锁的效率并不高,我们完全可以基于分布式系统的特点来实现锁的效果。比如在zookeeper中,我们利用节点的创建和监听实现锁的创建,即每个线程创建一个零时节点,当线程操作完或关闭删除节点,触发下一个线程的监听事件,监听启动拿到锁执行业务逻辑操作数据库。具体逻辑见文章:
Zookeeper分布式锁解决羊群效应的方案
接下来说Redis锁的实现逻辑
即创建了锁的线程执行业务代码service,执行完后删除节点。
伪代码如下:
public boolean lock(Integer goodNo) {
// 分布式锁的key
String key = "redis-lock";
// 根据key获取key的值是否存在
Object result = redisTemplate.opsForValue().get(key);
if (result != null) {
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (Exception e) {
e.printStackTrace();
}
// 锁还没有释放,递归获取锁\
logger.info("尝试获取锁");
orderV1(goodNo);
} else {
// 说明锁还没有设置
redisTemplate.opsForValue().set(key,UUID.randomUUID());
//执行业务代码
//拿到数据
StockInfo stockInfo = stockInfoMapper.selectById(id);
if (stockInfo != null ) {
//执行业务逻辑
service.executeService();
//记录日志
logger.info("业务逻辑成功");
// 释放锁,在执行释放锁上面的时候已经出现了异常,意味着锁没有释放,那么其他线程不可能在获取锁,导致死锁
redisTemplate.delete(key);
return true;
}
}
return false;
}
表面上这样看好像没有什么问题,但是仔细想想,倘若主机A拿到数据执行业务的时候宕机了,没有执行到删除节点代码,那么就出现死锁了。
为了避免的上面出现的问题,我们创建的时候给节点创建一个过期时间。
//新增过期时间30秒
redisTemplate.opsForValue().set(key,UUID.randomUUID(),Duration.ofSeconds(30));
增加一个报错删除节点
finally {
//增加trycatch,当报错的时候要删除锁
redisTemplate.delete(key);
logger.info("释放了锁");
}
继续思考临界值:
这个操作的根本原因就是不是原子操作,分步的一定会有这样的问题。
问题解决:
//新增一个线程副本变量
private static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();
public boolean lock(Integer goodNo) {
//在执行写入redis之前进行副本判断
//从redis中拿到返回值进行判断
result = redisTemplate.opsForValue().get(key);
//拿到生成的UUID
String threadId = THREAD_LOCAL.get();
//threadId和redisValue不为空,并且相等时
//线程重入不需要拿锁
if (StringUtils.hasText(threadId)
&& StringUtils.hasText(redisValue)
&& threadId.equals(redisValue)) {
//执行业务代码
service.executeService();
//删除锁
redisTemplate.delete(key);
return true;
}
//如果不是则将生成的UUID写入线程副本当中
THREAD_LOCAL.set(UUID);
//回到上面的代码lock方法中业务逻辑()
}
String uuidnew = redisTemplate.opsForValue().get(key)+"";
if(uuidnew ==uuid){ //当一样时才删除。
//删除key
redisTemplate.delete(key);
}
继续思考:
如果在删除的过程中发生报错了怎么办,redis内部报错,导致删除失败的情况
因为Lua脚本具有原子性,当lua脚本执行发生报错时会回滚数据。
lock.lua
-- Set a lock
-- 如果获取锁成功,则返回 1
local key = KEYS[1] --key
local content = KEYS[2] --UUID
local ttl = ARGV[1] --过期时间
local lockSet = redis.call('setnx', key, content) --调用setnx方法设置值
if lockSet == 1 then --如果等于1则表示拿到锁了
redis.call('pexpire', key, ttl) --设置过期时间
else
-- 如果value相同,则认为是同一个线程的请求,则认为重入锁
local value = redis.call('get', key) --拿Key
if(value == content) then --如果值是相同的则重入
lockSet = 1;
redis.call('pexpire', key, ttl) --重新刷新下过期时间,返回1,表示拿到了锁
end
end
return lockSet
local key = KEYS[1] --keyname
local content = KEYS[2] --UUID
local value = redis.call('get', key) --拿到redis中的UUID
if value == content then --如果UUID值相等才删除
return redis.call('del', key);
end
return 0
伪代码:
public boolean lock(Integer goodNo) {
//设置key
String key = "lock-redis";
//拿到UUID
String uuid = UUID.randomUUID().toString();
//设置超时时间
Duration duration = Duration.ofSeconds(30);
//调用lua脚本
int off = LuaManager.execute("lock.lua",key,uuid , duration );
if(off=1){//代表创建成功
try{
//执行业务代码
Service.execute();
}cathch(ex){
ex.message();
}finnaly {
//调用unlock.lua脚本
int off = LuaManager.execute("unlock.lua",key,uuid);
}
}
}