现在有一个需求,在不使用redis的前提下,设计一个可以多节点共同访问的自增ID系统
首先我想到的就是秒杀抢单的时候,我们常用的悲观锁原则,不然,在高并发场景下,利用数据库来做计数是无法保证数据安全的,也就是常说的,锁不住。
表设计(先设计一个最简单的表:当前需求是每天生成一套计数器):
CREATE TABLE `test_key` (
`key` int(6) DEFAULT '0',
`data_time` varchar(16) NOT NULL,
PRIMARY KEY (`data_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
现在来梳理一下流程:
1.查询当前标号(锁表):
SELECT `key` + 1 AS `key` FROM test_key WHERE data_time = "2021-12-15" FOR UPDATE
2.将当前标号加一:
UPDATE test_key SET `key` = `key` + 1 WHERE data_time = "2021-12-15"
3.如果查询到没有当天的数据,则新建一条数据:
INSERT INTO test_key (data_time) VALUES ("2021-12-16")
现在,来看在SpringBoot项目中是怎么实现这个流程的
dao层:
@Mapper
public interface KeyMapper {
@Select("SELECT `key` + 1 AS `key` FROM p1project.test_key WHERE data_time = #{dataTime} FOR UPDATE")
Integer getKeyByDataTime(@Param("dataTime")String dataTime);
@Update("UPDATE p1project.test_key SET `key` = `key` + 1 WHERE data_time = #{dataTime}")
void updateKey(@Param("dataTime")String dataTime);
@Insert("INSERT INTO p1project.test_key (data_time) VALUES (#{dataTime})")
void insertKey(@Param("dataTime")String dataTime);
}
service层(隔离机制:Isolation.READ_COMMITTED【读已提交】):
@Service
public class KeyService {
@Resource
private KeyMapper keyMapper;
@Transactional(isolation = Isolation.READ_COMMITTED)
public int getKeyByData() throws Exception{
String dataTime = DateUtil.formatDate(new DateTime());
Integer key = keyMapper.getKeyByDataTime(dataTime);
if (key == null){
keyMapper.insertKey(dataTime);
return 0;
} else {
keyMapper.updateKey(dataTime);
return key;
}
}
}
controller层:
/**
* 获取行业列表
* @param response
* @param request
* @throws IOException
*/
@GetMapping("/test")
@ResponseBody
public void test(HttpServletResponse response, HttpServletRequest request) throws IOException {
int key = 0;
try {
key = keyService.getKeyByData();
} catch (Exception e) {
//避免高并发场景下的新增唯一键冲突,做一次try-catch,保证事务完整性的前提下重试一次,取得计数
try {
key = keyService.getKeyByData();
} catch (Exception exception) {
log.error("处理异常",e);
}
}
JSONObject result = SystemUtils.responseOk(0, "success", key);
response.setContentType("json/application;charset=UTF-8");
response.getWriter().print(result.toString());
}
正如注释中写的:避免高并发场景下的新增唯一键冲突,做一次try-catch,保证事务完整性的前提下重试一次,取得计数。当不存在唯一键的时候,如果不做try-catch的话,会导致同时插入唯一键冲突。
可以开一个多线程模拟测试一下:
public class DemoRun implements Runnable{
//重写的是Runnable接口的run()
@Override
public void run() {
int outTime = 1000*60*10;
String result = HttpUtil.get("http://localhost:8084/test",outTime);
System.out.println(result);
}
public static void main(String[] args) {
for (int run=0;run<5;run++){
Thread thread1 = new Thread(new DemoRun());
Thread thread2 = new Thread(new DemoRun());
Thread thread3 = new Thread(new DemoRun());
Thread thread4 = new Thread(new DemoRun());
Thread thread5 = new Thread(new DemoRun());
Thread thread6 = new Thread(new DemoRun());
thread1.start();
thread2.start();
thread3.start();
thread4.start();
thread5.start();
thread6.start();
}
}
}
测试结果如下
可见,开启多线程访问并不会导致重复ID产生,在并发的情况下能保证数据的可靠性